Compare commits
25 Commits
master
...
build-vagr
Author | SHA1 | Date |
---|---|---|
Chris Roberts | 9f1914642f | |
Chris Roberts | 2ad8abd57c | |
Chris Roberts | c789fbf440 | |
Chris Roberts | 5c09116f53 | |
Chris Roberts | 1651269333 | |
Chris Roberts | 0d9ad25d69 | |
Chris Roberts | f4420f6b81 | |
Chris Roberts | fe7ac740df | |
Chris Roberts | 5605a6e843 | |
Chris Roberts | dcac2a167f | |
Chris Roberts | 6e9fc12b08 | |
Chris Roberts | 248f902345 | |
Chris Roberts | 7b8b75593b | |
Chris Roberts | ca58714a03 | |
Chris Roberts | a04e24378b | |
Chris Roberts | bac7689b36 | |
Chris Roberts | 338cf4cc0b | |
Chris Roberts | c441fac0db | |
Chris Roberts | db249d58ac | |
Chris Roberts | 8952168480 | |
Chris Roberts | 893771e535 | |
Chris Roberts | 9499553706 | |
Chris Roberts | 8aa0fd9445 | |
Chris Roberts | 9f8d883852 | |
Chris Roberts | b11c86528a |
|
@ -48,5 +48,10 @@ doc/
|
||||||
.ruby-version
|
.ruby-version
|
||||||
.rvmrc
|
.rvmrc
|
||||||
|
|
||||||
|
# Extensions
|
||||||
|
*.so
|
||||||
|
*.bundle
|
||||||
|
tmp/*
|
||||||
|
|
||||||
# Box storage for spec
|
# Box storage for spec
|
||||||
test/vagrant-spec/boxes/*.box
|
test/vagrant-spec/boxes/*.box
|
||||||
|
|
19
.travis.yml
19
.travis.yml
|
@ -4,10 +4,17 @@ sudo: false
|
||||||
|
|
||||||
cache: bundler
|
cache: bundler
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- which go
|
||||||
|
- sudo apt-get remove --purge golang-go
|
||||||
|
- sudo add-apt-repository ppa:gophers/archive -y
|
||||||
|
- sudo apt-get update -q
|
||||||
|
- sudo apt-get install golang-1.11-go -yq
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
packages:
|
packages:
|
||||||
- bsdtar
|
- bsdtar
|
||||||
|
|
||||||
rvm:
|
rvm:
|
||||||
- 2.3.8
|
- 2.3.8
|
||||||
|
@ -22,5 +29,13 @@ branches:
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- NOKOGIRI_USE_SYSTEM_LIBRARIES=true
|
- NOKOGIRI_USE_SYSTEM_LIBRARIES=true
|
||||||
|
- GO111MODULE=on
|
||||||
|
- GOPATH=$HOME/go
|
||||||
|
- GOROOT=/usr/lib/go-1.11
|
||||||
|
- PATH=/usr/lib/go-1.11/bin:$PATH
|
||||||
|
|
||||||
script: bundle exec rake test:unit
|
script:
|
||||||
|
- go version
|
||||||
|
- go test ./...
|
||||||
|
- bundle exec rake compile
|
||||||
|
- bundle exec rake test:unit
|
||||||
|
|
5
Rakefile
5
Rakefile
|
@ -1,5 +1,10 @@
|
||||||
require 'rubygems'
|
require 'rubygems'
|
||||||
require 'bundler/setup'
|
require 'bundler/setup'
|
||||||
|
require 'rake/extensiontask'
|
||||||
|
|
||||||
|
Rake::ExtensionTask.new "go-plugin" do |ext|
|
||||||
|
ext.lib_dir = "lib/vagrant/go_plugin"
|
||||||
|
end
|
||||||
|
|
||||||
# Immediately sync all stdout so that tools like buildbot can
|
# Immediately sync all stdout so that tools like buildbot can
|
||||||
# immediately load in the output.
|
# immediately load in the output.
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
lib = File.expand_path('./../../../lib', File.expand_path(__FILE__))
|
||||||
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||||
|
|
||||||
|
require 'mkmf'
|
||||||
|
require 'time'
|
||||||
|
|
||||||
|
find_executable('go')
|
||||||
|
|
||||||
|
go_version = /go version go(\d+\.\d+)/.match(`go version`).captures.first
|
||||||
|
raise "'go' version >=1.5.0 is required, found go #{go_version}" unless Gem::Dependency.new('', '>=1.5.0').match?('', go_version)
|
||||||
|
|
||||||
|
makefile = "Makefile"
|
||||||
|
makefile_content = <<MFEND
|
||||||
|
NAME := #{File.basename(File.dirname(File.expand_path(__FILE__)))}
|
||||||
|
BINARY := ${NAME}.so
|
||||||
|
|
||||||
|
V = 0
|
||||||
|
Q1 = $(V:1=)
|
||||||
|
Q = $(Q1:0=@)
|
||||||
|
ECHO1 = $(V:1=@:)
|
||||||
|
ECHO = $(ECHO1:0=@echo)
|
||||||
|
|
||||||
|
SOURCEDIR=.
|
||||||
|
SOURCES := $(shell find $(SOURCEDIR) -maxdepth 0 -name '*.go')
|
||||||
|
|
||||||
|
VERSION=1.0
|
||||||
|
BUILD_DATE=#{Time.now.iso8601}
|
||||||
|
|
||||||
|
cflags= $(optflags) $(warnflags)
|
||||||
|
optflags= -O3 -fno-fast-math
|
||||||
|
warnflags= -Wall -Wextra -Wno-unused-parameter -Wno-parentheses -Wno-long-long -Wno-missing-field-initializers -Wunused-variable -Wpointer-arith -Wwrite-strings -Wimplicit-function-declaration -Wdiv-by-zero -Wdeprecated-declarations
|
||||||
|
CCDLFLAGS= -fno-common
|
||||||
|
INCFLAGS= -I#{RbConfig::CONFIG['rubyhdrdir']}/ -I#{RbConfig::CONFIG['rubyarchhdrdir']}/ -I$(SOURCEDIR)
|
||||||
|
CFLAGS= $(CCDLFLAGS) $(cflags) -fno-common -pipe $(INCFLAGS)
|
||||||
|
|
||||||
|
LDFLAGS=-L#{RbConfig::CONFIG['libdir']} #{RbConfig::CONFIG['LIBRUBYARG']}
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := $(BINARY)
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
${ECHO} ${VERSION}
|
||||||
|
${ECHO} ${BUILD_DATE}
|
||||||
|
|
||||||
|
all:
|
||||||
|
make clean
|
||||||
|
$(BINARY)
|
||||||
|
|
||||||
|
$(BINARY): $(SOURCES)
|
||||||
|
CGO_CFLAGS="${CFLAGS}" CGO_LDFLAGS="${LDFLAGS}" go build -buildmode=c-shared -o ${BINARY} #{File.dirname(__FILE__)}/../../ext/${NAME}/
|
||||||
|
|
||||||
|
.PHONY: install
|
||||||
|
install:
|
||||||
|
# go install ${LDFLAGS} ./...
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi
|
||||||
|
if [ -f lib/${BINARY} ] ; then rm lib/${BINARY} ; fi
|
||||||
|
MFEND
|
||||||
|
|
||||||
|
puts "creating Makefile"
|
||||||
|
File.open(makefile, 'w') do |f|
|
||||||
|
f.write(makefile_content.gsub!(/(?:^|\G) {2}/m,"\t"))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
$makefile_created = true
|
|
@ -0,0 +1,139 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"C"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
hclog "github.com/hashicorp/go-hclog"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Plugins *plugin.VagrantPlugin
|
||||||
|
|
||||||
|
//export Setup
|
||||||
|
func Setup(enableLogger, timestamps bool, logLevel *C.char) bool {
|
||||||
|
lvl := to_gs(logLevel)
|
||||||
|
lopts := &hclog.LoggerOptions{Name: "vagrant"}
|
||||||
|
if enableLogger {
|
||||||
|
lopts.Output = os.Stderr
|
||||||
|
} else {
|
||||||
|
lopts.Output = ioutil.Discard
|
||||||
|
}
|
||||||
|
if !timestamps {
|
||||||
|
lopts.TimeFormat = " "
|
||||||
|
}
|
||||||
|
lopts.Level = hclog.LevelFromString(lvl)
|
||||||
|
vagrant.SetDefaultLogger(hclog.New(lopts))
|
||||||
|
|
||||||
|
if Plugins != nil {
|
||||||
|
Plugins.Logger.Error("plugins setup failure", "error", "already setup")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
Plugins = plugin.VagrantPluginInit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
//export LoadPlugins
|
||||||
|
func LoadPlugins(plgpath *C.char) bool {
|
||||||
|
if Plugins == nil {
|
||||||
|
vagrant.DefaultLogger().Error("cannot load plugins", "error", "not setup")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
p := to_gs(plgpath)
|
||||||
|
err := Plugins.LoadPlugins(p)
|
||||||
|
if err != nil {
|
||||||
|
Plugins.Logger.Error("failed loading plugins",
|
||||||
|
"path", p, "error", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Plugins.Logger.Info("plugins successfully loaded", "path", p)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
//export Reset
|
||||||
|
func Reset() {
|
||||||
|
if Plugins != nil {
|
||||||
|
Plugins.Logger.Info("resetting loaded plugins")
|
||||||
|
Teardown()
|
||||||
|
dirs := Plugins.PluginDirectories
|
||||||
|
Plugins.PluginDirectories = []string{}
|
||||||
|
for _, p := range dirs {
|
||||||
|
Plugins.LoadPlugins(p)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Plugins.Logger.Warn("plugin reset failure", "error", "not setup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export Teardown
|
||||||
|
func Teardown() {
|
||||||
|
// only teardown if setup
|
||||||
|
if Plugins == nil {
|
||||||
|
vagrant.DefaultLogger().Error("cannot teardown plugins", "error", "not setup")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Plugins.Logger.Debug("tearing down any active plugins")
|
||||||
|
Plugins.Kill()
|
||||||
|
Plugins.Logger.Info("plugins have been halted")
|
||||||
|
}
|
||||||
|
|
||||||
|
//export ListProviders
|
||||||
|
func ListProviders() *C.char {
|
||||||
|
list := map[string]interface{}{}
|
||||||
|
r := &Response{Result: list}
|
||||||
|
if Plugins == nil {
|
||||||
|
return r.Dump()
|
||||||
|
}
|
||||||
|
for n, p := range Plugins.Providers {
|
||||||
|
info := p.Provider.Info()
|
||||||
|
c := p.Client.ReattachConfig()
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"network": c.Addr.Network(),
|
||||||
|
"address": c.Addr.String(),
|
||||||
|
"description": info.Description,
|
||||||
|
"priority": info.Priority,
|
||||||
|
}
|
||||||
|
list[n] = data
|
||||||
|
}
|
||||||
|
r.Result = list
|
||||||
|
return r.Dump()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export ListSyncedFolders
|
||||||
|
func ListSyncedFolders() *C.char {
|
||||||
|
list := map[string]interface{}{}
|
||||||
|
r := &Response{Result: list}
|
||||||
|
if Plugins == nil {
|
||||||
|
return r.Dump()
|
||||||
|
}
|
||||||
|
for n, p := range Plugins.SyncedFolders {
|
||||||
|
info := p.SyncedFolder.Info()
|
||||||
|
c := p.Client.ReattachConfig()
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"network": c.Addr.Network(),
|
||||||
|
"address": c.Addr.String(),
|
||||||
|
"description": info.Description,
|
||||||
|
"priority": info.Priority,
|
||||||
|
}
|
||||||
|
list[n] = data
|
||||||
|
}
|
||||||
|
r.Result = list
|
||||||
|
return r.Dump()
|
||||||
|
}
|
||||||
|
|
||||||
|
// stub required for build
|
||||||
|
func main() {}
|
||||||
|
|
||||||
|
// helper to convert c string to go string
|
||||||
|
func to_gs(s *C.char) string {
|
||||||
|
return C.GoString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper to convert go string to c string
|
||||||
|
func to_cs(s string) *C.char {
|
||||||
|
return C.CString(s)
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"C"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Error error `json:"error"`
|
||||||
|
Result interface{} `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize the response into a JSON C string
|
||||||
|
func (r *Response) Dump() *C.char {
|
||||||
|
tmp := map[string]interface{}{}
|
||||||
|
if r.Error != nil {
|
||||||
|
tmp["error"] = r.Error.Error()
|
||||||
|
} else {
|
||||||
|
tmp["error"] = nil
|
||||||
|
}
|
||||||
|
tmp["result"] = r.Result
|
||||||
|
result, err := json.Marshal(tmp)
|
||||||
|
if err != nil {
|
||||||
|
return to_cs(fmt.Sprintf(`{"error": "failed to encode response - %s"}`, err))
|
||||||
|
}
|
||||||
|
return to_cs(string(result[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load a new response from a JSON C string
|
||||||
|
func LoadResponse(s *C.char) (r *Response, err error) {
|
||||||
|
tmp := map[string]interface{}{}
|
||||||
|
st := []byte(to_gs(s))
|
||||||
|
r = &Response{}
|
||||||
|
err = json.Unmarshal(st, &tmp)
|
||||||
|
if tmp["error"] != nil {
|
||||||
|
e, ok := tmp["error"].(string)
|
||||||
|
if !ok {
|
||||||
|
err = errors.New(
|
||||||
|
fmt.Sprintf("cannot load error content - %s", tmp["error"]))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Error = errors.New(e)
|
||||||
|
}
|
||||||
|
r.Result = tmp["result"]
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
type Box struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Directory string `json:"directory"`
|
||||||
|
Metadata map[string]string `json:"metadata"`
|
||||||
|
MetadataURL string `json:"metadata_url"`
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SystemCapability struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderCapability struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GuestCapabilities interface {
|
||||||
|
GuestCapabilities() (caps []SystemCapability, err error)
|
||||||
|
GuestCapability(ctx context.Context, cap *SystemCapability, args interface{}, machine *Machine) (result interface{}, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HostCapabilities interface {
|
||||||
|
HostCapabilities() (caps []SystemCapability, err error)
|
||||||
|
HostCapability(ctx context.Context, cap *SystemCapability, args interface{}, env *Environment) (result interface{}, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderCapabilities interface {
|
||||||
|
ProviderCapabilities() (caps []ProviderCapability, err error)
|
||||||
|
ProviderCapability(ctx context.Context, cap *ProviderCapability, args interface{}, machine *Machine) (result interface{}, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoGuestCapabilities struct{}
|
||||||
|
type NoHostCapabilities struct{}
|
||||||
|
type NoProviderCapabilities struct{}
|
||||||
|
|
||||||
|
func (g *NoGuestCapabilities) GuestCapabilities() (caps []SystemCapability, err error) {
|
||||||
|
caps = make([]SystemCapability, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *NoGuestCapabilities) GuestCapability(x context.Context, c *SystemCapability, a interface{}, m *Machine) (r interface{}, err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NoHostCapabilities) HostCapabilities() (caps []SystemCapability, err error) {
|
||||||
|
caps = make([]SystemCapability, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NoHostCapabilities) HostCapability(x context.Context, c *SystemCapability, a interface{}, e *Environment) (r interface{}, err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NoProviderCapabilities) ProviderCapabilities() (caps []ProviderCapability, err error) {
|
||||||
|
caps = make([]ProviderCapability, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NoProviderCapabilities) ProviderCapability(x context.Context, cap *ProviderCapability, args interface{}, machine *Machine) (result interface{}, err error) {
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNoGuestCapabilities(t *testing.T) {
|
||||||
|
g := NoGuestCapabilities{}
|
||||||
|
caps, err := g.GuestCapabilities()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if len(caps) != 0 {
|
||||||
|
t.Fatalf("guest capabilities should be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoGuestCapability(t *testing.T) {
|
||||||
|
g := NoGuestCapabilities{}
|
||||||
|
m := &Machine{}
|
||||||
|
cap := &SystemCapability{"Test", "Test"}
|
||||||
|
r, err := g.GuestCapability(context.Background(), cap, "args", m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if r != nil {
|
||||||
|
t.Fatalf("capability returned unexpected result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoHostCapabilities(t *testing.T) {
|
||||||
|
h := NoHostCapabilities{}
|
||||||
|
caps, err := h.HostCapabilities()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if len(caps) != 0 {
|
||||||
|
t.Fatalf("host capabilities should be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoHostCapability(t *testing.T) {
|
||||||
|
h := NoHostCapabilities{}
|
||||||
|
e := &Environment{}
|
||||||
|
cap := &SystemCapability{"Test", "Test"}
|
||||||
|
r, err := h.HostCapability(context.Background(), cap, "args", e)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if r != nil {
|
||||||
|
t.Fatalf("capability returned unexpected result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoProviderCapabilities(t *testing.T) {
|
||||||
|
p := NoProviderCapabilities{}
|
||||||
|
caps, err := p.ProviderCapabilities()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if len(caps) != 0 {
|
||||||
|
t.Fatalf("provider capabilities should be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoProviderCapability(t *testing.T) {
|
||||||
|
p := NoProviderCapabilities{}
|
||||||
|
m := &Machine{}
|
||||||
|
cap := &ProviderCapability{"Test", "Test"}
|
||||||
|
r, err := p.ProviderCapability(context.Background(), cap, "args", m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if r != nil {
|
||||||
|
t.Fatalf("capability returned unexpected result")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,202 @@
|
||||||
|
package communicator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
"github.com/mitchellh/iochan"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdDisconnect is a sentinel value to indicate a RemoteCmd
|
||||||
|
// exited because the remote side disconnected us.
|
||||||
|
const CmdDisconnect int = 2300218
|
||||||
|
|
||||||
|
// Cmd represents a remote command being prepared or run.
|
||||||
|
type Cmd struct {
|
||||||
|
// Command is the command to run remotely. This is executed as if
|
||||||
|
// it were a shell command, so you are expected to do any shell escaping
|
||||||
|
// necessary.
|
||||||
|
Command string
|
||||||
|
|
||||||
|
// Stdin specifies the process's standard input. If Stdin is
|
||||||
|
// nil, the process reads from an empty bytes.Buffer.
|
||||||
|
Stdin io.Reader
|
||||||
|
|
||||||
|
// Stdout and Stderr represent the process's standard output and
|
||||||
|
// error.
|
||||||
|
//
|
||||||
|
// If either is nil, it will be set to ioutil.Discard.
|
||||||
|
Stdout io.Writer
|
||||||
|
Stderr io.Writer
|
||||||
|
|
||||||
|
// Once Wait returns, his will contain the exit code of the process.
|
||||||
|
exitStatus int
|
||||||
|
|
||||||
|
// Internal fields
|
||||||
|
exitCh chan struct{}
|
||||||
|
|
||||||
|
// err is used to store any error reported by the Communicator during
|
||||||
|
// execution.
|
||||||
|
err error
|
||||||
|
|
||||||
|
// This thing is a mutex, lock when making modifications concurrently
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init must be called by the Communicator before executing the command.
|
||||||
|
func (c *Cmd) Init() {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
c.exitCh = make(chan struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExitStatus stores the exit status of the remote command as well as any
|
||||||
|
// communicator related error. SetExitStatus then unblocks any pending calls
|
||||||
|
// to Wait.
|
||||||
|
// This should only be called by communicators executing the remote.Cmd.
|
||||||
|
func (c *Cmd) SetExitStatus(status int, err error) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
c.exitStatus = status
|
||||||
|
c.err = err
|
||||||
|
|
||||||
|
close(c.exitCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartWithUi runs the remote command and streams the output to any
|
||||||
|
// configured Writers for stdout/stderr, while also writing each line
|
||||||
|
// as it comes to a Ui.
|
||||||
|
func (r *Cmd) StartWithUi(c Communicator, ui vagrant.Ui) error {
|
||||||
|
stdout_r, stdout_w := io.Pipe()
|
||||||
|
stderr_r, stderr_w := io.Pipe()
|
||||||
|
defer stdout_w.Close()
|
||||||
|
defer stderr_w.Close()
|
||||||
|
|
||||||
|
// Retain the original stdout/stderr that we can replace back in.
|
||||||
|
originalStdout := r.Stdout
|
||||||
|
originalStderr := r.Stderr
|
||||||
|
defer func() {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
|
||||||
|
r.Stdout = originalStdout
|
||||||
|
r.Stderr = originalStderr
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Set the writers for the output so that we get it streamed to us
|
||||||
|
if r.Stdout == nil {
|
||||||
|
r.Stdout = stdout_w
|
||||||
|
} else {
|
||||||
|
r.Stdout = io.MultiWriter(r.Stdout, stdout_w)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Stderr == nil {
|
||||||
|
r.Stderr = stderr_w
|
||||||
|
} else {
|
||||||
|
r.Stderr = io.MultiWriter(r.Stderr, stderr_w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the command
|
||||||
|
if err := c.Start(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the channels we'll use for data
|
||||||
|
exitCh := make(chan struct{})
|
||||||
|
stdoutCh := iochan.DelimReader(stdout_r, '\n')
|
||||||
|
stderrCh := iochan.DelimReader(stderr_r, '\n')
|
||||||
|
|
||||||
|
// Start the goroutine to watch for the exit
|
||||||
|
go func() {
|
||||||
|
defer close(exitCh)
|
||||||
|
defer stdout_w.Close()
|
||||||
|
defer stderr_w.Close()
|
||||||
|
r.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Loop and get all our output
|
||||||
|
OutputLoop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case output := <-stderrCh:
|
||||||
|
if output != "" {
|
||||||
|
ui.Say(r.cleanOutputLine(output))
|
||||||
|
}
|
||||||
|
case output := <-stdoutCh:
|
||||||
|
if output != "" {
|
||||||
|
ui.Say(r.cleanOutputLine(output))
|
||||||
|
}
|
||||||
|
case <-exitCh:
|
||||||
|
break OutputLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we finish off stdout/stderr because we may have gotten
|
||||||
|
// a message from the exit channel before finishing these first.
|
||||||
|
for output := range stdoutCh {
|
||||||
|
ui.Say(r.cleanOutputLine(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
for output := range stderrCh {
|
||||||
|
ui.Say(r.cleanOutputLine(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait waits for the remote command to complete.
|
||||||
|
// Wait may return an error from the communicator, or an ExitError if the
|
||||||
|
// process exits with a non-zero exit status.
|
||||||
|
func (c *Cmd) Wait() error {
|
||||||
|
<-c.exitCh
|
||||||
|
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
if c.err != nil || c.exitStatus != 0 {
|
||||||
|
return &ExitError{
|
||||||
|
Command: c.Command,
|
||||||
|
ExitStatus: c.exitStatus,
|
||||||
|
Err: c.err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanOutputLine cleans up a line so that '\r' don't muck up the
|
||||||
|
// UI output when we're reading from a remote command.
|
||||||
|
func (r *Cmd) cleanOutputLine(line string) string {
|
||||||
|
// Trim surrounding whitespace
|
||||||
|
line = strings.TrimRightFunc(line, unicode.IsSpace)
|
||||||
|
|
||||||
|
// Trim up to the first carriage return, since that text would be
|
||||||
|
// lost anyways.
|
||||||
|
idx := strings.LastIndex(line, "\r")
|
||||||
|
if idx > -1 {
|
||||||
|
line = line[idx+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExitError is returned by Wait to indicate and error executing the remote
|
||||||
|
// command, or a non-zero exit status.
|
||||||
|
type ExitError struct {
|
||||||
|
Command string
|
||||||
|
ExitStatus int
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExitError) Error() string {
|
||||||
|
if e.Err != nil {
|
||||||
|
return fmt.Sprintf("error executing %q: %v", e.Command, e.Err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%q exit status: %d", e.Command, e.ExitStatus)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package communicator
|
|
@ -0,0 +1,117 @@
|
||||||
|
package communicator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Communicator interface {
|
||||||
|
Connect() error
|
||||||
|
Disconnect() error
|
||||||
|
Timeout() time.Duration
|
||||||
|
Start(*Cmd) error
|
||||||
|
Download(path string, output io.Writer) error
|
||||||
|
DownloadDir(dst, src string, excludes []string) error
|
||||||
|
Upload(dst string, src io.Reader, srcinfo *os.FileInfo) error
|
||||||
|
UploadDir(dst, src string, excludes []string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxBackoffDelay is the maximum delay between retry attempts
|
||||||
|
var maxBackoffDelay = 20 * time.Second
|
||||||
|
var initialBackoffDelay = time.Second
|
||||||
|
var logger = vagrant.DefaultLogger().Named("communicator")
|
||||||
|
|
||||||
|
// Fatal is an interface that error values can return to halt Retry
|
||||||
|
type Fatal interface {
|
||||||
|
FatalError() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry retries the function f until it returns a nil error, a Fatal error, or
|
||||||
|
// the context expires.
|
||||||
|
func Retry(ctx context.Context, f func() error) error {
|
||||||
|
// container for atomic error value
|
||||||
|
type errWrap struct {
|
||||||
|
E error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try the function in a goroutine
|
||||||
|
var errVal atomic.Value
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(doneCh)
|
||||||
|
|
||||||
|
delay := time.Duration(0)
|
||||||
|
for {
|
||||||
|
// If our context ended, we want to exit right away.
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(delay):
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try the function call
|
||||||
|
err := f()
|
||||||
|
|
||||||
|
// return if we have no error, or a FatalError
|
||||||
|
done := false
|
||||||
|
switch e := err.(type) {
|
||||||
|
case nil:
|
||||||
|
done = true
|
||||||
|
case Fatal:
|
||||||
|
err = e.FatalError()
|
||||||
|
done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
errVal.Store(errWrap{err})
|
||||||
|
|
||||||
|
if done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Warn("retryable error", "error", err)
|
||||||
|
|
||||||
|
delay *= 2
|
||||||
|
|
||||||
|
if delay == 0 {
|
||||||
|
delay = initialBackoffDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
if delay > maxBackoffDelay {
|
||||||
|
delay = maxBackoffDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("sleeping for retry", "duration", delay)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for completion
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-doneCh:
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
// Check if we got an error executing
|
||||||
|
if ev, ok := errVal.Load().(errWrap); ok {
|
||||||
|
lastErr = ev.E
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have a context error to check if we're interrupted or timeout
|
||||||
|
switch ctx.Err() {
|
||||||
|
case context.Canceled:
|
||||||
|
return fmt.Errorf("interrupted - last error: %v", lastErr)
|
||||||
|
case context.DeadlineExceeded:
|
||||||
|
return fmt.Errorf("timeout - last error: %v", lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastErr != nil {
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package none
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Communicator struct {
|
||||||
|
config string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a null vagrant.Communicator implementation. This takes
|
||||||
|
// an already existing configuration.
|
||||||
|
func New(config string) (result *Communicator, err error) {
|
||||||
|
// Establish an initial connection and connect
|
||||||
|
result = &Communicator{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Connect() (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Disconnect() (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Start(cmd *communicator.Cmd) (err error) {
|
||||||
|
cmd.Init()
|
||||||
|
cmd.SetExitStatus(0, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Upload(path string, input io.Reader, fi *os.FileInfo) error {
|
||||||
|
return errors.New("Upload is not implemented when communicator = 'none'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) UploadDir(dst string, src string, excl []string) error {
|
||||||
|
return errors.New("UploadDir is not implemented when communicator = 'none'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Download(path string, output io.Writer) error {
|
||||||
|
return errors.New("Download is not implemented when communicator = 'none'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) DownloadDir(dst string, src string, excl []string) error {
|
||||||
|
return errors.New("DownloadDir is not implemented when communicator = 'none'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Timeout() time.Duration {
|
||||||
|
return 0
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package none
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommIsCommunicator(t *testing.T) {
|
||||||
|
// Force failure with explanation of why it's not valid
|
||||||
|
var _ communicator.Communicator = new(Communicator)
|
||||||
|
}
|
|
@ -0,0 +1,949 @@
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
|
||||||
|
"github.com/pkg/sftp"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/crypto/ssh/agent"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrHandshakeTimeout is returned from New() whenever we're unable to establish
|
||||||
|
// an ssh connection within a certain timeframe. By default the handshake time-
|
||||||
|
// out period is 1 minute. You can change it with Config.HandshakeTimeout.
|
||||||
|
var ErrHandshakeTimeout = fmt.Errorf("Timeout during SSH handshake")
|
||||||
|
|
||||||
|
var logger = vagrant.DefaultLogger().Named("communicator.ssh")
|
||||||
|
|
||||||
|
type Communicator struct {
|
||||||
|
client *ssh.Client
|
||||||
|
config *Config
|
||||||
|
conn net.Conn
|
||||||
|
address string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is the structure used to configure the SSH communicator.
|
||||||
|
type Config struct {
|
||||||
|
// The configuration of the Go SSH connection
|
||||||
|
SSHConfig *ssh.ClientConfig
|
||||||
|
|
||||||
|
// Connection returns a new connection. The current connection
|
||||||
|
// in use will be closed as part of the Close method, or in the
|
||||||
|
// case an error occurs.
|
||||||
|
Connection func() (net.Conn, error)
|
||||||
|
|
||||||
|
// Pty, if true, will request a pty from the remote end.
|
||||||
|
Pty bool
|
||||||
|
|
||||||
|
// DisableAgentForwarding, if true, will not forward the SSH agent.
|
||||||
|
DisableAgentForwarding bool
|
||||||
|
|
||||||
|
// HandshakeTimeout limits the amount of time we'll wait to handshake before
|
||||||
|
// saying the connection failed.
|
||||||
|
HandshakeTimeout time.Duration
|
||||||
|
|
||||||
|
// UseSftp, if true, sftp will be used instead of scp for file transfers
|
||||||
|
UseSftp bool
|
||||||
|
|
||||||
|
// KeepAliveInterval sets how often we send a channel request to the
|
||||||
|
// server. A value < 0 disables.
|
||||||
|
KeepAliveInterval time.Duration
|
||||||
|
|
||||||
|
// Timeout is how long to wait for a read or write to succeed.
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new vagrant.Communicator implementation over SSH. This takes
|
||||||
|
// an already existing TCP connection and SSH configuration.
|
||||||
|
func New(address string, config *Config) (result *Communicator, err error) {
|
||||||
|
// Establish an initial connection and connect
|
||||||
|
result = &Communicator{
|
||||||
|
config: config,
|
||||||
|
address: address,
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset the logger in case custom default has been set
|
||||||
|
logger = vagrant.DefaultLogger().Named("communicator.ssh")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Connect() error {
|
||||||
|
return c.reconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Disconnect() (err error) {
|
||||||
|
if c.conn != nil {
|
||||||
|
logger.Info("closing connection")
|
||||||
|
err = c.conn.Close()
|
||||||
|
} else {
|
||||||
|
err = errors.New("No connection currently established to close")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Timeout() time.Duration {
|
||||||
|
return c.config.Timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Start(cmd *communicator.Cmd) (err error) {
|
||||||
|
session, err := c.newSession()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup our session
|
||||||
|
session.Stdin = cmd.Stdin
|
||||||
|
session.Stdout = cmd.Stdout
|
||||||
|
session.Stderr = cmd.Stderr
|
||||||
|
|
||||||
|
if c.config.Pty {
|
||||||
|
// Request a PTY
|
||||||
|
termModes := ssh.TerminalModes{
|
||||||
|
ssh.ECHO: 0, // do not echo
|
||||||
|
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
|
||||||
|
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = session.RequestPty("xterm", 40, 80, termModes); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("starting remote command", "command", cmd.Command)
|
||||||
|
err = session.Start(cmd.Command + "\n")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if c.config.KeepAliveInterval <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c := time.NewTicker(c.config.KeepAliveInterval)
|
||||||
|
defer c.Stop()
|
||||||
|
for range c.C {
|
||||||
|
_, err := session.SendRequest("keepalive@vagrantup.com", true, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start a goroutine to wait for the session to end and set the
|
||||||
|
// exit boolean and status.
|
||||||
|
go func() {
|
||||||
|
defer session.Close()
|
||||||
|
|
||||||
|
err := session.Wait()
|
||||||
|
exitStatus := 0
|
||||||
|
if err != nil {
|
||||||
|
switch err.(type) {
|
||||||
|
case *ssh.ExitError:
|
||||||
|
exitStatus = err.(*ssh.ExitError).ExitStatus()
|
||||||
|
logger.Error("remote command exited non-zero",
|
||||||
|
"exitcode", exitStatus, "command", cmd.Command)
|
||||||
|
case *ssh.ExitMissingError:
|
||||||
|
logger.Error("remote command exited without exit status or exit signal",
|
||||||
|
"command", cmd.Command)
|
||||||
|
exitStatus = communicator.CmdDisconnect
|
||||||
|
default:
|
||||||
|
logger.Error("error waiting for ssh session", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd.SetExitStatus(exitStatus, err)
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Upload(path string, input io.Reader, fi *os.FileInfo) error {
|
||||||
|
if c.config.UseSftp {
|
||||||
|
return c.sftpUploadSession(path, input, fi)
|
||||||
|
} else {
|
||||||
|
return c.scpUploadSession(path, input, fi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) UploadDir(dst string, src string, excl []string) error {
|
||||||
|
logger.Debug("uploading directory", "source", src, "destination", dst)
|
||||||
|
if c.config.UseSftp {
|
||||||
|
return c.sftpUploadDirSession(dst, src, excl)
|
||||||
|
} else {
|
||||||
|
return c.scpUploadDirSession(dst, src, excl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) DownloadDir(src string, dst string, excl []string) error {
|
||||||
|
logger.Debug("downloading directory", "source", src, "destination", dst)
|
||||||
|
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
|
||||||
|
dirStack := []string{dst}
|
||||||
|
for {
|
||||||
|
fmt.Fprint(w, "\x00")
|
||||||
|
|
||||||
|
// read file info
|
||||||
|
fi, err := stdoutR.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fi) < 0 {
|
||||||
|
return fmt.Errorf("empty response from server")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fi[0] {
|
||||||
|
case '\x01', '\x02':
|
||||||
|
return fmt.Errorf("%s", fi[1:])
|
||||||
|
case 'C', 'D':
|
||||||
|
break
|
||||||
|
case 'E':
|
||||||
|
dirStack = dirStack[:len(dirStack)-1]
|
||||||
|
if len(dirStack) == 0 {
|
||||||
|
fmt.Fprint(w, "\x00")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected server response (%x)", fi[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
var mode int64
|
||||||
|
var size int64
|
||||||
|
var name string
|
||||||
|
logger.Debug("download directory", "str", fi)
|
||||||
|
n, err := fmt.Sscanf(fi[1:], "%o %d %s", &mode, &size, &name)
|
||||||
|
if err != nil || n != 3 {
|
||||||
|
return fmt.Errorf("can't parse server response (%s)", fi)
|
||||||
|
}
|
||||||
|
if size < 0 {
|
||||||
|
return fmt.Errorf("negative file size")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("download directory", "mode", mode, "size", size, "name", name)
|
||||||
|
|
||||||
|
dst = filepath.Join(dirStack...)
|
||||||
|
switch fi[0] {
|
||||||
|
case 'D':
|
||||||
|
err = os.MkdirAll(filepath.Join(dst, name), os.FileMode(mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dirStack = append(dirStack, name)
|
||||||
|
continue
|
||||||
|
case 'C':
|
||||||
|
fmt.Fprint(w, "\x00")
|
||||||
|
err = scpDownloadFile(filepath.Join(dst, name), stdoutR, size, os.FileMode(mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkSCPStatus(stdoutR); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.scpSession("scp -vrf "+src, scpFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Download(path string, output io.Writer) error {
|
||||||
|
if c.config.UseSftp {
|
||||||
|
return c.sftpDownloadSession(path, output)
|
||||||
|
}
|
||||||
|
return c.scpDownloadSession(path, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) newSession() (session *ssh.Session, err error) {
|
||||||
|
logger.Debug("opening new ssh session")
|
||||||
|
if c.client == nil {
|
||||||
|
err = errors.New("client not available")
|
||||||
|
} else {
|
||||||
|
session, err = c.client.NewSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("ssh session open failure", "error", err)
|
||||||
|
if err := c.reconnect(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.client == nil {
|
||||||
|
return nil, errors.New("client not available")
|
||||||
|
} else {
|
||||||
|
return c.client.NewSession()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) reconnect() (err error) {
|
||||||
|
// Ignore errors here because we don't care if it fails
|
||||||
|
c.Disconnect()
|
||||||
|
// Set the conn and client to nil since we'll recreate it
|
||||||
|
c.conn = nil
|
||||||
|
c.client = nil
|
||||||
|
|
||||||
|
logger.Debug("reconnection to tcp connection for ssh")
|
||||||
|
c.conn, err = c.config.Connection()
|
||||||
|
if err != nil {
|
||||||
|
// Explicitly set this to the REAL nil. Connection() can return
|
||||||
|
// a nil implementation of net.Conn which will make the
|
||||||
|
// "if c.conn == nil" check fail above. Read here for more information
|
||||||
|
// on this psychotic language feature:
|
||||||
|
//
|
||||||
|
// http://golang.org/doc/faq#nil_error
|
||||||
|
c.conn = nil
|
||||||
|
|
||||||
|
logger.Error("reconnection failure", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.config.Timeout > 0 {
|
||||||
|
c.conn = &timeoutConn{c.conn, c.config.Timeout, c.config.Timeout}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("handshaking with ssh")
|
||||||
|
|
||||||
|
// Default timeout to 1 minute if it wasn't specified (zero value). For
|
||||||
|
// when you need to handshake from low orbit.
|
||||||
|
var duration time.Duration
|
||||||
|
if c.config.HandshakeTimeout == 0 {
|
||||||
|
duration = 1 * time.Minute
|
||||||
|
} else {
|
||||||
|
duration = c.config.HandshakeTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionEstablished := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
var sshConn ssh.Conn
|
||||||
|
var sshChan <-chan ssh.NewChannel
|
||||||
|
var req <-chan *ssh.Request
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sshConn, sshChan, req, err = ssh.NewClientConn(c.conn, c.address, c.config.SSHConfig)
|
||||||
|
close(connectionEstablished)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-connectionEstablished:
|
||||||
|
// We don't need to do anything here. We just want select to block until
|
||||||
|
// we connect or timeout.
|
||||||
|
case <-time.After(duration):
|
||||||
|
if c.conn != nil {
|
||||||
|
c.conn.Close()
|
||||||
|
}
|
||||||
|
if sshConn != nil {
|
||||||
|
sshConn.Close()
|
||||||
|
}
|
||||||
|
return ErrHandshakeTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Debug("handshake complete")
|
||||||
|
if sshConn != nil {
|
||||||
|
c.client = ssh.NewClient(sshConn, sshChan, req)
|
||||||
|
}
|
||||||
|
c.connectToAgent()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) connectToAgent() {
|
||||||
|
if c.client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.config.DisableAgentForwarding {
|
||||||
|
logger.Info("SSH agent forwarding is disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// open connection to the local agent
|
||||||
|
socketLocation := os.Getenv("SSH_AUTH_SOCK")
|
||||||
|
if socketLocation == "" {
|
||||||
|
logger.Info("no local agent socket, will not connect agent")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
agentConn, err := net.Dial("unix", socketLocation)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("could not connect to local agent socket", "path", socketLocation)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// create agent and add in auth
|
||||||
|
forwardingAgent := agent.NewClient(agentConn)
|
||||||
|
if forwardingAgent == nil {
|
||||||
|
logger.Error("could not create agent client")
|
||||||
|
agentConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// add callback for forwarding agent to SSH config
|
||||||
|
// XXX - might want to handle reconnects appending multiple callbacks
|
||||||
|
auth := ssh.PublicKeysCallback(forwardingAgent.Signers)
|
||||||
|
c.config.SSHConfig.Auth = append(c.config.SSHConfig.Auth, auth)
|
||||||
|
agent.ForwardToAgent(c.client, forwardingAgent)
|
||||||
|
|
||||||
|
// Setup a session to request agent forwarding
|
||||||
|
session, err := c.newSession()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer session.Close()
|
||||||
|
|
||||||
|
err = agent.RequestAgentForwarding(session)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("request agent forwarding failed", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("agent forwarding enabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) sftpUploadSession(path string, input io.Reader, fi *os.FileInfo) error {
|
||||||
|
sftpFunc := func(client *sftp.Client) error {
|
||||||
|
return c.sftpUploadFile(path, input, client, fi)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sftpSession(sftpFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) sftpUploadFile(path string, input io.Reader, client *sftp.Client, fi *os.FileInfo) error {
|
||||||
|
logger.Debug("sftp uploading", "path", path)
|
||||||
|
f, err := client.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(f, input); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi != nil && (*fi).Mode().IsRegular() {
|
||||||
|
mode := (*fi).Mode().Perm()
|
||||||
|
err = client.Chmod(path, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) sftpUploadDirSession(dst string, src string, excl []string) error {
|
||||||
|
sftpFunc := func(client *sftp.Client) error {
|
||||||
|
rootDst := dst
|
||||||
|
if src[len(src)-1] != '/' {
|
||||||
|
logger.Debug("no trailing slash, creating the source directory name")
|
||||||
|
rootDst = filepath.Join(dst, filepath.Base(src))
|
||||||
|
}
|
||||||
|
walkFunc := func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Calculate the final destination using the
|
||||||
|
// base source and root destination
|
||||||
|
relSrc, err := filepath.Rel(src, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
finalDst := filepath.Join(rootDst, relSrc)
|
||||||
|
|
||||||
|
// In Windows, Join uses backslashes which we don't want to get
|
||||||
|
// to the sftp server
|
||||||
|
finalDst = filepath.ToSlash(finalDst)
|
||||||
|
|
||||||
|
// Skip the creation of the target destination directory since
|
||||||
|
// it should exist and we might not even own it
|
||||||
|
if finalDst == dst {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sftpVisitFile(finalDst, path, info, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Walk(src, walkFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sftpSession(sftpFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) sftpMkdir(path string, client *sftp.Client, fi os.FileInfo) error {
|
||||||
|
logger.Debug("sftp create directory", "path", path)
|
||||||
|
|
||||||
|
if err := client.Mkdir(path); err != nil {
|
||||||
|
// Do not consider it an error if the directory existed
|
||||||
|
remoteFi, fiErr := client.Lstat(path)
|
||||||
|
if fiErr != nil || !remoteFi.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := fi.Mode().Perm()
|
||||||
|
if err := client.Chmod(path, mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) sftpVisitFile(dst string, src string, fi os.FileInfo, client *sftp.Client) error {
|
||||||
|
if !fi.IsDir() {
|
||||||
|
f, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return c.sftpUploadFile(dst, f, client, &fi)
|
||||||
|
} else {
|
||||||
|
err := c.sftpMkdir(dst, client, fi)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) sftpDownloadSession(path string, output io.Writer) error {
|
||||||
|
sftpFunc := func(client *sftp.Client) error {
|
||||||
|
f, err := client.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(output, f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sftpSession(sftpFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) sftpSession(f func(*sftp.Client) error) error {
|
||||||
|
client, err := c.newSftpClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("sftpSession error: %s", err.Error())
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
return f(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) newSftpClient() (*sftp.Client, error) {
|
||||||
|
session, err := c.newSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := session.RequestSubsystem("sftp"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pw, err := session.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pr, err := session.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture stdout so we can return errors to the user
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
tee := io.TeeReader(pr, &stdout)
|
||||||
|
client, err := sftp.NewClientPipe(tee, pw)
|
||||||
|
if err != nil && stdout.Len() > 0 {
|
||||||
|
logger.Error("upload failed", "error", stdout.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) scpUploadSession(path string, input io.Reader, fi *os.FileInfo) error {
|
||||||
|
|
||||||
|
// The target directory and file for talking the SCP protocol
|
||||||
|
target_dir := filepath.Dir(path)
|
||||||
|
target_file := filepath.Base(path)
|
||||||
|
|
||||||
|
// On windows, filepath.Dir uses backslash separators (ie. "\tmp").
|
||||||
|
// This does not work when the target host is unix. Switch to forward slash
|
||||||
|
// which works for unix and windows
|
||||||
|
target_dir = filepath.ToSlash(target_dir)
|
||||||
|
|
||||||
|
// Escape spaces in remote directory
|
||||||
|
target_dir = strings.Replace(target_dir, " ", "\\ ", -1)
|
||||||
|
|
||||||
|
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
|
||||||
|
return scpUploadFile(target_file, input, w, stdoutR, fi)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.scpSession("scp -vt "+target_dir, scpFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) scpUploadDirSession(dst string, src string, excl []string) error {
|
||||||
|
scpFunc := func(w io.Writer, r *bufio.Reader) error {
|
||||||
|
uploadEntries := func() error {
|
||||||
|
f, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
entries, err := f.Readdir(-1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return scpUploadDir(src, entries, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if src[len(src)-1] != '/' {
|
||||||
|
logger.Debug("no trailing slash, creating the source directory name")
|
||||||
|
fi, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return scpUploadDirProtocol(filepath.Base(src), w, r, uploadEntries, fi)
|
||||||
|
} else {
|
||||||
|
// Trailing slash, so only upload the contents
|
||||||
|
return uploadEntries()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.scpSession("scp -rvt "+dst, scpFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) scpDownloadSession(path string, output io.Writer) error {
|
||||||
|
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
|
||||||
|
fmt.Fprint(w, "\x00")
|
||||||
|
|
||||||
|
// read file info
|
||||||
|
fi, err := stdoutR.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fi) < 0 {
|
||||||
|
return fmt.Errorf("empty response from server")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fi[0] {
|
||||||
|
case '\x01', '\x02':
|
||||||
|
return fmt.Errorf("%s", fi[1:])
|
||||||
|
case 'C':
|
||||||
|
case 'D':
|
||||||
|
return fmt.Errorf("remote file is directory")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected server response (%x)", fi[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
var mode string
|
||||||
|
var size int64
|
||||||
|
|
||||||
|
n, err := fmt.Sscanf(fi, "%6s %d ", &mode, &size)
|
||||||
|
if err != nil || n != 2 {
|
||||||
|
return fmt.Errorf("can't parse server response (%s)", fi)
|
||||||
|
}
|
||||||
|
if size < 0 {
|
||||||
|
return fmt.Errorf("negative file size")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, "\x00")
|
||||||
|
|
||||||
|
if _, err := io.CopyN(output, stdoutR, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, "\x00")
|
||||||
|
|
||||||
|
return checkSCPStatus(stdoutR)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(path, " ") {
|
||||||
|
return c.scpSession("scp -vf "+path, scpFunc)
|
||||||
|
}
|
||||||
|
return c.scpSession("scp -vf "+strconv.Quote(path), scpFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) error {
|
||||||
|
session, err := c.newSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer session.Close()
|
||||||
|
|
||||||
|
// Get a pipe to stdin so that we can send data down
|
||||||
|
stdinW, err := session.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only want to close once, so we nil w after we close it,
|
||||||
|
// and only close in the defer if it hasn't been closed already.
|
||||||
|
defer func() {
|
||||||
|
if stdinW != nil {
|
||||||
|
stdinW.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Get a pipe to stdout so that we can get responses back
|
||||||
|
stdoutPipe, err := session.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdoutR := bufio.NewReader(stdoutPipe)
|
||||||
|
|
||||||
|
// Set stderr to a bytes buffer
|
||||||
|
stderr := new(bytes.Buffer)
|
||||||
|
session.Stderr = stderr
|
||||||
|
|
||||||
|
// Start the sink mode on the other side
|
||||||
|
// TODO(mitchellh): There are probably issues with shell escaping the path
|
||||||
|
logger.Debug("starting remote scp process", "command", scpCommand)
|
||||||
|
if err := session.Start(scpCommand); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call our callback that executes in the context of SCP. We ignore
|
||||||
|
// EOF errors if they occur because it usually means that SCP prematurely
|
||||||
|
// ended on the other side.
|
||||||
|
logger.Debug("started scp session, beginning transfers")
|
||||||
|
if err := f(stdinW, stdoutR); err != nil && err != io.EOF {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the stdin, which sends an EOF, and then set w to nil so that
|
||||||
|
// our defer func doesn't close it again since that is unsafe with
|
||||||
|
// the Go SSH package.
|
||||||
|
logger.Debug("scp sessiono complete, closing stdin pipe")
|
||||||
|
stdinW.Close()
|
||||||
|
stdinW = nil
|
||||||
|
|
||||||
|
// Wait for the SCP connection to close, meaning it has consumed all
|
||||||
|
// our data and has completed. Or has errored.
|
||||||
|
logger.Debug("waiting for ssh session to complete")
|
||||||
|
err = session.Wait()
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*ssh.ExitError); ok {
|
||||||
|
// Otherwise, we have an ExitError, meaning we can just read
|
||||||
|
// the exit status
|
||||||
|
logger.Debug("non-zero exit status", "exitcode", exitErr.ExitStatus())
|
||||||
|
stdoutB, err := ioutil.ReadAll(stdoutR)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Debug("scp output", "output", stdoutB)
|
||||||
|
|
||||||
|
// If we exited with status 127, it means SCP isn't available.
|
||||||
|
// Return a more descriptive error for that.
|
||||||
|
if exitErr.ExitStatus() == 127 {
|
||||||
|
return errors.New(
|
||||||
|
"SCP failed to start. This usually means that SCP is not\n" +
|
||||||
|
"properly installed on the remote system.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("scp stderr", "length", stderr.Len(), "content", stderr.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSCPStatus checks that a prior command sent to SCP completed
|
||||||
|
// successfully. If it did not complete successfully, an error will
|
||||||
|
// be returned.
|
||||||
|
func checkSCPStatus(r *bufio.Reader) error {
|
||||||
|
code, err := r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if code != 0 {
|
||||||
|
// Treat any non-zero (really 1 and 2) as fatal errors
|
||||||
|
message, _, err := r.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error reading error message: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(string(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scpDownloadFile(dst string, src io.Reader, size int64, mode os.FileMode) error {
|
||||||
|
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if _, err := io.CopyN(f, src, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scpUploadFile(dst string, src io.Reader, w io.Writer, r *bufio.Reader, fi *os.FileInfo) error {
|
||||||
|
var mode os.FileMode
|
||||||
|
var size int64
|
||||||
|
|
||||||
|
if fi != nil && (*fi).Mode().IsRegular() {
|
||||||
|
mode = (*fi).Mode().Perm()
|
||||||
|
size = (*fi).Size()
|
||||||
|
} else {
|
||||||
|
// Create a temporary file where we can copy the contents of the src
|
||||||
|
// so that we can determine the length, since SCP is length-prefixed.
|
||||||
|
tf, err := ioutil.TempFile("", "vagrant-upload")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating temporary file for upload: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tf.Name())
|
||||||
|
defer tf.Close()
|
||||||
|
|
||||||
|
mode = 0644
|
||||||
|
|
||||||
|
logger.Debug("copying input data to temporary file to read length")
|
||||||
|
if _, err := io.Copy(tf, src); err != nil {
|
||||||
|
return fmt.Errorf("Error copying input data into local temporary "+
|
||||||
|
"file. Check that TEMPDIR has enough space. Error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync the file so that the contents are definitely on disk, then
|
||||||
|
// read the length of it.
|
||||||
|
if err := tf.Sync(); err != nil {
|
||||||
|
return fmt.Errorf("Error creating temporary file for upload: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek the file to the beginning so we can re-read all of it
|
||||||
|
if _, err := tf.Seek(0, 0); err != nil {
|
||||||
|
return fmt.Errorf("Error creating temporary file for upload: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tfi, err := tf.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating temporary file for upload: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
size = tfi.Size()
|
||||||
|
src = tf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the protocol
|
||||||
|
perms := fmt.Sprintf("C%04o", mode)
|
||||||
|
logger.Debug("scp uploading", "path", dst, "perms", perms, "size", size)
|
||||||
|
|
||||||
|
fmt.Fprintln(w, perms, size, dst)
|
||||||
|
if err := checkSCPStatus(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.CopyN(w, src, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, "\x00")
|
||||||
|
return checkSCPStatus(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scpUploadDirProtocol(name string, w io.Writer, r *bufio.Reader, f func() error, fi os.FileInfo) error {
|
||||||
|
logger.Debug("scp directory upload", "path", name)
|
||||||
|
|
||||||
|
mode := fi.Mode().Perm()
|
||||||
|
|
||||||
|
perms := fmt.Sprintf("D%04o 0", mode)
|
||||||
|
|
||||||
|
fmt.Fprintln(w, perms, name)
|
||||||
|
err := checkSCPStatus(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(w, "E")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func scpUploadDir(root string, fs []os.FileInfo, w io.Writer, r *bufio.Reader) error {
|
||||||
|
for _, fi := range fs {
|
||||||
|
realPath := filepath.Join(root, fi.Name())
|
||||||
|
|
||||||
|
// Track if this is actually a symlink to a directory. If it is
|
||||||
|
// a symlink to a file we don't do any special behavior because uploading
|
||||||
|
// a file just works. If it is a directory, we need to know so we
|
||||||
|
// treat it as such.
|
||||||
|
isSymlinkToDir := false
|
||||||
|
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||||
|
symPath, err := filepath.EvalSymlinks(realPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
symFi, err := os.Lstat(symPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
isSymlinkToDir = symFi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fi.IsDir() && !isSymlinkToDir {
|
||||||
|
// It is a regular file (or symlink to a file), just upload it
|
||||||
|
f, err := os.Open(realPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = func() error {
|
||||||
|
defer f.Close()
|
||||||
|
return scpUploadFile(fi.Name(), f, w, r, &fi)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// It is a directory, recursively upload
|
||||||
|
err := scpUploadDirProtocol(fi.Name(), w, r, func() error {
|
||||||
|
f, err := os.Open(realPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
entries, err := f.Readdir(-1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return scpUploadDir(realPath, entries, w, r)
|
||||||
|
}, fi)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,235 @@
|
||||||
|
// +build !race
|
||||||
|
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
// private key for mock server
|
||||||
|
const testServerPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpAIBAAKCAQEA19lGVsTqIT5iiNYRgnoY1CwkbETW5cq+Rzk5v/kTlf31XpSU
|
||||||
|
70HVWkbTERECjaYdXM2gGcbb+sxpq6GtXf1M3kVomycqhxwhPv4Cr6Xp4WT/jkFx
|
||||||
|
9z+FFzpeodGJWjOH6L2H5uX1Cvr9EDdQp9t9/J32/qBFntY8GwoUI/y/1MSTmMiF
|
||||||
|
tupdMODN064vd3gyMKTwrlQ8tZM6aYuyOPsutLlUY7M5x5FwMDYvnPDSeyT/Iw0z
|
||||||
|
s3B+NCyqeeMd2T7YzQFnRATj0M7rM5LoSs7DVqVriOEABssFyLj31PboaoLhOKgc
|
||||||
|
qoM9khkNzr7FHVvi+DhYM2jD0DwvqZLN6NmnLwIDAQABAoIBAQCGVj+kuSFOV1lT
|
||||||
|
+IclQYA6bM6uY5mroqcSBNegVxCNhWU03BxlW//BE9tA/+kq53vWylMeN9mpGZea
|
||||||
|
riEMIh25KFGWXqXlOOioH8bkMsqA8S7sBmc7jljyv+0toQ9vCCtJ+sueNPhxQQxH
|
||||||
|
D2YvUjfzBQ04I9+wn30BByDJ1QA/FoPsunxIOUCcRBE/7jxuLYcpR+JvEF68yYIh
|
||||||
|
atXRld4W4in7T65YDR8jK1Uj9XAcNeDYNpT/M6oFLx1aPIlkG86aCWRO19S1jLPT
|
||||||
|
b1ZAKHHxPMCVkSYW0RqvIgLXQOR62D0Zne6/2wtzJkk5UCjkSQ2z7ZzJpMkWgDgN
|
||||||
|
ifCULFPBAoGBAPoMZ5q1w+zB+knXUD33n1J+niN6TZHJulpf2w5zsW+m2K6Zn62M
|
||||||
|
MXndXlVAHtk6p02q9kxHdgov34Uo8VpuNjbS1+abGFTI8NZgFo+bsDxJdItemwC4
|
||||||
|
KJ7L1iz39hRN/ZylMRLz5uTYRGddCkeIHhiG2h7zohH/MaYzUacXEEy3AoGBANz8
|
||||||
|
e/msleB+iXC0cXKwds26N4hyMdAFE5qAqJXvV3S2W8JZnmU+sS7vPAWMYPlERPk1
|
||||||
|
D8Q2eXqdPIkAWBhrx4RxD7rNc5qFNcQWEhCIxC9fccluH1y5g2M+4jpMX2CT8Uv+
|
||||||
|
3z+NoJ5uDTXZTnLCfoZzgZ4nCZVZ+6iU5U1+YXFJAoGBANLPpIV920n/nJmmquMj
|
||||||
|
orI1R/QXR9Cy56cMC65agezlGOfTYxk5Cfl5Ve+/2IJCfgzwJyjWUsFx7RviEeGw
|
||||||
|
64o7JoUom1HX+5xxdHPsyZ96OoTJ5RqtKKoApnhRMamau0fWydH1yeOEJd+TRHhc
|
||||||
|
XStGfhz8QNa1dVFvENczja1vAoGABGWhsd4VPVpHMc7lUvrf4kgKQtTC2PjA4xoc
|
||||||
|
QJ96hf/642sVE76jl+N6tkGMzGjnVm4P2j+bOy1VvwQavKGoXqJBRd5Apppv727g
|
||||||
|
/SM7hBXKFc/zH80xKBBgP/i1DR7kdjakCoeu4ngeGywvu2jTS6mQsqzkK+yWbUxJ
|
||||||
|
I7mYBsECgYB/KNXlTEpXtz/kwWCHFSYA8U74l7zZbVD8ul0e56JDK+lLcJ0tJffk
|
||||||
|
gqnBycHj6AhEycjda75cs+0zybZvN4x65KZHOGW/O/7OAWEcZP5TPb3zf9ned3Hl
|
||||||
|
NsZoFj52ponUM6+99A2CmezFCN16c4mbA//luWF+k3VVqR6BpkrhKw==
|
||||||
|
-----END RSA PRIVATE KEY-----`
|
||||||
|
|
||||||
|
var serverConfig = &ssh.ServerConfig{
|
||||||
|
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
||||||
|
if c.User() == "user" && string(pass) == "pass" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("password rejected for %q", c.User())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Parse and set the private key of the server, required to accept connections
|
||||||
|
signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey))
|
||||||
|
if err != nil {
|
||||||
|
panic("unable to parse private key: " + err.Error())
|
||||||
|
}
|
||||||
|
serverConfig.AddHostKey(signer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockLineServer(t *testing.T) string {
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to listen for connection: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer l.Close()
|
||||||
|
c, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to accept incoming connection: %s", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
conn, chans, _, err := ssh.NewServerConn(c, serverConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Handshaking error: %v", err)
|
||||||
|
}
|
||||||
|
t.Log("Accepted SSH connection")
|
||||||
|
for newChannel := range chans {
|
||||||
|
channel, _, err := newChannel.Accept()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to accept channel.")
|
||||||
|
}
|
||||||
|
t.Log("Accepted channel")
|
||||||
|
|
||||||
|
go func(channelType string) {
|
||||||
|
defer channel.Close()
|
||||||
|
conn.OpenChannel(channelType, nil)
|
||||||
|
}(newChannel.ChannelType())
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return l.Addr().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockBrokenServer(t *testing.T) string {
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable tp listen for connection: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer l.Close()
|
||||||
|
c, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to accept incoming connection: %s", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
// This should block for a period of time longer than our timeout in
|
||||||
|
// the test case. That way we invoke a failure scenario.
|
||||||
|
t.Log("Block on handshaking for SSH connection")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return l.Addr().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommIsCommunicator(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &Communicator{}
|
||||||
|
if _, ok := raw.(communicator.Communicator); !ok {
|
||||||
|
t.Fatalf("Communicator must be a communicator")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew_Invalid(t *testing.T) {
|
||||||
|
clientConfig := &ssh.ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
Auth: []ssh.AuthMethod{
|
||||||
|
ssh.Password("i-am-invalid"),
|
||||||
|
},
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
address := newMockLineServer(t)
|
||||||
|
conn := func() (net.Conn, error) {
|
||||||
|
conn, err := net.Dial("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to accept incoming connection: %v", err)
|
||||||
|
}
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
Connection: conn,
|
||||||
|
SSHConfig: clientConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
comm, err := New(address, config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to setup communicator: %s", err)
|
||||||
|
}
|
||||||
|
err = comm.Connect()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have had an error connecting")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart(t *testing.T) {
|
||||||
|
clientConfig := &ssh.ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
Auth: []ssh.AuthMethod{
|
||||||
|
ssh.Password("pass"),
|
||||||
|
},
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
address := newMockLineServer(t)
|
||||||
|
conn := func() (net.Conn, error) {
|
||||||
|
conn, err := net.Dial("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to dial to remote side: %s", err)
|
||||||
|
}
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
Connection: conn,
|
||||||
|
SSHConfig: clientConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := New(address, config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error connecting to SSH: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &communicator.Cmd{
|
||||||
|
Command: "echo foo",
|
||||||
|
Stdout: new(bytes.Buffer),
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Start(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandshakeTimeout(t *testing.T) {
|
||||||
|
clientConfig := &ssh.ClientConfig{
|
||||||
|
User: "user",
|
||||||
|
Auth: []ssh.AuthMethod{
|
||||||
|
ssh.Password("pass"),
|
||||||
|
},
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
address := newMockBrokenServer(t)
|
||||||
|
conn := func() (net.Conn, error) {
|
||||||
|
conn, err := net.Dial("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to dial to remote side: %s", err)
|
||||||
|
}
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
Connection: conn,
|
||||||
|
SSHConfig: clientConfig,
|
||||||
|
HandshakeTimeout: 50 * time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
comm, err := New(address, config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to setup communicator: %s", err)
|
||||||
|
}
|
||||||
|
err = comm.Connect()
|
||||||
|
if err != ErrHandshakeTimeout {
|
||||||
|
// Note: there's another error that can come back from this call:
|
||||||
|
// ssh: handshake failed: EOF
|
||||||
|
// This should appear in cases where the handshake fails because of
|
||||||
|
// malformed (or no) data sent back by the server, but should not happen
|
||||||
|
// in a timeout scenario.
|
||||||
|
t.Fatalf("Expected handshake timeout, got: %s", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnectFunc is a convenience method for returning a function
|
||||||
|
// that just uses net.Dial to communicate with the remote end that
|
||||||
|
// is suitable for use with the SSH communicator configuration.
|
||||||
|
func ConnectFunc(network, addr string) func() (net.Conn, error) {
|
||||||
|
return func() (net.Conn, error) {
|
||||||
|
c, err := net.DialTimeout(network, addr, 15*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tcpConn, ok := c.(*net.TCPConn); ok {
|
||||||
|
tcpConn.SetKeepAlive(true)
|
||||||
|
tcpConn.SetKeepAlivePeriod(5 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyConnectFunc is a convenience method for returning a function
|
||||||
|
// that connects to a host using SOCKS5 proxy
|
||||||
|
func ProxyConnectFunc(socksProxy string, socksAuth *proxy.Auth, network, addr string) func() (net.Conn, error) {
|
||||||
|
return func() (net.Conn, error) {
|
||||||
|
// create a socks5 dialer
|
||||||
|
dialer, err := proxy.SOCKS5("tcp", socksProxy, socksAuth, proxy.Direct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Can't connect to the proxy: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := dialer.Dial(network, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BastionConnectFunc is a convenience method for returning a function
|
||||||
|
// that connects to a host over a bastion connection.
|
||||||
|
func BastionConnectFunc(
|
||||||
|
bProto string,
|
||||||
|
bAddr string,
|
||||||
|
bConf *ssh.ClientConfig,
|
||||||
|
proto string,
|
||||||
|
addr string) func() (net.Conn, error) {
|
||||||
|
return func() (net.Conn, error) {
|
||||||
|
// Connect to the bastion
|
||||||
|
bastion, err := ssh.Dial(bProto, bAddr, bConf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error connecting to bastion: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect through to the end host
|
||||||
|
conn, err := bastion.Dial(proto, addr)
|
||||||
|
if err != nil {
|
||||||
|
bastion.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap it up so we close both things properly
|
||||||
|
return &bastionConn{
|
||||||
|
Conn: conn,
|
||||||
|
Bastion: bastion,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type bastionConn struct {
|
||||||
|
net.Conn
|
||||||
|
Bastion *ssh.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *bastionConn) Close() error {
|
||||||
|
c.Conn.Close()
|
||||||
|
return c.Bastion.Close()
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// timeoutConn wraps a net.Conn, and sets a deadline for every read
|
||||||
|
// and write operation.
|
||||||
|
type timeoutConn struct {
|
||||||
|
net.Conn
|
||||||
|
ReadTimeout time.Duration
|
||||||
|
WriteTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *timeoutConn) Read(b []byte) (int, error) {
|
||||||
|
err := c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return c.Conn.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *timeoutConn) Write(b []byte) (int, error) {
|
||||||
|
err := c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return c.Conn.Write(b)
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An implementation of ssh.KeyboardInteractiveChallenge that simply sends
|
||||||
|
// back the password for all questions. The questions are logged.
|
||||||
|
func PasswordKeyboardInteractive(password string) ssh.KeyboardInteractiveChallenge {
|
||||||
|
return func(user, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||||
|
logger.Info("keyboard interactive challenge", "user", user,
|
||||||
|
"instructions", instruction)
|
||||||
|
for i, question := range questions {
|
||||||
|
logger.Info("challenge question", "number", i+1, "question", question)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just send the password back for all questions
|
||||||
|
answers := make([]string, len(questions))
|
||||||
|
for i := range answers {
|
||||||
|
answers[i] = password
|
||||||
|
}
|
||||||
|
|
||||||
|
return answers, nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPasswordKeyboardInteractive_Impl(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = PasswordKeyboardInteractive("foo")
|
||||||
|
if _, ok := raw.(ssh.KeyboardInteractiveChallenge); !ok {
|
||||||
|
t.Fatal("PasswordKeyboardInteractive must implement KeyboardInteractiveChallenge")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPasswordKeyboardInteractive_Challenge(t *testing.T) {
|
||||||
|
p := PasswordKeyboardInteractive("foo")
|
||||||
|
result, err := p("foo", "bar", []string{"one", "two"}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err not nil: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, []string{"foo", "foo"}) {
|
||||||
|
t.Fatalf("invalid password: %#v", result)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,257 @@
|
||||||
|
package winrm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
|
||||||
|
"github.com/masterzen/winrm"
|
||||||
|
"github.com/packer-community/winrmcp/winrmcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = vagrant.DefaultLogger().Named("communicator.winrm")
|
||||||
|
|
||||||
|
// Communicator represents the WinRM communicator
|
||||||
|
type Communicator struct {
|
||||||
|
config *Config
|
||||||
|
client *winrm.Client
|
||||||
|
endpoint *winrm.Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new communicator implementation over WinRM.
|
||||||
|
func New(config *Config) (*Communicator, error) {
|
||||||
|
endpoint := &winrm.Endpoint{
|
||||||
|
Host: config.Host,
|
||||||
|
Port: config.Port,
|
||||||
|
HTTPS: config.Https,
|
||||||
|
Insecure: config.Insecure,
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO
|
||||||
|
HTTPS: connInfo.HTTPS,
|
||||||
|
Insecure: connInfo.Insecure,
|
||||||
|
CACert: connInfo.CACert,
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the client
|
||||||
|
params := *winrm.DefaultParameters
|
||||||
|
|
||||||
|
if config.TransportDecorator != nil {
|
||||||
|
params.TransportDecorator = config.TransportDecorator
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Timeout = formatDuration(config.Timeout)
|
||||||
|
client, err := winrm.NewClientWithParameters(
|
||||||
|
endpoint, config.Username, config.Password, ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Communicator{
|
||||||
|
config: config,
|
||||||
|
client: client,
|
||||||
|
endpoint: endpoint,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Connect() (err error) {
|
||||||
|
// Create the shell to verify the connection
|
||||||
|
logger.Debug("connecting to remote shell")
|
||||||
|
shell, err := c.client.CreateShell()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("connection failure", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = shell.Close(); err != nil {
|
||||||
|
logger.Error("connection close failure", "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start implementation of communicator.Communicator interface
|
||||||
|
func (c *Communicator) Start(rc *communicator.Cmd) error {
|
||||||
|
shell, err := c.client.CreateShell()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("starting remote command", "commmand", rc.Command)
|
||||||
|
|
||||||
|
rc.Init()
|
||||||
|
cmd, err := shell.Execute(rc.Command)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go runCommand(shell, cmd, rc)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCommand(shell *winrm.Shell, cmd *winrm.Command, rc *communicator.Cmd) {
|
||||||
|
defer shell.Close()
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
copyFunc := func(w io.Writer, r io.Reader) {
|
||||||
|
defer wg.Done()
|
||||||
|
io.Copy(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rc.Stdout != nil && cmd.Stdout != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
go copyFunc(rc.Stdout, cmd.Stdout)
|
||||||
|
} else {
|
||||||
|
logger.Warn("failed to read stdout", "command", rc.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rc.Stderr != nil && cmd.Stderr != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
go copyFunc(rc.Stderr, cmd.Stderr)
|
||||||
|
} else {
|
||||||
|
logger.Warn("failed to read stderr", "command", rc.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Wait()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
code := cmd.ExitCode()
|
||||||
|
logger.Info("command complete", "exitcode", code, "command", rc.Command)
|
||||||
|
rc.SetExitStatus(code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload implementation of communicator.Communicator interface
|
||||||
|
func (c *Communicator) Upload(path string, input io.Reader, fi *os.FileInfo) error {
|
||||||
|
wcp, err := c.newCopyClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Was unable to create winrm client: %s", err)
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(path, `\`) {
|
||||||
|
// path is a directory
|
||||||
|
path += filepath.Base((*fi).Name())
|
||||||
|
}
|
||||||
|
logger.Info("uploading file", "path", path)
|
||||||
|
return wcp.Write(path, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadDir implementation of communicator.Communicator interface
|
||||||
|
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
|
||||||
|
if !strings.HasSuffix(src, "/") {
|
||||||
|
dst = fmt.Sprintf("%s\\%s", dst, filepath.Base(src))
|
||||||
|
}
|
||||||
|
logger.Info("uploading directory", "source", src, "destination", dst)
|
||||||
|
wcp, err := c.newCopyClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return wcp.Copy(src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Download(src string, dst io.Writer) error {
|
||||||
|
client, err := c.newWinRMClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeScript := `$file=[System.IO.File]::ReadAllBytes("%s"); Write-Output $([System.Convert]::ToBase64String($file))`
|
||||||
|
|
||||||
|
base64DecodePipe := &Base64Pipe{w: dst}
|
||||||
|
|
||||||
|
cmd := winrm.Powershell(fmt.Sprintf(encodeScript, src))
|
||||||
|
_, err = client.Run(cmd, base64DecodePipe, ioutil.Discard)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error {
|
||||||
|
return fmt.Errorf("WinRM doesn't support download dir.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) getClientConfig() *winrmcp.Config {
|
||||||
|
return &winrmcp.Config{
|
||||||
|
Auth: winrmcp.Auth{
|
||||||
|
User: c.config.Username,
|
||||||
|
Password: c.config.Password,
|
||||||
|
},
|
||||||
|
Https: c.config.Https,
|
||||||
|
Insecure: c.config.Insecure,
|
||||||
|
OperationTimeout: c.config.Timeout,
|
||||||
|
MaxOperationsPerShell: 15, // lowest common denominator
|
||||||
|
TransportDecorator: c.config.TransportDecorator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) newCopyClient() (*winrmcp.Winrmcp, error) {
|
||||||
|
addr := fmt.Sprintf("%s:%d", c.endpoint.Host, c.endpoint.Port)
|
||||||
|
clientConfig := c.getClientConfig()
|
||||||
|
return winrmcp.New(addr, clientConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) newWinRMClient() (*winrm.Client, error) {
|
||||||
|
conf := c.getClientConfig()
|
||||||
|
|
||||||
|
// Shamelessly borrowed from the winrmcp client to ensure
|
||||||
|
// that the client is configured using the same defaulting behaviors that
|
||||||
|
// winrmcp uses even we we aren't using winrmcp. This ensures similar
|
||||||
|
// behavior between upload, download, and copy functions. We can't use the
|
||||||
|
// one generated by winrmcp because it isn't exported.
|
||||||
|
var endpoint *winrm.Endpoint
|
||||||
|
endpoint = &winrm.Endpoint{
|
||||||
|
Host: c.endpoint.Host,
|
||||||
|
Port: c.endpoint.Port,
|
||||||
|
HTTPS: conf.Https,
|
||||||
|
Insecure: conf.Insecure,
|
||||||
|
TLSServerName: conf.TLSServerName,
|
||||||
|
CACert: conf.CACertBytes,
|
||||||
|
Timeout: conf.ConnectTimeout,
|
||||||
|
}
|
||||||
|
params := winrm.NewParameters(
|
||||||
|
winrm.DefaultParameters.Timeout,
|
||||||
|
winrm.DefaultParameters.Locale,
|
||||||
|
winrm.DefaultParameters.EnvelopeSize,
|
||||||
|
)
|
||||||
|
|
||||||
|
params.TransportDecorator = conf.TransportDecorator
|
||||||
|
params.Timeout = "PT3M"
|
||||||
|
|
||||||
|
client, err := winrm.NewClientWithParameters(
|
||||||
|
endpoint, conf.Auth.User, conf.Auth.Password, params)
|
||||||
|
return client, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Base64Pipe struct {
|
||||||
|
w io.Writer // underlying writer (file, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Base64Pipe) ReadFrom(r io.Reader) (int64, error) {
|
||||||
|
b, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var i int
|
||||||
|
i, err = d.Write(b)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(i), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Base64Pipe) Write(p []byte) (int, error) {
|
||||||
|
dst := make([]byte, base64.StdEncoding.DecodedLen(len(p)))
|
||||||
|
|
||||||
|
decodedBytes, err := base64.StdEncoding.Decode(dst, p)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.w.Write(dst[0:decodedBytes])
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
package winrm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dylanmei/winrmtest"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
|
||||||
|
)
|
||||||
|
|
||||||
|
const PAYLOAD = "stuff"
|
||||||
|
const BASE64_ENCODED_PAYLOAD = "c3R1ZmY="
|
||||||
|
|
||||||
|
func newMockWinRMServer(t *testing.T) *winrmtest.Remote {
|
||||||
|
wrm := winrmtest.NewRemote()
|
||||||
|
|
||||||
|
wrm.CommandFunc(
|
||||||
|
winrmtest.MatchText("echo foo"),
|
||||||
|
func(out, err io.Writer) int {
|
||||||
|
out.Write([]byte("foo"))
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
wrm.CommandFunc(
|
||||||
|
winrmtest.MatchPattern(`^echo c29tZXRoaW5n >> ".*"$`),
|
||||||
|
func(out, err io.Writer) int {
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
wrm.CommandFunc(
|
||||||
|
winrmtest.MatchPattern(`^echo `+BASE64_ENCODED_PAYLOAD+` >> ".*"$`),
|
||||||
|
func(out, err io.Writer) int {
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
wrm.CommandFunc(
|
||||||
|
winrmtest.MatchPattern(`^powershell.exe -EncodedCommand .*$`),
|
||||||
|
func(out, err io.Writer) int {
|
||||||
|
out.Write([]byte(BASE64_ENCODED_PAYLOAD))
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
wrm.CommandFunc(
|
||||||
|
winrmtest.MatchText("powershell"),
|
||||||
|
func(out, err io.Writer) int {
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
wrm.CommandFunc(
|
||||||
|
winrmtest.MatchText(`powershell -Command "(Get-Item C:/Temp/vagrant.cmd) -is [System.IO.DirectoryInfo]"`),
|
||||||
|
func(out, err io.Writer) int {
|
||||||
|
out.Write([]byte("False"))
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return wrm
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart(t *testing.T) {
|
||||||
|
wrm := newMockWinRMServer(t)
|
||||||
|
defer wrm.Close()
|
||||||
|
|
||||||
|
c, err := New(&Config{
|
||||||
|
Host: wrm.Host,
|
||||||
|
Port: wrm.Port,
|
||||||
|
Username: "user",
|
||||||
|
Password: "pass",
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating communicator: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd communicator.Cmd
|
||||||
|
stdout := new(bytes.Buffer)
|
||||||
|
cmd.Command = "echo foo"
|
||||||
|
cmd.Stdout = stdout
|
||||||
|
|
||||||
|
err = c.Start(&cmd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error executing remote command: %s", err)
|
||||||
|
}
|
||||||
|
cmd.Wait()
|
||||||
|
|
||||||
|
if stdout.String() != "foo" {
|
||||||
|
t.Fatalf("bad command response: expected %q, got %q", "foo", stdout.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpload(t *testing.T) {
|
||||||
|
wrm := newMockWinRMServer(t)
|
||||||
|
defer wrm.Close()
|
||||||
|
|
||||||
|
c, err := New(&Config{
|
||||||
|
Host: wrm.Host,
|
||||||
|
Port: wrm.Port,
|
||||||
|
Username: "user",
|
||||||
|
Password: "pass",
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating communicator: %s", err)
|
||||||
|
}
|
||||||
|
file := "C:/Temp/vagrant.cmd"
|
||||||
|
err = c.Upload(file, strings.NewReader(PAYLOAD), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error uploading file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := new(bytes.Buffer)
|
||||||
|
err = c.Download(file, dest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error downloading file: %s", err)
|
||||||
|
}
|
||||||
|
downloadedPayload := dest.String()
|
||||||
|
|
||||||
|
if downloadedPayload != PAYLOAD {
|
||||||
|
t.Fatalf("files are not equal: expected [%s] length: %v, got [%s] length %v", PAYLOAD, len(PAYLOAD), downloadedPayload, len(downloadedPayload))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package winrm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/masterzen/winrm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is used to configure the WinRM connection
|
||||||
|
type Config struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Timeout time.Duration
|
||||||
|
Https bool
|
||||||
|
Insecure bool
|
||||||
|
TransportDecorator func() winrm.Transporter
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package winrm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// formatDuration formats the given time.Duration into an ISO8601
|
||||||
|
// duration string.
|
||||||
|
func formatDuration(duration time.Duration) string {
|
||||||
|
// We're not supporting negative durations
|
||||||
|
if duration.Seconds() <= 0 {
|
||||||
|
return "PT0S"
|
||||||
|
}
|
||||||
|
|
||||||
|
h := int(duration.Hours())
|
||||||
|
m := int(duration.Minutes()) - (h * 60)
|
||||||
|
s := int(duration.Seconds()) - (h*3600 + m*60)
|
||||||
|
|
||||||
|
res := "PT"
|
||||||
|
if h > 0 {
|
||||||
|
res = fmt.Sprintf("%s%dH", res, h)
|
||||||
|
}
|
||||||
|
if m > 0 {
|
||||||
|
res = fmt.Sprintf("%s%dM", res, m)
|
||||||
|
}
|
||||||
|
if s > 0 {
|
||||||
|
res = fmt.Sprintf("%s%dS", res, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package winrm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormatDuration(t *testing.T) {
|
||||||
|
// Test complex duration with hours, minutes, seconds
|
||||||
|
d := time.Duration(3701) * time.Second
|
||||||
|
s := formatDuration(d)
|
||||||
|
if s != "PT1H1M41S" {
|
||||||
|
t.Fatalf("bad ISO 8601 duration string: %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test only minutes duration
|
||||||
|
d = time.Duration(20) * time.Minute
|
||||||
|
s = formatDuration(d)
|
||||||
|
if s != "PT20M" {
|
||||||
|
t.Fatalf("bad ISO 8601 duration string for 20M: %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test only seconds
|
||||||
|
d = time.Duration(1) * time.Second
|
||||||
|
s = formatDuration(d)
|
||||||
|
if s != "PT1S" {
|
||||||
|
t.Fatalf("bad ISO 8601 duration string for 1S: %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test negative duration (unsupported)
|
||||||
|
d = time.Duration(-1) * time.Second
|
||||||
|
s = formatDuration(d)
|
||||||
|
if s != "PT0S" {
|
||||||
|
t.Fatalf("bad ISO 8601 duration string for negative: %s", s)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config interface {
|
||||||
|
ConfigAttributes() (attrs []string, err error)
|
||||||
|
ConfigLoad(ctx context.Context, data map[string]interface{}) (loaddata map[string]interface{}, err error)
|
||||||
|
ConfigValidate(ctx context.Context, data map[string]interface{}, m *Machine) (errors []string, err error)
|
||||||
|
ConfigFinalize(ctx context.Context, data map[string]interface{}) (finaldata map[string]interface{}, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoConfig struct{}
|
||||||
|
|
||||||
|
func (c *NoConfig) ConfigAttributes() (a []string, e error) { return }
|
||||||
|
func (c *NoConfig) ConfigLoad(context.Context, map[string]interface{}) (d map[string]interface{}, e error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
func (c *NoConfig) ConfigValidate(context.Context, map[string]interface{}, *Machine) (es []string, e error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
func (c *NoConfig) ConfigFinalize(context.Context, map[string]interface{}) (f map[string]interface{}, e error) {
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Environment struct {
|
||||||
|
ActiveMachines map[string]string `json:"active_machines,omitempty"`
|
||||||
|
AliasesPath string `json:"aliases_path,omitempty"`
|
||||||
|
BoxesPath string `json:"boxes_path,omitempty"`
|
||||||
|
CWD string `json:"cwd,omitempty"`
|
||||||
|
DataDir string `json:"data_dir,omitempty"`
|
||||||
|
DefaultPrivateKeyPath string `json:"default_private_key_path,omitempty"`
|
||||||
|
GemsPath string `json:"gems_path,omitempty"`
|
||||||
|
HomePath string `json:"home_path,omitempty"`
|
||||||
|
LocalDataPath string `json:"local_data_path,omitempty"`
|
||||||
|
MachineNames []string `json:"machine_names,omitempty"`
|
||||||
|
PrimaryMachineName string `json:"primary_machine_name,omitempty"`
|
||||||
|
RootPath string `json:"root_path,omitempty"`
|
||||||
|
TmpPath string `json:"tmp_path,omitempty"`
|
||||||
|
VagrantfileName string `json:"vagrantfile_name,omitempty"`
|
||||||
|
UI Ui `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DumpEnvironment(e *Environment) (s string, err error) {
|
||||||
|
DefaultLogger().Debug("dumping environment to serialized data")
|
||||||
|
b, err := json.Marshal(e)
|
||||||
|
if err != nil {
|
||||||
|
DefaultLogger().Error("environment dump failure", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s = string(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadEnvironment(edata string, ios IOServer) (e *Environment, err error) {
|
||||||
|
DefaultLogger().Debug("loading environment from serialized data")
|
||||||
|
e = &Environment{}
|
||||||
|
err = json.Unmarshal([]byte(edata), e)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var stdout io.Writer
|
||||||
|
var stderr io.Writer
|
||||||
|
if ios == nil {
|
||||||
|
stdout = os.Stdout
|
||||||
|
stderr = os.Stderr
|
||||||
|
} else {
|
||||||
|
stdout = &IOWriter{target: "stdout", srv: ios}
|
||||||
|
stderr = &IOWriter{target: "stderr", srv: ios}
|
||||||
|
}
|
||||||
|
e.UI = &TargetedUi{
|
||||||
|
Target: "vagrant",
|
||||||
|
Ui: &ColoredUi{
|
||||||
|
ErrorColor: UiColorRed,
|
||||||
|
Ui: &BasicUi{
|
||||||
|
Reader: os.Stdin,
|
||||||
|
Writer: stdout,
|
||||||
|
ErrorWriter: stderr},
|
||||||
|
}}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadEnvironment(t *testing.T) {
|
||||||
|
env, err := LoadEnvironment("{}", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected load error: %s", err)
|
||||||
|
}
|
||||||
|
if env.UI == nil {
|
||||||
|
t.Fatalf("no UI configured for environment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadLoadEnvironment(t *testing.T) {
|
||||||
|
_, err := LoadEnvironment("ack", nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected load error but none provided")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadEnvironmentUIStdout(t *testing.T) {
|
||||||
|
iosrv := buildio()
|
||||||
|
env, err := LoadEnvironment("{}", iosrv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected load error: %s", err)
|
||||||
|
}
|
||||||
|
go func() { env.UI.Info("test string") }()
|
||||||
|
str := <-iosrv.Streams()["stdout"]
|
||||||
|
if !strings.Contains(str, "test string") {
|
||||||
|
t.Fatalf("unexpected output: %s", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadEnvironmentUIStderr(t *testing.T) {
|
||||||
|
iosrv := buildio()
|
||||||
|
env, err := LoadEnvironment("{}", iosrv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected load error: %s", err)
|
||||||
|
}
|
||||||
|
go func() { env.UI.Error("test string") }()
|
||||||
|
str, err := iosrv.Read("stderr")
|
||||||
|
if !strings.Contains(str, "test string") {
|
||||||
|
t.Fatalf("unexpected output: %s", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDumpEnvironment(t *testing.T) {
|
||||||
|
env, err := LoadEnvironment("{}", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected load error: %s", err)
|
||||||
|
}
|
||||||
|
d, err := DumpEnvironment(env)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected dump error: %s", err)
|
||||||
|
}
|
||||||
|
if d != "{}" {
|
||||||
|
t.Fatalf("unexpected dump information: %s", d)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
hclog "github.com/hashicorp/go-hclog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var GlobalIOServer *IOServer
|
||||||
|
|
||||||
|
var defaultLogger = hclog.Default().Named("vagrant")
|
||||||
|
|
||||||
|
func DefaultLogger() hclog.Logger {
|
||||||
|
return defaultLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetDefaultLogger(l hclog.Logger) {
|
||||||
|
defaultLogger = l
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StreamIO interface {
|
||||||
|
Read(target string) (content string, err error)
|
||||||
|
Write(content, target string) (n int, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IOServer interface {
|
||||||
|
Streams() map[string]chan (string)
|
||||||
|
StreamIO
|
||||||
|
}
|
||||||
|
|
||||||
|
type IOSrv struct {
|
||||||
|
Targets map[string]chan (string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IOSrv) Streams() map[string]chan (string) {
|
||||||
|
return i.Targets
|
||||||
|
}
|
||||||
|
|
||||||
|
type IOWriter struct {
|
||||||
|
target string
|
||||||
|
srv IOServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IOWriter) Write(b []byte) (n int, err error) {
|
||||||
|
content := string(b)
|
||||||
|
n, err = i.srv.Write(content, i.target)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IOSrv) Read(target string) (content string, err error) {
|
||||||
|
if _, ok := i.Streams()[target]; !ok {
|
||||||
|
err = errors.New("Unknown target defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content = <-i.Streams()[target]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IOSrv) Write(content, target string) (n int, err error) {
|
||||||
|
if _, ok := i.Streams()[target]; !ok {
|
||||||
|
err = errors.New("Unknown target defined")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i.Streams()[target] <- content
|
||||||
|
n = len(content)
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildio() IOServer {
|
||||||
|
return &IOSrv{
|
||||||
|
Targets: map[string]chan (string){
|
||||||
|
"stdout": make(chan string),
|
||||||
|
"stderr": make(chan string)}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIOSrvWrite(t *testing.T) {
|
||||||
|
iosrv := buildio()
|
||||||
|
var i int
|
||||||
|
go func() { i, _ = iosrv.Write("test string", "stdout") }()
|
||||||
|
_, _ = iosrv.Read("stdout")
|
||||||
|
if i != len("test string") {
|
||||||
|
t.Fatalf("unexpected write bytes %d != %d",
|
||||||
|
len("test string"), i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIOSrvRead(t *testing.T) {
|
||||||
|
iosrv := buildio()
|
||||||
|
go func() { _, _ = iosrv.Write("test string", "stdout") }()
|
||||||
|
r, _ := iosrv.Read("stdout")
|
||||||
|
if r != "test string" {
|
||||||
|
t.Fatalf("unexpected read result: %s", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIOSrvWriteBadTarget(t *testing.T) {
|
||||||
|
iosrv := buildio()
|
||||||
|
_, err := iosrv.Write("test string", "stdno")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error on write")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIOSrvReadBadTarget(t *testing.T) {
|
||||||
|
iosrv := buildio()
|
||||||
|
_, err := iosrv.Read("stdno")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error on read")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Machine struct {
|
||||||
|
Box Box `json:"box"`
|
||||||
|
Config map[string]interface{} `json:"config"`
|
||||||
|
DataDir string `json:"data_dir,omitempty"`
|
||||||
|
Env Environment `json:"environment"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ProviderConfig map[string]interface{} `json:"provider_config"`
|
||||||
|
ProviderName string `json:"provider_name,omitempty"`
|
||||||
|
ProviderOptions map[string]string `json:"provider_options"`
|
||||||
|
UI Ui `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DumpMachine(m *Machine) (s string, err error) {
|
||||||
|
DefaultLogger().Debug("dumping machine to serialized data")
|
||||||
|
b, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
DefaultLogger().Debug("machine dump failure", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s = string(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadMachine(mdata string, ios IOServer) (m *Machine, err error) {
|
||||||
|
DefaultLogger().Debug("loading machine from serialized data")
|
||||||
|
m = &Machine{}
|
||||||
|
err = json.Unmarshal([]byte(mdata), m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var stdout io.Writer
|
||||||
|
var stderr io.Writer
|
||||||
|
if ios == nil {
|
||||||
|
stdout = os.Stdout
|
||||||
|
stderr = os.Stderr
|
||||||
|
} else {
|
||||||
|
stdout = &IOWriter{target: "stdout", srv: ios}
|
||||||
|
stderr = &IOWriter{target: "stderr", srv: ios}
|
||||||
|
}
|
||||||
|
m.UI = &TargetedUi{
|
||||||
|
Target: m.Name,
|
||||||
|
Ui: &ColoredUi{
|
||||||
|
ErrorColor: UiColorRed,
|
||||||
|
Ui: &BasicUi{
|
||||||
|
Reader: os.Stdin,
|
||||||
|
Writer: stdout,
|
||||||
|
ErrorWriter: stderr},
|
||||||
|
}}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
type MachineState struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
ShortDesc string `json:"short_description"`
|
||||||
|
LongDesc string `json:"long_description"`
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMachineLoad(t *testing.T) {
|
||||||
|
_, err := LoadMachine("{}", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load machine: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMachineDump(t *testing.T) {
|
||||||
|
m, err := LoadMachine("{}", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected load error: %s", err)
|
||||||
|
}
|
||||||
|
_, err = DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to dump machine: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMachineUI(t *testing.T) {
|
||||||
|
iosrv := buildio()
|
||||||
|
m, err := LoadMachine("{}", iosrv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected load error: %s", err)
|
||||||
|
}
|
||||||
|
go func() { m.UI.Info("test string") }()
|
||||||
|
r, _ := iosrv.Read("stdout")
|
||||||
|
if !strings.Contains(r, "test string") {
|
||||||
|
t.Fatalf("unexpected read result: %s", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMachineUINamed(t *testing.T) {
|
||||||
|
iosrv := buildio()
|
||||||
|
m, err := LoadMachine("{\"name\":\"plugintest\"}", iosrv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected load error: %s", err)
|
||||||
|
}
|
||||||
|
go func() { m.UI.Info("test string") }()
|
||||||
|
r, _ := iosrv.Read("stdout")
|
||||||
|
if !strings.Contains(r, "test string") {
|
||||||
|
t.Fatalf("unexpected read result: %s", r)
|
||||||
|
}
|
||||||
|
if !strings.Contains(r, "plugintest") {
|
||||||
|
t.Fatalf("output does not contain name: %s", r)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,294 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
hclog "github.com/hashicorp/go-hclog"
|
||||||
|
go_plugin "github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Handshake = go_plugin.HandshakeConfig{
|
||||||
|
MagicCookieKey: "VAGRANT_PLUGIN_MAGIC_COOKIE",
|
||||||
|
MagicCookieValue: "1561a662a76642f98df77ad025aa13a9b16225d93f90475e91090fbe577317ed",
|
||||||
|
ProtocolVersion: 1}
|
||||||
|
ErrPluginShutdown = errors.New("plugin has shutdown")
|
||||||
|
)
|
||||||
|
|
||||||
|
type RemotePlugin interface {
|
||||||
|
Impl() interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoteConfig struct {
|
||||||
|
Client *go_plugin.Client
|
||||||
|
Config vagrant.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteConfig) Impl() interface{} {
|
||||||
|
return r.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoteProvider struct {
|
||||||
|
Client *go_plugin.Client
|
||||||
|
Provider Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteProvider) Impl() interface{} {
|
||||||
|
return r.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoteGuestCapabilities struct {
|
||||||
|
Client *go_plugin.Client
|
||||||
|
GuestCapabilities vagrant.GuestCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteGuestCapabilities) Impl() interface{} {
|
||||||
|
return r.GuestCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoteHostCapabilities struct {
|
||||||
|
Client *go_plugin.Client
|
||||||
|
HostCapabilities vagrant.HostCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteHostCapabilities) Impl() interface{} {
|
||||||
|
return r.HostCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoteProviderCapabilities struct {
|
||||||
|
Client *go_plugin.Client
|
||||||
|
ProviderCapabilities vagrant.ProviderCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteProviderCapabilities) Impl() interface{} {
|
||||||
|
return r.ProviderCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoteSyncedFolder struct {
|
||||||
|
Client *go_plugin.Client
|
||||||
|
SyncedFolder vagrant.SyncedFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSyncedFolder) Impl() interface{} {
|
||||||
|
return r.SyncedFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
type VagrantPlugin struct {
|
||||||
|
Providers map[string]*RemoteProvider
|
||||||
|
SyncedFolders map[string]*RemoteSyncedFolder
|
||||||
|
PluginDirectories []string
|
||||||
|
PluginLookup func(name, kind string) (p interface{}, err error)
|
||||||
|
Logger hclog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func VagrantPluginInit() *VagrantPlugin {
|
||||||
|
v := &VagrantPlugin{
|
||||||
|
PluginDirectories: []string{},
|
||||||
|
Providers: map[string]*RemoteProvider{},
|
||||||
|
SyncedFolders: map[string]*RemoteSyncedFolder{},
|
||||||
|
Logger: vagrant.DefaultLogger().Named("go-plugin")}
|
||||||
|
v.PluginLookup = v.DefaultPluginLookup
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VagrantPlugin) DefaultPluginLookup(name, kind string) (p interface{}, err error) {
|
||||||
|
switch kind {
|
||||||
|
case "provider":
|
||||||
|
p = v.Providers[name].Impl()
|
||||||
|
case "synced_folder":
|
||||||
|
p = v.SyncedFolders[name].Impl()
|
||||||
|
default:
|
||||||
|
err = errors.New("invalid plugin type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p == nil {
|
||||||
|
err = errors.New(fmt.Sprintf("Failed to locate %s plugin of type %s", name, kind))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VagrantPlugin) LoadPlugins(pluginPath string) error {
|
||||||
|
for _, p := range v.PluginDirectories {
|
||||||
|
if p == pluginPath {
|
||||||
|
v.Logger.Error("plugin directory path already loaded", "path", pluginPath)
|
||||||
|
return errors.New("plugin directory already loaded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.PluginDirectories = append(v.PluginDirectories, pluginPath)
|
||||||
|
if err := v.LoadProviders(pluginPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := v.LoadSyncedFolders(pluginPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VagrantPlugin) LoadProviders(pluginPath string) error {
|
||||||
|
providerPaths, err := go_plugin.Discover("*_provider", pluginPath)
|
||||||
|
if err != nil {
|
||||||
|
v.Logger.Error("error during plugin discovery", "type", "provider",
|
||||||
|
"error", err, "path", pluginPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, providerPath := range providerPaths {
|
||||||
|
v.Logger.Info("loading provider plugin", "path", providerPath)
|
||||||
|
|
||||||
|
client := go_plugin.NewClient(&go_plugin.ClientConfig{
|
||||||
|
AllowedProtocols: []go_plugin.Protocol{go_plugin.ProtocolGRPC},
|
||||||
|
Logger: v.Logger,
|
||||||
|
HandshakeConfig: Handshake,
|
||||||
|
Cmd: exec.Command(providerPath),
|
||||||
|
VersionedPlugins: map[int]go_plugin.PluginSet{
|
||||||
|
2: {"provider": &ProviderPlugin{}}}})
|
||||||
|
gclient, err := client.Client()
|
||||||
|
if err != nil {
|
||||||
|
v.Logger.Error("error loading provider client", "error", err, "path", providerPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
raw, err := gclient.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
v.Logger.Error("error loading provider plugin", "error", err, "path", providerPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
prov := raw.(Provider)
|
||||||
|
n := prov.Name()
|
||||||
|
v.Providers[n] = &RemoteProvider{
|
||||||
|
Client: client,
|
||||||
|
Provider: prov}
|
||||||
|
v.Logger.Info("plugin loaded", "type", "provider", "name", n, "path", providerPath)
|
||||||
|
go v.StreamIO("stdout", prov, n, "provider")
|
||||||
|
go v.StreamIO("stderr", prov, n, "provider")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VagrantPlugin) LoadSyncedFolders(pluginPath string) error {
|
||||||
|
folderPaths, err := go_plugin.Discover("*_synced_folder", pluginPath)
|
||||||
|
if err != nil {
|
||||||
|
v.Logger.Error("error during plugin discovery", "type", "synced_folder",
|
||||||
|
"error", err, "path", pluginPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, folderPath := range folderPaths {
|
||||||
|
v.Logger.Info("loading synced_folder plugin", "path", folderPath)
|
||||||
|
|
||||||
|
client := go_plugin.NewClient(&go_plugin.ClientConfig{
|
||||||
|
AllowedProtocols: []go_plugin.Protocol{go_plugin.ProtocolGRPC},
|
||||||
|
Logger: v.Logger,
|
||||||
|
HandshakeConfig: Handshake,
|
||||||
|
Cmd: exec.Command(folderPath),
|
||||||
|
VersionedPlugins: map[int]go_plugin.PluginSet{
|
||||||
|
2: {"synced_folders": &SyncedFolderPlugin{}}}})
|
||||||
|
gclient, err := client.Client()
|
||||||
|
if err != nil {
|
||||||
|
v.Logger.Error("error loading synced_folder client", "error", err, "path", folderPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
raw, err := gclient.Dispense("synced_folder")
|
||||||
|
if err != nil {
|
||||||
|
v.Logger.Error("error loading synced_folder plugin", "error", err, "path", folderPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fold := raw.(SyncedFolder)
|
||||||
|
n := fold.Name()
|
||||||
|
v.SyncedFolders[n] = &RemoteSyncedFolder{
|
||||||
|
Client: client,
|
||||||
|
SyncedFolder: fold}
|
||||||
|
v.Logger.Info("plugin loaded", "type", "synced_folder", "name", n, "path", folderPath)
|
||||||
|
go v.StreamIO("stdout", fold, n, "synced_folder")
|
||||||
|
go v.StreamIO("stderr", fold, n, "synced_folder")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VagrantPlugin) StreamIO(target string, i vagrant.IOServer, name, kind string) {
|
||||||
|
v.Logger.Info("starting plugin IO streaming", "target", target, "plugin", name, "type", kind)
|
||||||
|
for {
|
||||||
|
str, err := i.Read(target)
|
||||||
|
if err != nil {
|
||||||
|
v.Logger.Error("plugin IO streaming failure", "target", target, "plugin", name,
|
||||||
|
"type", kind, "error", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
v.Logger.Debug("received plugin IO content", "target", target, "plugin", name,
|
||||||
|
"type", kind, "content", str)
|
||||||
|
if target == "stdout" {
|
||||||
|
os.Stdout.Write([]byte(str))
|
||||||
|
} else if target == "stderr" {
|
||||||
|
os.Stderr.Write([]byte(str))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.Logger.Info("completed plugin IO streaming", "target", target, "plugin", name, "type", kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VagrantPlugin) Kill() {
|
||||||
|
v.Logger.Debug("killing all running plugins")
|
||||||
|
for n, p := range v.Providers {
|
||||||
|
v.Logger.Debug("killing plugin", "name", n, "type", "provider")
|
||||||
|
p.Client.Kill()
|
||||||
|
v.Logger.Info("plugin killed", "name", n, "type", "provider")
|
||||||
|
}
|
||||||
|
for n, p := range v.SyncedFolders {
|
||||||
|
v.Logger.Debug("killing plugin", "name", n, "type", "synced_folder")
|
||||||
|
p.Client.Kill()
|
||||||
|
v.Logger.Info("plugin killed", "name", n, "type", "synced_folder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper used for inspect GRPC related errors and providing "correct"
|
||||||
|
// error message
|
||||||
|
func handleGrpcError(err error, pluginCtx context.Context, reqCtx context.Context) error {
|
||||||
|
// If there was no error then nothing to process
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a request context is provided, check that it
|
||||||
|
// was not canceled or timed out. If no context
|
||||||
|
// provided, stub one for later.
|
||||||
|
if reqCtx != nil {
|
||||||
|
s := status.FromContextError(reqCtx.Err())
|
||||||
|
switch s.Code() {
|
||||||
|
case codes.Canceled:
|
||||||
|
return context.Canceled
|
||||||
|
case codes.DeadlineExceeded:
|
||||||
|
return context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reqCtx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
s, ok := status.FromError(err)
|
||||||
|
if ok && (s.Code() == codes.Unavailable || s.Code() == codes.Canceled) {
|
||||||
|
select {
|
||||||
|
case <-pluginCtx.Done():
|
||||||
|
err = ErrPluginShutdown
|
||||||
|
case <-reqCtx.Done():
|
||||||
|
err = reqCtx.Err()
|
||||||
|
select {
|
||||||
|
case <-pluginCtx.Done():
|
||||||
|
err = ErrPluginShutdown
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
case <-time.After(5):
|
||||||
|
return errors.New("exceeded context check timeout - " + err.Error())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
} else if s != nil && s.Message() != "" {
|
||||||
|
// Extract actual error message received
|
||||||
|
// and create new error
|
||||||
|
return errors.New(s.Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,365 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
go_plugin "github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
|
||||||
|
|
||||||
|
"github.com/LK4D4/joincontext"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GuestCapabilities interface {
|
||||||
|
vagrant.GuestCapabilities
|
||||||
|
Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
type GuestCapabilitiesPlugin struct {
|
||||||
|
go_plugin.NetRPCUnsupportedPlugin
|
||||||
|
Impl GuestCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GuestCapabilitiesPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
|
||||||
|
g.Impl.Init()
|
||||||
|
vagrant_proto.RegisterGuestCapabilitiesServer(s, &GRPCGuestCapabilitiesServer{
|
||||||
|
Impl: g.Impl,
|
||||||
|
GRPCIOServer: GRPCIOServer{
|
||||||
|
Impl: g.Impl}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GuestCapabilitiesPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||||
|
client := vagrant_proto.NewGuestCapabilitiesClient(c)
|
||||||
|
return &GRPCGuestCapabilitiesClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx,
|
||||||
|
GRPCIOClient: GRPCIOClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCGuestCapabilitiesServer struct {
|
||||||
|
GRPCIOServer
|
||||||
|
Impl GuestCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCGuestCapabilitiesServer) GuestCapabilities(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.SystemCapabilityList, err error) {
|
||||||
|
resp = &vagrant_proto.SystemCapabilityList{}
|
||||||
|
r, err := s.Impl.GuestCapabilities()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, cap := range r {
|
||||||
|
rcap := &vagrant_proto.SystemCapability{Name: cap.Name, Platform: cap.Platform}
|
||||||
|
resp.Capabilities = append(resp.Capabilities, rcap)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCGuestCapabilitiesServer) GuestCapability(ctx context.Context, req *vagrant_proto.GuestCapabilityRequest) (resp *vagrant_proto.GenericResponse, err error) {
|
||||||
|
resp = &vagrant_proto.GenericResponse{}
|
||||||
|
var args interface{}
|
||||||
|
if err = json.Unmarshal([]byte(req.Arguments), &args); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cap := &vagrant.SystemCapability{
|
||||||
|
Name: req.Capability.Name,
|
||||||
|
Platform: req.Capability.Platform}
|
||||||
|
r, err := s.Impl.GuestCapability(ctx, cap, args, machine)
|
||||||
|
result, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Result = string(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCGuestCapabilitiesClient struct {
|
||||||
|
GRPCCoreClient
|
||||||
|
GRPCIOClient
|
||||||
|
client vagrant_proto.GuestCapabilitiesClient
|
||||||
|
doneCtx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCGuestCapabilitiesClient) GuestCapabilities() (caps []vagrant.SystemCapability, err error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.GuestCapabilities(jctx, &vagrant_proto.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
caps = make([]vagrant.SystemCapability, len(resp.Capabilities))
|
||||||
|
for i := 0; i < len(resp.Capabilities); i++ {
|
||||||
|
cap := vagrant.SystemCapability{
|
||||||
|
Name: resp.Capabilities[i].Name,
|
||||||
|
Platform: resp.Capabilities[i].Platform}
|
||||||
|
caps[i] = cap
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCGuestCapabilitiesClient) GuestCapability(ctx context.Context, cap *vagrant.SystemCapability, args interface{}, machine *vagrant.Machine) (result interface{}, err error) {
|
||||||
|
a, err := json.Marshal(args)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m, err := vagrant.DumpMachine(machine)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.GuestCapability(jctx, &vagrant_proto.GuestCapabilityRequest{
|
||||||
|
Capability: &vagrant_proto.SystemCapability{Name: cap.Name, Platform: cap.Platform},
|
||||||
|
Machine: m,
|
||||||
|
Arguments: string(a)})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp.Result), &result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type HostCapabilities interface {
|
||||||
|
vagrant.HostCapabilities
|
||||||
|
Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
type HostCapabilitiesPlugin struct {
|
||||||
|
go_plugin.NetRPCUnsupportedPlugin
|
||||||
|
Impl HostCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HostCapabilitiesPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
|
||||||
|
h.Impl.Init()
|
||||||
|
vagrant_proto.RegisterHostCapabilitiesServer(s, &GRPCHostCapabilitiesServer{
|
||||||
|
Impl: h.Impl,
|
||||||
|
GRPCIOServer: GRPCIOServer{
|
||||||
|
Impl: h.Impl}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HostCapabilitiesPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||||
|
client := vagrant_proto.NewHostCapabilitiesClient(c)
|
||||||
|
return &GRPCHostCapabilitiesClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx,
|
||||||
|
GRPCIOClient: GRPCIOClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCHostCapabilitiesServer struct {
|
||||||
|
GRPCIOServer
|
||||||
|
Impl HostCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCHostCapabilitiesServer) HostCapabilities(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.SystemCapabilityList, err error) {
|
||||||
|
resp = &vagrant_proto.SystemCapabilityList{}
|
||||||
|
r, err := s.Impl.HostCapabilities()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, cap := range r {
|
||||||
|
rcap := &vagrant_proto.SystemCapability{Name: cap.Name, Platform: cap.Platform}
|
||||||
|
resp.Capabilities = append(resp.Capabilities, rcap)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCHostCapabilitiesServer) HostCapability(ctx context.Context, req *vagrant_proto.HostCapabilityRequest) (resp *vagrant_proto.GenericResponse, err error) {
|
||||||
|
resp = &vagrant_proto.GenericResponse{}
|
||||||
|
var args interface{}
|
||||||
|
if err = json.Unmarshal([]byte(req.Arguments), &args); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
env, err := vagrant.LoadEnvironment(req.Environment, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cap := &vagrant.SystemCapability{
|
||||||
|
Name: req.Capability.Name,
|
||||||
|
Platform: req.Capability.Platform}
|
||||||
|
|
||||||
|
r, err := s.Impl.HostCapability(ctx, cap, args, env)
|
||||||
|
result, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Result = string(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCHostCapabilitiesClient struct {
|
||||||
|
GRPCCoreClient
|
||||||
|
GRPCIOClient
|
||||||
|
client vagrant_proto.HostCapabilitiesClient
|
||||||
|
doneCtx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCHostCapabilitiesClient) HostCapabilities() (caps []vagrant.SystemCapability, err error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.HostCapabilities(jctx, &vagrant_proto.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
caps = make([]vagrant.SystemCapability, len(resp.Capabilities))
|
||||||
|
for i := 0; i < len(resp.Capabilities); i++ {
|
||||||
|
cap := vagrant.SystemCapability{
|
||||||
|
Name: resp.Capabilities[i].Name,
|
||||||
|
Platform: resp.Capabilities[i].Platform}
|
||||||
|
caps[i] = cap
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCHostCapabilitiesClient) HostCapability(ctx context.Context, cap *vagrant.SystemCapability, args interface{}, env *vagrant.Environment) (result interface{}, err error) {
|
||||||
|
a, err := json.Marshal(args)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e, err := vagrant.DumpEnvironment(env)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.HostCapability(jctx, &vagrant_proto.HostCapabilityRequest{
|
||||||
|
Capability: &vagrant_proto.SystemCapability{
|
||||||
|
Name: cap.Name,
|
||||||
|
Platform: cap.Platform},
|
||||||
|
Environment: e,
|
||||||
|
Arguments: string(a)})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp.Result), &result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderCapabilities interface {
|
||||||
|
vagrant.ProviderCapabilities
|
||||||
|
Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderCapabilitiesPlugin struct {
|
||||||
|
go_plugin.NetRPCUnsupportedPlugin
|
||||||
|
Impl ProviderCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProviderCapabilitiesPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
|
||||||
|
p.Impl.Init()
|
||||||
|
vagrant_proto.RegisterProviderCapabilitiesServer(s, &GRPCProviderCapabilitiesServer{
|
||||||
|
Impl: p.Impl,
|
||||||
|
GRPCIOServer: GRPCIOServer{
|
||||||
|
Impl: p.Impl}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProviderCapabilitiesPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||||
|
client := vagrant_proto.NewProviderCapabilitiesClient(c)
|
||||||
|
return &GRPCProviderCapabilitiesClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx,
|
||||||
|
GRPCIOClient: GRPCIOClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCProviderCapabilitiesServer struct {
|
||||||
|
GRPCIOServer
|
||||||
|
Impl ProviderCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCProviderCapabilitiesServer) ProviderCapabilities(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.ProviderCapabilityList, err error) {
|
||||||
|
resp = &vagrant_proto.ProviderCapabilityList{}
|
||||||
|
r, err := s.Impl.ProviderCapabilities()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, cap := range r {
|
||||||
|
rcap := &vagrant_proto.ProviderCapability{Name: cap.Name, Provider: cap.Provider}
|
||||||
|
resp.Capabilities = append(resp.Capabilities, rcap)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCProviderCapabilitiesServer) ProviderCapability(ctx context.Context, req *vagrant_proto.ProviderCapabilityRequest) (resp *vagrant_proto.GenericResponse, err error) {
|
||||||
|
resp = &vagrant_proto.GenericResponse{}
|
||||||
|
var args interface{}
|
||||||
|
if err = json.Unmarshal([]byte(req.Arguments), &args); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cap := &vagrant.ProviderCapability{
|
||||||
|
Name: req.Capability.Name,
|
||||||
|
Provider: req.Capability.Provider}
|
||||||
|
|
||||||
|
r, err := s.Impl.ProviderCapability(ctx, cap, args, m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Result = string(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCProviderCapabilitiesClient struct {
|
||||||
|
GRPCCoreClient
|
||||||
|
GRPCIOClient
|
||||||
|
client vagrant_proto.ProviderCapabilitiesClient
|
||||||
|
doneCtx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCProviderCapabilitiesClient) ProviderCapabilities() (caps []vagrant.ProviderCapability, err error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.ProviderCapabilities(jctx, &vagrant_proto.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
caps = make([]vagrant.ProviderCapability, len(resp.Capabilities))
|
||||||
|
for i := 0; i < len(resp.Capabilities); i++ {
|
||||||
|
cap := vagrant.ProviderCapability{
|
||||||
|
Name: resp.Capabilities[i].Name,
|
||||||
|
Provider: resp.Capabilities[i].Provider}
|
||||||
|
caps[i] = cap
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCProviderCapabilitiesClient) ProviderCapability(ctx context.Context, cap *vagrant.ProviderCapability, args interface{}, machine *vagrant.Machine) (result interface{}, err error) {
|
||||||
|
a, err := json.Marshal(args)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m, err := vagrant.DumpMachine(machine)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.ProviderCapability(jctx, &vagrant_proto.ProviderCapabilityRequest{
|
||||||
|
Capability: &vagrant_proto.ProviderCapability{
|
||||||
|
Name: cap.Name,
|
||||||
|
Provider: cap.Provider},
|
||||||
|
Machine: m,
|
||||||
|
Arguments: string(a)})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp.Result), &result)
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,499 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCapabilities_GuestCapabilities(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(GuestCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
resp, err := impl.GuestCapabilities()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
if len(resp) != 1 {
|
||||||
|
t.Fatalf("length %d != 1", len(resp))
|
||||||
|
}
|
||||||
|
if resp[0].Name != "test_cap" {
|
||||||
|
t.Errorf("name - %s != test_cap", resp[0].Name)
|
||||||
|
}
|
||||||
|
if resp[0].Platform != "testOS" {
|
||||||
|
t.Errorf("platform - %s != testOS", resp[0].Platform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_GuestCapability(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(GuestCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cap := &vagrant.SystemCapability{
|
||||||
|
Name: "test_cap",
|
||||||
|
Platform: "TestOS"}
|
||||||
|
m := &vagrant.Machine{}
|
||||||
|
args := []string{"test_value", "next_test_value"}
|
||||||
|
|
||||||
|
resp, err := impl.GuestCapability(context.Background(), cap, args, m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
result, ok := resp.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", result)
|
||||||
|
}
|
||||||
|
if result[0] != "test_cap" {
|
||||||
|
t.Errorf("%s != test_cap", result[0])
|
||||||
|
}
|
||||||
|
if result[1] != "test_value" {
|
||||||
|
t.Errorf("%s != test_value", result[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_GuestCapability_noargs(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(GuestCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cap := &vagrant.SystemCapability{
|
||||||
|
Name: "test_cap",
|
||||||
|
Platform: "TestOS"}
|
||||||
|
m := &vagrant.Machine{}
|
||||||
|
var args interface{}
|
||||||
|
args = nil
|
||||||
|
|
||||||
|
resp, err := impl.GuestCapability(context.Background(), cap, args, m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
result, ok := resp.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", result)
|
||||||
|
}
|
||||||
|
if result[0] != "test_cap" {
|
||||||
|
t.Errorf("%s != test_cap", result[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_GuestCapability_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(GuestCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cap := &vagrant.SystemCapability{
|
||||||
|
Name: "test_cap",
|
||||||
|
Platform: "TestOS"}
|
||||||
|
m := &vagrant.Machine{}
|
||||||
|
args := []string{"pause", "test_value", "next_test_value"}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.GuestCapability(ctx, cap, args, m)
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_GuestCapability_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(GuestCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cap := &vagrant.SystemCapability{
|
||||||
|
Name: "test_cap",
|
||||||
|
Platform: "TestOS"}
|
||||||
|
m := &vagrant.Machine{}
|
||||||
|
args := []string{"pause", "test_value", "next_test_value"}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
_, err = impl.GuestCapability(ctx, cap, args, m)
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_HostCapabilities(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(HostCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
resp, err := impl.HostCapabilities()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
if len(resp) != 1 {
|
||||||
|
t.Fatalf("length %d != 1", len(resp))
|
||||||
|
}
|
||||||
|
if resp[0].Name != "test_cap" {
|
||||||
|
t.Errorf("name - %s != test_cap", resp[0].Name)
|
||||||
|
}
|
||||||
|
if resp[0].Platform != "testOS" {
|
||||||
|
t.Errorf("platform - %s != testOS", resp[0].Platform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_HostCapability(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(HostCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cap := &vagrant.SystemCapability{
|
||||||
|
Name: "test_cap",
|
||||||
|
Platform: "TestOS"}
|
||||||
|
e := &vagrant.Environment{}
|
||||||
|
args := []string{"test_value", "next_test_value"}
|
||||||
|
|
||||||
|
resp, err := impl.HostCapability(context.Background(), cap, args, e)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
result, ok := resp.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", result)
|
||||||
|
}
|
||||||
|
if result[0] != "test_cap" {
|
||||||
|
t.Errorf("%s != test_cap", result[0])
|
||||||
|
}
|
||||||
|
if result[1] != "test_value" {
|
||||||
|
t.Errorf("%s != test_value", result[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_HostCapability_noargs(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(HostCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cap := &vagrant.SystemCapability{
|
||||||
|
Name: "test_cap",
|
||||||
|
Platform: "TestOS"}
|
||||||
|
e := &vagrant.Environment{}
|
||||||
|
var args interface{}
|
||||||
|
args = nil
|
||||||
|
|
||||||
|
resp, err := impl.HostCapability(context.Background(), cap, args, e)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
result, ok := resp.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", result)
|
||||||
|
}
|
||||||
|
if result[0] != "test_cap" {
|
||||||
|
t.Errorf("%s != test_cap", result[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_HostCapability_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(HostCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cap := &vagrant.SystemCapability{
|
||||||
|
Name: "test_cap",
|
||||||
|
Platform: "TestOS"}
|
||||||
|
e := &vagrant.Environment{}
|
||||||
|
args := []string{"pause", "test_value", "next_test_value"}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.HostCapability(ctx, cap, args, e)
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_HostCapability_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(HostCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cap := &vagrant.SystemCapability{
|
||||||
|
Name: "test_cap",
|
||||||
|
Platform: "TestOS"}
|
||||||
|
e := &vagrant.Environment{}
|
||||||
|
args := []string{"pause", "test_value", "next_test_value"}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
_, err = impl.HostCapability(ctx, cap, args, e)
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_ProviderCapabilities(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(ProviderCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
resp, err := impl.ProviderCapabilities()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
if len(resp) != 1 {
|
||||||
|
t.Fatalf("length %d != 1", len(resp))
|
||||||
|
}
|
||||||
|
if resp[0].Name != "test_cap" {
|
||||||
|
t.Errorf("name - %s != test_cap", resp[0].Name)
|
||||||
|
}
|
||||||
|
if resp[0].Provider != "testProvider" {
|
||||||
|
t.Errorf("provider - %s != testProvdier", resp[0].Provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_ProviderCapability(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(ProviderCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cap := &vagrant.ProviderCapability{
|
||||||
|
Name: "test_cap",
|
||||||
|
Provider: "test_provider"}
|
||||||
|
m := &vagrant.Machine{}
|
||||||
|
args := []string{"test_value", "next_test_value"}
|
||||||
|
|
||||||
|
resp, err := impl.ProviderCapability(context.Background(), cap, args, m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
result, ok := resp.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", result)
|
||||||
|
}
|
||||||
|
if result[0] != "test_cap" {
|
||||||
|
t.Errorf("%s != test_cap", result[0])
|
||||||
|
}
|
||||||
|
if result[1] != "test_value" {
|
||||||
|
t.Errorf("%s != test_value", result[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_ProviderCapability_noargs(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(ProviderCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cap := &vagrant.ProviderCapability{
|
||||||
|
Name: "test_cap",
|
||||||
|
Provider: "test_provider"}
|
||||||
|
m := &vagrant.Machine{}
|
||||||
|
var args interface{}
|
||||||
|
args = nil
|
||||||
|
|
||||||
|
resp, err := impl.ProviderCapability(context.Background(), cap, args, m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
result, ok := resp.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", result)
|
||||||
|
}
|
||||||
|
if result[0] != "test_cap" {
|
||||||
|
t.Errorf("%s != test_cap", result[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_ProviderCapability_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(ProviderCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cap := &vagrant.ProviderCapability{
|
||||||
|
Name: "test_cap",
|
||||||
|
Provider: "test_provider"}
|
||||||
|
m := &vagrant.Machine{}
|
||||||
|
args := []string{"pause", "test_value", "next_test_value"}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.ProviderCapability(ctx, cap, args, m)
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilities_ProviderCapability_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("caps")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(ProviderCapabilities)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cap := &vagrant.ProviderCapability{
|
||||||
|
Name: "test_cap",
|
||||||
|
Provider: "test_provider"}
|
||||||
|
m := &vagrant.Machine{}
|
||||||
|
args := []string{"pause", "test_value", "next_test_value"}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
_, err = impl.ProviderCapability(ctx, cap, args, m)
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
go_plugin "github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
|
||||||
|
|
||||||
|
"github.com/LK4D4/joincontext"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config interface {
|
||||||
|
vagrant.Config
|
||||||
|
Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigPlugin struct {
|
||||||
|
go_plugin.NetRPCUnsupportedPlugin
|
||||||
|
Impl Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
|
||||||
|
c.Impl.Init()
|
||||||
|
vagrant_proto.RegisterConfigServer(s, &GRPCConfigServer{
|
||||||
|
Impl: c.Impl,
|
||||||
|
GRPCIOServer: GRPCIOServer{
|
||||||
|
Impl: c.Impl}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, con *grpc.ClientConn) (interface{}, error) {
|
||||||
|
client := vagrant_proto.NewConfigClient(con)
|
||||||
|
return &GRPCConfigClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx,
|
||||||
|
GRPCIOClient: GRPCIOClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCConfigServer struct {
|
||||||
|
GRPCIOServer
|
||||||
|
Impl Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCConfigServer) ConfigAttributes(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.ListResponse, err error) {
|
||||||
|
resp = &vagrant_proto.ListResponse{}
|
||||||
|
resp.Items, err = s.Impl.ConfigAttributes()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCConfigServer) ConfigLoad(ctx context.Context, req *vagrant_proto.Configuration) (resp *vagrant_proto.Configuration, err error) {
|
||||||
|
resp = &vagrant_proto.Configuration{}
|
||||||
|
var data map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(req.Data), &data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := s.Impl.ConfigLoad(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mdata, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Data = string(mdata)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCConfigServer) ConfigValidate(ctx context.Context, req *vagrant_proto.Configuration) (resp *vagrant_proto.ListResponse, err error) {
|
||||||
|
resp = &vagrant_proto.ListResponse{}
|
||||||
|
var data map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(req.Data), &data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Items, err = s.Impl.ConfigValidate(ctx, data, m)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCConfigServer) ConfigFinalize(ctx context.Context, req *vagrant_proto.Configuration) (resp *vagrant_proto.Configuration, err error) {
|
||||||
|
resp = &vagrant_proto.Configuration{}
|
||||||
|
var data map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(req.Data), &data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := s.Impl.ConfigFinalize(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mdata, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Data = string(mdata)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCConfigClient struct {
|
||||||
|
GRPCCoreClient
|
||||||
|
GRPCIOClient
|
||||||
|
client vagrant_proto.ConfigClient
|
||||||
|
doneCtx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCConfigClient) ConfigAttributes() (attrs []string, err error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.ConfigAttributes(jctx, &vagrant_proto.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, nil)
|
||||||
|
}
|
||||||
|
attrs = resp.Items
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCConfigClient) ConfigLoad(ctx context.Context, data map[string]interface{}) (loaddata map[string]interface{}, err error) {
|
||||||
|
mdata, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.ConfigLoad(jctx, &vagrant_proto.Configuration{
|
||||||
|
Data: string(mdata)})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp.Data), &loaddata)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCConfigClient) ConfigValidate(ctx context.Context, data map[string]interface{}, m *vagrant.Machine) (errs []string, err error) {
|
||||||
|
machData, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mdata, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.ConfigValidate(jctx, &vagrant_proto.Configuration{
|
||||||
|
Data: string(mdata),
|
||||||
|
Machine: machData})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
errs = resp.Items
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCConfigClient) ConfigFinalize(ctx context.Context, data map[string]interface{}) (finaldata map[string]interface{}, err error) {
|
||||||
|
mdata, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.ConfigFinalize(jctx, &vagrant_proto.Configuration{
|
||||||
|
Data: string(mdata)})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp.Data), &finaldata)
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,246 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigPlugin_Attributes(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("configs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Config)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := impl.ConfigAttributes()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp %s", err)
|
||||||
|
}
|
||||||
|
if resp[0] != "fubar" {
|
||||||
|
t.Errorf("%s != fubar", resp[0])
|
||||||
|
}
|
||||||
|
if resp[1] != "foobar" {
|
||||||
|
t.Errorf("%s != foobar", resp[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigPlugin_Load(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("configs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Config)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{}
|
||||||
|
var resp map[string]interface{}
|
||||||
|
resp, err = impl.ConfigLoad(context.Background(), data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
if _, ok := resp["test_key"]; !ok {
|
||||||
|
t.Fatalf("bad resp content %#v", resp)
|
||||||
|
}
|
||||||
|
v := resp["test_key"].(string)
|
||||||
|
if v != "test_val" {
|
||||||
|
t.Errorf("%s != test_val", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigPlugin_Load_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("configs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Config)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{"pause": true}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
_, err = impl.ConfigLoad(ctx, data)
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigPlugin_Load_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("configs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Config)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{"pause": true}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.ConfigLoad(ctx, data)
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigPlugin_Validate(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("configs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Config)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{}
|
||||||
|
machine := &vagrant.Machine{}
|
||||||
|
|
||||||
|
resp, err := impl.ConfigValidate(context.Background(), data, machine)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
if len(resp) != 1 {
|
||||||
|
t.Fatalf("bad size %d != 1", len(resp))
|
||||||
|
}
|
||||||
|
if resp[0] != "test error" {
|
||||||
|
t.Errorf("%s != test error", resp[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigPlugin_Validate_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("configs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Config)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{"pause": true}
|
||||||
|
machine := &vagrant.Machine{}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.ConfigValidate(ctx, data, machine)
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigPlugin_Finalize(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("configs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Config)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"test_key": "test_val",
|
||||||
|
"other_key": "other_val"}
|
||||||
|
|
||||||
|
resp, err := impl.ConfigFinalize(context.Background(), data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
if _, ok := resp["test_key"]; !ok {
|
||||||
|
t.Fatalf("bad resp content %#v", resp)
|
||||||
|
}
|
||||||
|
v := resp["test_key"].(string)
|
||||||
|
if v != "test_val-updated" {
|
||||||
|
t.Errorf("%s != test_val-updated", v)
|
||||||
|
}
|
||||||
|
v = resp["other_key"].(string)
|
||||||
|
if v != "other_val-updated" {
|
||||||
|
t.Errorf("%s != other_val-updated", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigPlugin_Finalize_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("configs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Config)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"pause": true,
|
||||||
|
"test_key": "test_val",
|
||||||
|
"other_key": "other_val"}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.ConfigFinalize(ctx, data)
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
go_plugin "github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
|
||||||
|
|
||||||
|
"github.com/LK4D4/joincontext"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IO interface {
|
||||||
|
vagrant.StreamIO
|
||||||
|
}
|
||||||
|
|
||||||
|
type IOPlugin struct {
|
||||||
|
go_plugin.NetRPCUnsupportedPlugin
|
||||||
|
Impl vagrant.StreamIO
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCIOServer struct {
|
||||||
|
Impl vagrant.StreamIO
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCIOServer) Read(ctx context.Context, req *vagrant_proto.Identifier) (r *vagrant_proto.Content, err error) {
|
||||||
|
r = &vagrant_proto.Content{}
|
||||||
|
r.Value, err = s.Impl.Read(req.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCIOServer) Write(ctx context.Context, req *vagrant_proto.Content) (r *vagrant_proto.WriteResponse, err error) {
|
||||||
|
r = &vagrant_proto.WriteResponse{}
|
||||||
|
bytes := 0
|
||||||
|
bytes, err = s.Impl.Write(req.Value, req.Target)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Length = int32(bytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCIOClient struct {
|
||||||
|
client vagrant_proto.IOClient
|
||||||
|
doneCtx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCIOClient) Read(target string) (content string, err error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.Read(jctx, &vagrant_proto.Identifier{
|
||||||
|
Name: target})
|
||||||
|
if err != nil {
|
||||||
|
return content, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
content = resp.Value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCIOClient) Write(content, target string) (length int, err error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.Write(jctx, &vagrant_proto.Content{
|
||||||
|
Value: content,
|
||||||
|
Target: target})
|
||||||
|
if err != nil {
|
||||||
|
return length, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
length = int(resp.Length)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IOPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
|
||||||
|
vagrant_proto.RegisterIOServer(s, &GRPCIOServer{Impl: i.Impl})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IOPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||||
|
return &GRPCIOClient{
|
||||||
|
client: vagrant_proto.NewIOClient(c),
|
||||||
|
doneCtx: ctx}, nil
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockIO struct {
|
||||||
|
Core
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIO_ReadWrite(t *testing.T) {
|
||||||
|
ioplugin := &MockIO{}
|
||||||
|
ioplugin.Init()
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"io": &IOPlugin{Impl: ioplugin}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("io")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(IO)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
length, err := impl.Write("test_message", "stdout")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad write: %s", err)
|
||||||
|
}
|
||||||
|
if length != len("test_message") {
|
||||||
|
t.Fatalf("bad length %d != %d", length, len("test_message"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resp, err := impl.Read("stdout")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad read: %s", err)
|
||||||
|
}
|
||||||
|
if resp != "test_message" {
|
||||||
|
t.Errorf("%s != test_message", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIO_Write_bad(t *testing.T) {
|
||||||
|
ioplugin := &MockIO{}
|
||||||
|
ioplugin.Init()
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"io": &IOPlugin{Impl: ioplugin}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("io")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(IO)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
_, err = impl.Write("test_message", "bad-target")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("illegal write")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIO_Read_bad(t *testing.T) {
|
||||||
|
ioplugin := &MockIO{}
|
||||||
|
ioplugin.Init()
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"io": &IOPlugin{Impl: ioplugin}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("io")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(IO)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
_, err = impl.Read("bad-target")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("illegal read")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Meta interface {
|
||||||
|
Init()
|
||||||
|
vagrant.IOServer
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCCoreClient struct{}
|
||||||
|
|
||||||
|
func (c *GRPCCoreClient) Init() {}
|
||||||
|
func (c *GRPCCoreClient) Streams() (s map[string]chan (string)) { return }
|
||||||
|
|
||||||
|
type Core struct {
|
||||||
|
vagrant.IOServer
|
||||||
|
io vagrant.StreamIO
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) Init() {
|
||||||
|
if c.io == nil {
|
||||||
|
c.io = &vagrant.IOSrv{
|
||||||
|
map[string]chan (string){
|
||||||
|
"stdout": make(chan string),
|
||||||
|
"stderr": make(chan string),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) Read(target string) (string, error) {
|
||||||
|
return c.io.Read(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) Write(content, target string) (int, error) {
|
||||||
|
return c.io.Write(content, target)
|
||||||
|
}
|
|
@ -0,0 +1,300 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockGuestCapabilities struct{ Core }
|
||||||
|
|
||||||
|
func (g *MockGuestCapabilities) GuestCapabilities() (caps []vagrant.SystemCapability, err error) {
|
||||||
|
caps = []vagrant.SystemCapability{
|
||||||
|
vagrant.SystemCapability{Name: "test_cap", Platform: "testOS"}}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *MockGuestCapabilities) GuestCapability(ctx context.Context, cap *vagrant.SystemCapability, args interface{}, m *vagrant.Machine) (result interface{}, err error) {
|
||||||
|
if args != nil {
|
||||||
|
arguments := args.([]interface{})
|
||||||
|
if arguments[0] == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
if len(arguments) > 0 {
|
||||||
|
result = []string{
|
||||||
|
cap.Name,
|
||||||
|
arguments[0].(string)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = []string{cap.Name}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockHostCapabilities struct{ Core }
|
||||||
|
|
||||||
|
func (h *MockHostCapabilities) HostCapabilities() (caps []vagrant.SystemCapability, err error) {
|
||||||
|
caps = []vagrant.SystemCapability{
|
||||||
|
vagrant.SystemCapability{Name: "test_cap", Platform: "testOS"}}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MockHostCapabilities) HostCapability(ctx context.Context, cap *vagrant.SystemCapability, args interface{}, e *vagrant.Environment) (result interface{}, err error) {
|
||||||
|
if args != nil {
|
||||||
|
arguments := args.([]interface{})
|
||||||
|
if arguments[0] == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
if len(arguments) > 0 {
|
||||||
|
result = []string{
|
||||||
|
cap.Name,
|
||||||
|
arguments[0].(string)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = []string{cap.Name}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockProviderCapabilities struct{ Core }
|
||||||
|
|
||||||
|
func (p *MockProviderCapabilities) ProviderCapabilities() (caps []vagrant.ProviderCapability, err error) {
|
||||||
|
caps = []vagrant.ProviderCapability{
|
||||||
|
vagrant.ProviderCapability{Name: "test_cap", Provider: "testProvider"}}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MockProviderCapabilities) ProviderCapability(ctx context.Context, cap *vagrant.ProviderCapability, args interface{}, m *vagrant.Machine) (result interface{}, err error) {
|
||||||
|
if args != nil {
|
||||||
|
arguments := args.([]interface{})
|
||||||
|
if arguments[0] == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
if len(arguments) > 0 {
|
||||||
|
result = []string{
|
||||||
|
cap.Name,
|
||||||
|
arguments[0].(string)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = []string{cap.Name}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockConfig struct {
|
||||||
|
Core
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockConfig) ConfigAttributes() (attrs []string, err error) {
|
||||||
|
attrs = []string{"fubar", "foobar"}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockConfig) ConfigLoad(ctx context.Context, data map[string]interface{}) (loaddata map[string]interface{}, err error) {
|
||||||
|
if data["pause"] == true {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
loaddata = map[string]interface{}{
|
||||||
|
"test_key": "test_val"}
|
||||||
|
if data["test_key"] != nil {
|
||||||
|
loaddata["sent_key"] = data["test_key"]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockConfig) ConfigValidate(ctx context.Context, data map[string]interface{}, m *vagrant.Machine) (errors []string, err error) {
|
||||||
|
errors = []string{"test error"}
|
||||||
|
if data["pause"] == true {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockConfig) ConfigFinalize(ctx context.Context, data map[string]interface{}) (finaldata map[string]interface{}, err error) {
|
||||||
|
finaldata = make(map[string]interface{})
|
||||||
|
for key, tval := range data {
|
||||||
|
val, ok := tval.(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
finaldata[key] = val + "-updated"
|
||||||
|
}
|
||||||
|
if data["pause"] == true {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockProvider struct {
|
||||||
|
Core
|
||||||
|
vagrant.NoConfig
|
||||||
|
vagrant.NoGuestCapabilities
|
||||||
|
vagrant.NoHostCapabilities
|
||||||
|
vagrant.NoProviderCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockProvider) Action(ctx context.Context, actionName string, m *vagrant.Machine) (actions []string, err error) {
|
||||||
|
switch actionName {
|
||||||
|
case "valid":
|
||||||
|
actions = []string{"self::DoTask"}
|
||||||
|
case "pause":
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
default:
|
||||||
|
err = errors.New("Unknown action requested")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockProvider) IsInstalled(ctx context.Context, m *vagrant.Machine) (bool, error) {
|
||||||
|
if m.Name == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockProvider) IsUsable(ctx context.Context, m *vagrant.Machine) (bool, error) {
|
||||||
|
if m.Name == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockProvider) MachineIdChanged(ctx context.Context, m *vagrant.Machine) error {
|
||||||
|
if m.Name == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockProvider) Name() string {
|
||||||
|
return "mock_provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockProvider) RunAction(ctx context.Context, actionName string, args interface{}, m *vagrant.Machine) (r interface{}, err error) {
|
||||||
|
switch actionName {
|
||||||
|
case "send_output":
|
||||||
|
m.UI.Say("test_output_p")
|
||||||
|
case "pause":
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
case "valid":
|
||||||
|
default:
|
||||||
|
return nil, errors.New("invalid action name")
|
||||||
|
}
|
||||||
|
var arguments []interface{}
|
||||||
|
if args != nil {
|
||||||
|
arguments = args.([]interface{})
|
||||||
|
} else {
|
||||||
|
arguments = []interface{}{"unset"}
|
||||||
|
}
|
||||||
|
r = []string{
|
||||||
|
actionName,
|
||||||
|
arguments[0].(string)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockProvider) SshInfo(ctx context.Context, m *vagrant.Machine) (*vagrant.SshInfo, error) {
|
||||||
|
if m.Name == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &vagrant.SshInfo{
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 2222}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockProvider) State(ctx context.Context, m *vagrant.Machine) (*vagrant.MachineState, error) {
|
||||||
|
if m.Name == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &vagrant.MachineState{
|
||||||
|
Id: "default",
|
||||||
|
ShortDesc: "running"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockProvider) Info() *vagrant.ProviderInfo {
|
||||||
|
return &vagrant.ProviderInfo{
|
||||||
|
Description: "Custom",
|
||||||
|
Priority: 10}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockSyncedFolder struct {
|
||||||
|
Core
|
||||||
|
vagrant.NoGuestCapabilities
|
||||||
|
vagrant.NoHostCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MockSyncedFolder) Cleanup(ctx context.Context, m *vagrant.Machine, opts vagrant.FolderOptions) error {
|
||||||
|
if m.Name == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts != nil {
|
||||||
|
err, _ := opts["error"].(bool)
|
||||||
|
ui, _ := opts["ui"].(bool)
|
||||||
|
if err {
|
||||||
|
return errors.New("cleanup error")
|
||||||
|
}
|
||||||
|
if ui {
|
||||||
|
m.UI.Say("test_output_sf")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MockSyncedFolder) Disable(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, opts vagrant.FolderOptions) error {
|
||||||
|
if m.Name == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts != nil && opts["error"].(bool) {
|
||||||
|
return errors.New("disable error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MockSyncedFolder) Enable(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, opts vagrant.FolderOptions) error {
|
||||||
|
if m.Name == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts != nil && opts["error"].(bool) {
|
||||||
|
return errors.New("enable error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MockSyncedFolder) Info() *vagrant.SyncedFolderInfo {
|
||||||
|
return &vagrant.SyncedFolderInfo{
|
||||||
|
Description: "mock_folder",
|
||||||
|
Priority: 100}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MockSyncedFolder) IsUsable(ctx context.Context, m *vagrant.Machine) (bool, error) {
|
||||||
|
if m.Name == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MockSyncedFolder) Name() string {
|
||||||
|
return "mock_folder"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MockSyncedFolder) Prepare(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, opts vagrant.FolderOptions) error {
|
||||||
|
if m.Name == "pause" {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts != nil && opts["error"].(bool) {
|
||||||
|
return errors.New("prepare error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
echo -n "Parsing proto files and generating go output... "
|
||||||
|
|
||||||
|
for i in *
|
||||||
|
do
|
||||||
|
if [ -d "${i}" ]; then
|
||||||
|
protoc --proto_path=`go env GOPATH`/src --proto_path=. --go_out=plugins=grpc:. "${i}"/*.proto;
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
protoc --proto_path=`go env GOPATH`/src --proto_path=. --go_out=plugins=grpc:. *.proto;
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "done!"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,196 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package vagrant.proto;
|
||||||
|
|
||||||
|
message Empty{}
|
||||||
|
|
||||||
|
message Machine {
|
||||||
|
string machine = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Valid {
|
||||||
|
bool result = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Identifier {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PluginInfo {
|
||||||
|
string description = 1;
|
||||||
|
int64 priority = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Content {
|
||||||
|
string target = 1;
|
||||||
|
string value = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WriteResponse {
|
||||||
|
int32 length = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service IO {
|
||||||
|
rpc Read(Identifier) returns (Content);
|
||||||
|
rpc Write(Content) returns (WriteResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message SystemCapability {
|
||||||
|
string name = 1;
|
||||||
|
string platform = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ProviderCapability {
|
||||||
|
string name = 1;
|
||||||
|
string provider = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SystemCapabilityList {
|
||||||
|
repeated SystemCapability capabilities = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ProviderCapabilityList {
|
||||||
|
repeated ProviderCapability capabilities = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GenericResponse {
|
||||||
|
string result = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GuestCapabilityRequest {
|
||||||
|
SystemCapability capability = 1;
|
||||||
|
string machine = 2;
|
||||||
|
string arguments = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HostCapabilityRequest {
|
||||||
|
SystemCapability capability = 1;
|
||||||
|
string environment = 2;
|
||||||
|
string arguments = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ProviderCapabilityRequest {
|
||||||
|
ProviderCapability capability = 1;
|
||||||
|
string machine = 2;
|
||||||
|
string arguments = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
service GuestCapabilities {
|
||||||
|
rpc GuestCapabilities(Empty) returns (SystemCapabilityList);
|
||||||
|
rpc GuestCapability(GuestCapabilityRequest) returns (GenericResponse);
|
||||||
|
// IO helpers for streaming (copied from Stream service)
|
||||||
|
rpc Read(Identifier) returns (Content);
|
||||||
|
rpc Write(Content) returns (WriteResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
service HostCapabilities {
|
||||||
|
rpc HostCapabilities(Empty) returns (SystemCapabilityList);
|
||||||
|
rpc HostCapability(HostCapabilityRequest) returns (GenericResponse);
|
||||||
|
// IO helpers for streaming (copied from Stream service)
|
||||||
|
rpc Read(Identifier) returns (Content);
|
||||||
|
rpc Write(Content) returns (WriteResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
service ProviderCapabilities {
|
||||||
|
rpc ProviderCapabilities (Empty) returns (ProviderCapabilityList);
|
||||||
|
rpc ProviderCapability (ProviderCapabilityRequest) returns (GenericResponse);
|
||||||
|
// IO helpers for streaming (copied from Stream service)
|
||||||
|
rpc Read(Identifier) returns (Content);
|
||||||
|
rpc Write(Content) returns (WriteResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message Configuration {
|
||||||
|
string data = 1;
|
||||||
|
string machine = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListResponse {
|
||||||
|
repeated string items = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service Config {
|
||||||
|
rpc ConfigAttributes(Empty) returns (ListResponse);
|
||||||
|
rpc ConfigLoad(Configuration) returns (Configuration);
|
||||||
|
rpc ConfigValidate(Configuration) returns (ListResponse);
|
||||||
|
rpc ConfigFinalize(Configuration) returns (Configuration);
|
||||||
|
// IO helpers for streaming (copied from Stream service)
|
||||||
|
rpc Read(Identifier) returns (Content);
|
||||||
|
rpc Write(Content) returns (WriteResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message SyncedFolders {
|
||||||
|
string machine = 1;
|
||||||
|
string folders = 2;
|
||||||
|
string options = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
service SyncedFolder {
|
||||||
|
rpc Cleanup(SyncedFolders) returns (Empty);
|
||||||
|
rpc Disable(SyncedFolders) returns (Empty);
|
||||||
|
rpc Enable(SyncedFolders) returns (Empty);
|
||||||
|
rpc Info(Empty) returns (PluginInfo);
|
||||||
|
rpc IsUsable(Machine) returns (Valid);
|
||||||
|
rpc Name(Empty) returns (Identifier);
|
||||||
|
rpc Prepare(SyncedFolders) returns (Empty);
|
||||||
|
// IO helpers for streaming (copied from Stream service)
|
||||||
|
rpc Read(Identifier) returns (Content);
|
||||||
|
rpc Write(Content) returns (WriteResponse);
|
||||||
|
// Guest capabilities helpers (copied from GuestCapabilities service)
|
||||||
|
rpc GuestCapabilities(Empty) returns (SystemCapabilityList);
|
||||||
|
rpc GuestCapability(GuestCapabilityRequest) returns (GenericResponse);
|
||||||
|
// Host capabilities helpers (copied from GuestCapabilities service)
|
||||||
|
rpc HostCapabilities(Empty) returns (SystemCapabilityList);
|
||||||
|
rpc HostCapability(HostCapabilityRequest) returns (GenericResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message GenericAction {
|
||||||
|
string name = 1;
|
||||||
|
string machine = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ExecuteAction {
|
||||||
|
string name = 1;
|
||||||
|
string data = 2;
|
||||||
|
string machine = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineSshInfo {
|
||||||
|
string host = 1;
|
||||||
|
int64 port = 2;
|
||||||
|
string private_key_path = 3;
|
||||||
|
string username = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MachineState {
|
||||||
|
string id = 1;
|
||||||
|
string short_description = 2;
|
||||||
|
string long_description = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
service Provider {
|
||||||
|
rpc Action(GenericAction) returns (ListResponse);
|
||||||
|
rpc Info(Empty) returns (PluginInfo);
|
||||||
|
rpc IsInstalled(Machine) returns (Valid);
|
||||||
|
rpc IsUsable(Machine) returns (Valid);
|
||||||
|
rpc MachineIdChanged(Machine) returns (Machine);
|
||||||
|
rpc Name(Empty) returns (Identifier);
|
||||||
|
rpc RunAction(ExecuteAction) returns (GenericResponse);
|
||||||
|
rpc SshInfo(Machine) returns (MachineSshInfo);
|
||||||
|
rpc State(Machine) returns (MachineState);
|
||||||
|
// IO helpers for streaming (copied from Stream service)
|
||||||
|
rpc Read(Identifier) returns (Content);
|
||||||
|
rpc Write(Content) returns (WriteResponse);
|
||||||
|
// Config helpers (copied from Config service)
|
||||||
|
rpc ConfigAttributes(Empty) returns (ListResponse);
|
||||||
|
rpc ConfigLoad(Configuration) returns (Configuration);
|
||||||
|
rpc ConfigValidate(Configuration) returns (ListResponse);
|
||||||
|
rpc ConfigFinalize(Configuration) returns (Configuration);
|
||||||
|
// Guest capabilities helpers (copied from GuestCapabilities service)
|
||||||
|
rpc GuestCapabilities(Empty) returns (SystemCapabilityList);
|
||||||
|
rpc GuestCapability(GuestCapabilityRequest) returns (GenericResponse);
|
||||||
|
// Host capabilities helpers (copied from HostCapabilities service)
|
||||||
|
rpc HostCapabilities(Empty) returns (SystemCapabilityList);
|
||||||
|
rpc HostCapability(HostCapabilityRequest) returns (GenericResponse);
|
||||||
|
// Provider capabilities helpers (copied from ProviderCapabilities service)
|
||||||
|
rpc ProviderCapabilities (Empty) returns (ProviderCapabilityList);
|
||||||
|
rpc ProviderCapability (ProviderCapabilityRequest) returns (GenericResponse);
|
||||||
|
}
|
|
@ -0,0 +1,346 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
go_plugin "github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
|
||||||
|
|
||||||
|
"github.com/LK4D4/joincontext"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
vagrant.Provider
|
||||||
|
Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderPlugin struct {
|
||||||
|
go_plugin.NetRPCUnsupportedPlugin
|
||||||
|
Impl Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCProviderClient struct {
|
||||||
|
GRPCCoreClient
|
||||||
|
GRPCConfigClient
|
||||||
|
GRPCGuestCapabilitiesClient
|
||||||
|
GRPCHostCapabilitiesClient
|
||||||
|
GRPCProviderCapabilitiesClient
|
||||||
|
GRPCIOClient
|
||||||
|
client vagrant_proto.ProviderClient
|
||||||
|
doneCtx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCProviderClient) Action(ctx context.Context, actionName string, m *vagrant.Machine) (r []string, err error) {
|
||||||
|
machData, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.Action(jctx, &vagrant_proto.GenericAction{
|
||||||
|
Name: actionName,
|
||||||
|
Machine: machData})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
r = resp.Items
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCProviderClient) Info() *vagrant.ProviderInfo {
|
||||||
|
ctx := context.Background()
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.Info(jctx, &vagrant_proto.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return &vagrant.ProviderInfo{}
|
||||||
|
}
|
||||||
|
return &vagrant.ProviderInfo{
|
||||||
|
Description: resp.Description,
|
||||||
|
Priority: resp.Priority}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCProviderClient) IsInstalled(ctx context.Context, m *vagrant.Machine) (r bool, err error) {
|
||||||
|
machData, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.IsInstalled(jctx, &vagrant_proto.Machine{
|
||||||
|
Machine: machData})
|
||||||
|
if err != nil {
|
||||||
|
return false, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
r = resp.Result
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCProviderClient) IsUsable(ctx context.Context, m *vagrant.Machine) (r bool, err error) {
|
||||||
|
machData, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.IsUsable(jctx, &vagrant_proto.Machine{
|
||||||
|
Machine: machData})
|
||||||
|
if err != nil {
|
||||||
|
return false, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
r = resp.Result
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCProviderClient) MachineIdChanged(ctx context.Context, m *vagrant.Machine) (err error) {
|
||||||
|
machData, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
_, err = c.client.MachineIdChanged(jctx, &vagrant_proto.Machine{
|
||||||
|
Machine: machData})
|
||||||
|
if err != nil {
|
||||||
|
return handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCProviderClient) RunAction(ctx context.Context, actName string, args interface{}, m *vagrant.Machine) (r interface{}, err error) {
|
||||||
|
machData, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runData, err := json.Marshal(args)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.RunAction(jctx, &vagrant_proto.ExecuteAction{
|
||||||
|
Name: actName,
|
||||||
|
Data: string(runData),
|
||||||
|
Machine: machData})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp.Result), &r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCProviderClient) SshInfo(ctx context.Context, m *vagrant.Machine) (r *vagrant.SshInfo, err error) {
|
||||||
|
machData, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.SshInfo(jctx, &vagrant_proto.Machine{
|
||||||
|
Machine: machData})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
r = &vagrant.SshInfo{
|
||||||
|
Host: resp.Host,
|
||||||
|
Port: resp.Port,
|
||||||
|
PrivateKeyPath: resp.PrivateKeyPath,
|
||||||
|
Username: resp.Username}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCProviderClient) State(ctx context.Context, m *vagrant.Machine) (r *vagrant.MachineState, err error) {
|
||||||
|
machData, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.State(jctx, &vagrant_proto.Machine{
|
||||||
|
Machine: machData})
|
||||||
|
if err != nil {
|
||||||
|
return nil, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
r = &vagrant.MachineState{
|
||||||
|
Id: resp.Id,
|
||||||
|
ShortDesc: resp.ShortDescription,
|
||||||
|
LongDesc: resp.LongDescription}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCProviderClient) Name() string {
|
||||||
|
ctx := context.Background()
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.Name(jctx, &vagrant_proto.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return resp.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProviderPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||||
|
client := vagrant_proto.NewProviderClient(c)
|
||||||
|
return &GRPCProviderClient{
|
||||||
|
GRPCConfigClient: GRPCConfigClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx},
|
||||||
|
GRPCGuestCapabilitiesClient: GRPCGuestCapabilitiesClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx},
|
||||||
|
GRPCHostCapabilitiesClient: GRPCHostCapabilitiesClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx},
|
||||||
|
GRPCProviderCapabilitiesClient: GRPCProviderCapabilitiesClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx},
|
||||||
|
GRPCIOClient: GRPCIOClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx},
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProviderPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
|
||||||
|
p.Impl.Init()
|
||||||
|
vagrant_proto.RegisterProviderServer(s, &GRPCProviderServer{
|
||||||
|
Impl: p.Impl,
|
||||||
|
GRPCConfigServer: GRPCConfigServer{
|
||||||
|
Impl: p.Impl},
|
||||||
|
GRPCGuestCapabilitiesServer: GRPCGuestCapabilitiesServer{
|
||||||
|
Impl: p.Impl},
|
||||||
|
GRPCHostCapabilitiesServer: GRPCHostCapabilitiesServer{
|
||||||
|
Impl: p.Impl},
|
||||||
|
GRPCProviderCapabilitiesServer: GRPCProviderCapabilitiesServer{
|
||||||
|
Impl: p.Impl},
|
||||||
|
GRPCIOServer: GRPCIOServer{
|
||||||
|
Impl: p.Impl}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCProviderServer struct {
|
||||||
|
GRPCIOServer
|
||||||
|
GRPCConfigServer
|
||||||
|
GRPCGuestCapabilitiesServer
|
||||||
|
GRPCHostCapabilitiesServer
|
||||||
|
GRPCProviderCapabilitiesServer
|
||||||
|
Impl Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCProviderServer) Action(ctx context.Context, req *vagrant_proto.GenericAction) (resp *vagrant_proto.ListResponse, err error) {
|
||||||
|
resp = &vagrant_proto.ListResponse{}
|
||||||
|
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := s.Impl.Action(ctx, req.Name, m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Items = r
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCProviderServer) RunAction(ctx context.Context, req *vagrant_proto.ExecuteAction) (resp *vagrant_proto.GenericResponse, err error) {
|
||||||
|
resp = &vagrant_proto.GenericResponse{}
|
||||||
|
var args interface{}
|
||||||
|
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal([]byte(req.Data), &args); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := s.Impl.RunAction(ctx, req.Name, args, m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Result = string(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCProviderServer) Info(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.PluginInfo, err error) {
|
||||||
|
resp = &vagrant_proto.PluginInfo{}
|
||||||
|
r := s.Impl.Info()
|
||||||
|
resp.Description = r.Description
|
||||||
|
resp.Priority = r.Priority
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCProviderServer) IsInstalled(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.Valid, err error) {
|
||||||
|
resp = &vagrant_proto.Valid{}
|
||||||
|
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Result, err = s.Impl.IsInstalled(ctx, m)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCProviderServer) IsUsable(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.Valid, err error) {
|
||||||
|
resp = &vagrant_proto.Valid{}
|
||||||
|
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Result, err = s.Impl.IsUsable(ctx, m)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCProviderServer) SshInfo(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.MachineSshInfo, err error) {
|
||||||
|
resp = &vagrant_proto.MachineSshInfo{}
|
||||||
|
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := s.Impl.SshInfo(ctx, m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Host = r.Host
|
||||||
|
resp.Port = r.Port
|
||||||
|
resp.Username = r.Username
|
||||||
|
resp.PrivateKeyPath = r.PrivateKeyPath
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCProviderServer) State(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.MachineState, err error) {
|
||||||
|
resp = &vagrant_proto.MachineState{}
|
||||||
|
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := s.Impl.State(ctx, m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Id = r.Id
|
||||||
|
resp.ShortDescription = r.ShortDesc
|
||||||
|
resp.LongDescription = r.LongDesc
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCProviderServer) MachineIdChanged(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.Machine, err error) {
|
||||||
|
resp = &vagrant_proto.Machine{}
|
||||||
|
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = s.Impl.MachineIdChanged(ctx, m); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mdata, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp = &vagrant_proto.Machine{Machine: mdata}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCProviderServer) Name(ctx context.Context, req *vagrant_proto.Empty) (*vagrant_proto.Identifier, error) {
|
||||||
|
return &vagrant_proto.Identifier{Name: s.Impl.Name()}, nil
|
||||||
|
}
|
|
@ -0,0 +1,667 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProvider_Action(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := impl.Action(context.Background(), "valid", &vagrant.Machine{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
if resp[0] != "self::DoTask" {
|
||||||
|
t.Errorf("%s != self::DoTask", resp[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_Action_invalid(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = impl.Action(context.Background(), "invalid", &vagrant.Machine{})
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("illegal action")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_Action_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.Action(ctx, "pause", &vagrant.Machine{})
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_Action_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
_, err = impl.Action(ctx, "pause", &vagrant.Machine{})
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_IsInstalled(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
installed, err := impl.IsInstalled(context.Background(), &vagrant.Machine{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
if !installed {
|
||||||
|
t.Errorf("bad result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_IsInstalled_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.IsInstalled(ctx, &vagrant.Machine{Name: "pause"})
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_IsInstalled_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
_, err = impl.IsInstalled(ctx, &vagrant.Machine{Name: "pause"})
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestProvider_IsUsable(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
usable, err := impl.IsUsable(context.Background(), &vagrant.Machine{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
if !usable {
|
||||||
|
t.Errorf("bad result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_IsUsable_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.IsUsable(ctx, &vagrant.Machine{Name: "pause"})
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_IsUsable_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
_, err = impl.IsUsable(ctx, &vagrant.Machine{Name: "pause"})
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestProvider_MachineIdChanged(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = impl.MachineIdChanged(context.Background(), &vagrant.Machine{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_MachineIdChanged_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
err = impl.MachineIdChanged(ctx, &vagrant.Machine{Name: "pause"})
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_MachineIdChanged_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
err = impl.MachineIdChanged(ctx, &vagrant.Machine{Name: "pause"})
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_Name(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := impl.Name()
|
||||||
|
if resp != "mock_provider" {
|
||||||
|
t.Errorf("%s != mock_provider", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_RunAction(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"test_arg", "other_arg"}
|
||||||
|
m := &vagrant.Machine{}
|
||||||
|
|
||||||
|
resp, err := impl.RunAction(context.Background(), "valid", args, m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := resp.([]interface{})
|
||||||
|
if result[0] != "valid" {
|
||||||
|
t.Errorf("%s != valid", result[0])
|
||||||
|
}
|
||||||
|
if result[1] != "test_arg" {
|
||||||
|
t.Errorf("%s != test_arg", result[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_RunAction_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"test_arg", "other_arg"}
|
||||||
|
m := &vagrant.Machine{}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.RunAction(ctx, "pause", args, m)
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_RunAction_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"test_arg", "other_arg"}
|
||||||
|
m := &vagrant.Machine{}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
_, err = impl.RunAction(ctx, "pause", args, m)
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_RunAction_invalid(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"test_arg", "other_arg"}
|
||||||
|
m := &vagrant.Machine{}
|
||||||
|
|
||||||
|
_, err = impl.RunAction(context.Background(), "invalid", args, m)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("illegal action run")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_SshInfo(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := impl.SshInfo(context.Background(), &vagrant.Machine{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("invalid resp: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Host != "localhost" {
|
||||||
|
t.Errorf("%s != localhost", resp.Host)
|
||||||
|
}
|
||||||
|
if resp.Port != 2222 {
|
||||||
|
t.Errorf("%d != 2222", resp.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_SshInfo_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.SshInfo(ctx, &vagrant.Machine{Name: "pause"})
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("invalid resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_SshInfo_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
_, err = impl.SshInfo(ctx, &vagrant.Machine{Name: "pause"})
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("invalid resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_State(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := impl.State(context.Background(), &vagrant.Machine{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("invalid resp: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Id != "default" {
|
||||||
|
t.Errorf("%s != default", resp.Id)
|
||||||
|
}
|
||||||
|
if resp.ShortDesc != "running" {
|
||||||
|
t.Errorf("%s != running", resp.ShortDesc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_State_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.State(ctx, &vagrant.Machine{Name: "pause"})
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("invalid resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_State_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
_, err = impl.State(ctx, &vagrant.Machine{Name: "pause"})
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("invalid resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_Info(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := impl.Info()
|
||||||
|
|
||||||
|
if resp.Description != "Custom" {
|
||||||
|
t.Errorf("%s != Custom", resp.Description)
|
||||||
|
}
|
||||||
|
if resp.Priority != 10 {
|
||||||
|
t.Errorf("%d != 10", resp.Priority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_MachineUI_output(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("provider")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(Provider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
go func() {
|
||||||
|
_, err = impl.RunAction(ctx, "send_output", nil, &vagrant.Machine{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
resp, err := impl.Read("stdout")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(resp, "test_output_p") {
|
||||||
|
t.Errorf("%s !~ test_output_p", resp)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,283 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
go_plugin "github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
|
||||||
|
|
||||||
|
"github.com/LK4D4/joincontext"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SyncedFolder interface {
|
||||||
|
vagrant.SyncedFolder
|
||||||
|
Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncedFolderPlugin struct {
|
||||||
|
go_plugin.NetRPCUnsupportedPlugin
|
||||||
|
Impl SyncedFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCSyncedFolderClient struct {
|
||||||
|
GRPCCoreClient
|
||||||
|
GRPCGuestCapabilitiesClient
|
||||||
|
GRPCHostCapabilitiesClient
|
||||||
|
GRPCIOClient
|
||||||
|
client vagrant_proto.SyncedFolderClient
|
||||||
|
doneCtx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCSyncedFolderClient) Cleanup(ctx context.Context, m *vagrant.Machine, o vagrant.FolderOptions) (err error) {
|
||||||
|
machine, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts, err := json.Marshal(o)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
_, err = c.client.Cleanup(jctx, &vagrant_proto.SyncedFolders{
|
||||||
|
Machine: machine,
|
||||||
|
Options: string(opts)})
|
||||||
|
return handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCSyncedFolderClient) Disable(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, o vagrant.FolderOptions) (err error) {
|
||||||
|
machine, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
folders, err := json.Marshal(f)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts, err := json.Marshal(o)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
_, err = c.client.Disable(jctx, &vagrant_proto.SyncedFolders{
|
||||||
|
Machine: machine,
|
||||||
|
Folders: string(folders),
|
||||||
|
Options: string(opts)})
|
||||||
|
return handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCSyncedFolderClient) Enable(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, o vagrant.FolderOptions) (err error) {
|
||||||
|
machine, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
folders, err := json.Marshal(f)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts, err := json.Marshal(o)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
_, err = c.client.Enable(jctx, &vagrant_proto.SyncedFolders{
|
||||||
|
Machine: machine,
|
||||||
|
Folders: string(folders),
|
||||||
|
Options: string(opts)})
|
||||||
|
return handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCSyncedFolderClient) Info() *vagrant.SyncedFolderInfo {
|
||||||
|
resp, err := c.client.Info(context.Background(), &vagrant_proto.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return &vagrant.SyncedFolderInfo{}
|
||||||
|
}
|
||||||
|
return &vagrant.SyncedFolderInfo{
|
||||||
|
Description: resp.Description,
|
||||||
|
Priority: resp.Priority}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCSyncedFolderClient) IsUsable(ctx context.Context, m *vagrant.Machine) (u bool, err error) {
|
||||||
|
machine, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
resp, err := c.client.IsUsable(jctx, &vagrant_proto.Machine{
|
||||||
|
Machine: machine})
|
||||||
|
if err != nil {
|
||||||
|
return false, handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
u = resp.Result
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCSyncedFolderClient) Name() string {
|
||||||
|
resp, err := c.client.Name(context.Background(), &vagrant_proto.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return resp.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCSyncedFolderClient) Prepare(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, o vagrant.FolderOptions) (err error) {
|
||||||
|
machine, err := vagrant.DumpMachine(m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
folders, err := json.Marshal(f)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts, err := json.Marshal(o)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jctx, _ := joincontext.Join(ctx, c.doneCtx)
|
||||||
|
_, err = c.client.Prepare(jctx, &vagrant_proto.SyncedFolders{
|
||||||
|
Machine: machine,
|
||||||
|
Folders: string(folders),
|
||||||
|
Options: string(opts)})
|
||||||
|
return handleGrpcError(err, c.doneCtx, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCSyncedFolderServer struct {
|
||||||
|
GRPCGuestCapabilitiesServer
|
||||||
|
GRPCHostCapabilitiesServer
|
||||||
|
GRPCIOServer
|
||||||
|
Impl SyncedFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCSyncedFolderServer) Cleanup(ctx context.Context, req *vagrant_proto.SyncedFolders) (resp *vagrant_proto.Empty, err error) {
|
||||||
|
resp = &vagrant_proto.Empty{}
|
||||||
|
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var options vagrant.FolderOptions
|
||||||
|
err = json.Unmarshal([]byte(req.Options), &options)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = s.Impl.Cleanup(ctx, machine, options)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCSyncedFolderServer) Disable(ctx context.Context, req *vagrant_proto.SyncedFolders) (resp *vagrant_proto.Empty, err error) {
|
||||||
|
resp = &vagrant_proto.Empty{}
|
||||||
|
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var folders vagrant.FolderList
|
||||||
|
err = json.Unmarshal([]byte(req.Folders), &folders)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var options vagrant.FolderOptions
|
||||||
|
err = json.Unmarshal([]byte(req.Options), &options)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = s.Impl.Disable(ctx, machine, folders, options)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCSyncedFolderServer) Enable(ctx context.Context, req *vagrant_proto.SyncedFolders) (resp *vagrant_proto.Empty, err error) {
|
||||||
|
resp = &vagrant_proto.Empty{}
|
||||||
|
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var folders vagrant.FolderList
|
||||||
|
err = json.Unmarshal([]byte(req.Folders), &folders)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var options vagrant.FolderOptions
|
||||||
|
err = json.Unmarshal([]byte(req.Options), &options)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = s.Impl.Enable(ctx, machine, folders, options)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCSyncedFolderServer) Info(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.PluginInfo, err error) {
|
||||||
|
resp = &vagrant_proto.PluginInfo{}
|
||||||
|
r := s.Impl.Info()
|
||||||
|
resp.Description = r.Description
|
||||||
|
resp.Priority = r.Priority
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCSyncedFolderServer) IsUsable(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.Valid, err error) {
|
||||||
|
resp = &vagrant_proto.Valid{}
|
||||||
|
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := s.Impl.IsUsable(ctx, machine)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Result = r
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCSyncedFolderServer) Name(_ context.Context, req *vagrant_proto.Empty) (*vagrant_proto.Identifier, error) {
|
||||||
|
return &vagrant_proto.Identifier{Name: s.Impl.Name()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GRPCSyncedFolderServer) Prepare(ctx context.Context, req *vagrant_proto.SyncedFolders) (resp *vagrant_proto.Empty, err error) {
|
||||||
|
resp = &vagrant_proto.Empty{}
|
||||||
|
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var folders vagrant.FolderList
|
||||||
|
err = json.Unmarshal([]byte(req.Folders), &folders)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var options vagrant.FolderOptions
|
||||||
|
err = json.Unmarshal([]byte(req.Options), &options)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = s.Impl.Prepare(ctx, machine, folders, options)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *SyncedFolderPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
|
||||||
|
f.Impl.Init()
|
||||||
|
vagrant_proto.RegisterSyncedFolderServer(s,
|
||||||
|
&GRPCSyncedFolderServer{
|
||||||
|
Impl: f.Impl,
|
||||||
|
GRPCIOServer: GRPCIOServer{
|
||||||
|
Impl: f.Impl},
|
||||||
|
GRPCGuestCapabilitiesServer: GRPCGuestCapabilitiesServer{
|
||||||
|
Impl: f.Impl},
|
||||||
|
GRPCHostCapabilitiesServer: GRPCHostCapabilitiesServer{
|
||||||
|
Impl: f.Impl}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *SyncedFolderPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||||
|
client := vagrant_proto.NewSyncedFolderClient(c)
|
||||||
|
return &GRPCSyncedFolderClient{
|
||||||
|
GRPCIOClient: GRPCIOClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx},
|
||||||
|
GRPCGuestCapabilitiesClient: GRPCGuestCapabilitiesClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx},
|
||||||
|
GRPCHostCapabilitiesClient: GRPCHostCapabilitiesClient{
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx},
|
||||||
|
client: client,
|
||||||
|
doneCtx: ctx}, nil
|
||||||
|
}
|
|
@ -0,0 +1,562 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-plugin"
|
||||||
|
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSyncedFolder_Cleanup(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = impl.Cleanup(context.Background(), &vagrant.Machine{}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %#v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Cleanup_error(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := map[string]interface{}{
|
||||||
|
"error": true}
|
||||||
|
|
||||||
|
err = impl.Cleanup(context.Background(), &vagrant.Machine{}, args)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("illegal cleanup")
|
||||||
|
}
|
||||||
|
if err.Error() != "cleanup error" {
|
||||||
|
t.Errorf("%s != cleanup error", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Cleanup_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
err = impl.Cleanup(ctx, &vagrant.Machine{Name: "pause"}, nil)
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Cleanup_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
err = impl.Cleanup(ctx, &vagrant.Machine{Name: "pause"}, nil)
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Disable(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = impl.Disable(context.Background(), &vagrant.Machine{}, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Disable_error(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
folders := map[string]interface{}{
|
||||||
|
"folder_name": "options"}
|
||||||
|
args := map[string]interface{}{
|
||||||
|
"error": true}
|
||||||
|
|
||||||
|
err = impl.Disable(context.Background(), &vagrant.Machine{}, folders, args)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("illegal disable")
|
||||||
|
}
|
||||||
|
if err.Error() != "disable error" {
|
||||||
|
t.Errorf("%s != disable error", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Disable_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
err = impl.Disable(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Disable_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
err = impl.Disable(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Enable(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = impl.Enable(context.Background(), &vagrant.Machine{}, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Enable_error(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
folders := map[string]interface{}{
|
||||||
|
"folder_name": "options"}
|
||||||
|
args := map[string]interface{}{
|
||||||
|
"error": true}
|
||||||
|
|
||||||
|
err = impl.Enable(context.Background(), &vagrant.Machine{}, folders, args)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("illegal enable")
|
||||||
|
}
|
||||||
|
if err.Error() != "enable error" {
|
||||||
|
t.Errorf("%s != enable error", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Enable_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
err = impl.Enable(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Enable_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
err = impl.Enable(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Prepare(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = impl.Prepare(context.Background(), &vagrant.Machine{}, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Prepare_error(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
folders := map[string]interface{}{
|
||||||
|
"folder_name": "options"}
|
||||||
|
args := map[string]interface{}{
|
||||||
|
"error": true}
|
||||||
|
|
||||||
|
err = impl.Prepare(context.Background(), &vagrant.Machine{}, folders, args)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("illegal prepare")
|
||||||
|
}
|
||||||
|
if err.Error() != "prepare error" {
|
||||||
|
t.Errorf("%s != prepare error", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Prepare_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
err = impl.Prepare(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Prepare_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
err = impl.Prepare(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestSyncedFolder_Info(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := impl.Info()
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatalf("bad resp")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Description != "mock_folder" {
|
||||||
|
t.Errorf("%s != mock_folder", resp.Description)
|
||||||
|
}
|
||||||
|
if resp.Priority != 100 {
|
||||||
|
t.Errorf("%d != 100", resp.Priority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_IsUsable(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := impl.IsUsable(context.Background(), &vagrant.Machine{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
if !resp {
|
||||||
|
t.Errorf("bad result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_IsUsable_context_cancel(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
_, err = impl.IsUsable(ctx, &vagrant.Machine{Name: "pause"})
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_IsUsable_context_timeout(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
_, err = impl.IsUsable(ctx, &vagrant.Machine{Name: "pause"})
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_Name(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := impl.Name()
|
||||||
|
if resp != "mock_folder" {
|
||||||
|
t.Errorf("%s != mock_folder", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncedFolder_MachineUI_output(t *testing.T) {
|
||||||
|
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
|
||||||
|
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
|
||||||
|
defer server.Stop()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
raw, err := client.Dispense("folder")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
impl, ok := raw.(SyncedFolder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bad %#v", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := impl.Cleanup(context.Background(), &vagrant.Machine{}, map[string]interface{}{"ui": true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
resp, err := impl.Read("stdout")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad resp: %s", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(resp, "test_output") {
|
||||||
|
t.Errorf("%s !~ test_output", resp)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
Info() *ProviderInfo
|
||||||
|
Action(ctx context.Context, actionName string, machData *Machine) ([]string, error)
|
||||||
|
IsInstalled(ctx context.Context, machData *Machine) (bool, error)
|
||||||
|
IsUsable(ctx context.Context, machData *Machine) (bool, error)
|
||||||
|
MachineIdChanged(ctx context.Context, machData *Machine) error
|
||||||
|
Name() string
|
||||||
|
RunAction(ctx context.Context, actionName string, args interface{}, machData *Machine) (interface{}, error)
|
||||||
|
SshInfo(ctx context.Context, machData *Machine) (*SshInfo, error)
|
||||||
|
State(ctx context.Context, machData *Machine) (*MachineState, error)
|
||||||
|
|
||||||
|
Config
|
||||||
|
GuestCapabilities
|
||||||
|
HostCapabilities
|
||||||
|
ProviderCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderInfo struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
Priority int64 `json:"priority"`
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
type SshInfo struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int64 `json:"port"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
PrivateKeyPath string `json:"private_key_path"`
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FolderList map[string]interface{}
|
||||||
|
type FolderOptions map[string]interface{}
|
||||||
|
|
||||||
|
type SyncedFolderInfo struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
Priority int64 `json:"priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncedFolder interface {
|
||||||
|
Cleanup(ctx context.Context, m *Machine, opts FolderOptions) error
|
||||||
|
Disable(ctx context.Context, m *Machine, f FolderList, opts FolderOptions) error
|
||||||
|
Enable(ctx context.Context, m *Machine, f FolderList, opts FolderOptions) error
|
||||||
|
Info() *SyncedFolderInfo
|
||||||
|
IsUsable(ctx context.Context, m *Machine) (bool, error)
|
||||||
|
Name() string
|
||||||
|
Prepare(ctx context.Context, m *Machine, f FolderList, opts FolderOptions) error
|
||||||
|
|
||||||
|
GuestCapabilities
|
||||||
|
HostCapabilities
|
||||||
|
}
|
|
@ -0,0 +1,472 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UiColor uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
UiColorRed UiColor = 31
|
||||||
|
UiColorGreen = 32
|
||||||
|
UiColorYellow = 33
|
||||||
|
UiColorBlue = 34
|
||||||
|
UiColorMagenta = 35
|
||||||
|
UiColorCyan = 36
|
||||||
|
)
|
||||||
|
|
||||||
|
type UiChannel uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
UiOutput UiChannel = 1
|
||||||
|
UiError = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = DefaultLogger().Named("ui")
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Channel UiChannel
|
||||||
|
NewLine bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultOptions = &Options{
|
||||||
|
Channel: UiOutput,
|
||||||
|
NewLine: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Ui interface handles all communication for Vagrant with the outside
|
||||||
|
// world. This sort of control allows us to strictly control how output
|
||||||
|
// is formatted and various levels of output.
|
||||||
|
type Ui interface {
|
||||||
|
Ask(string) (string, error)
|
||||||
|
Detail(string)
|
||||||
|
Info(string)
|
||||||
|
Error(string)
|
||||||
|
Machine(string, ...string)
|
||||||
|
Message(string, *Options)
|
||||||
|
Output(string)
|
||||||
|
Say(string)
|
||||||
|
Success(string)
|
||||||
|
Warn(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The BasicUI is a UI that reads and writes from a standard Go reader
|
||||||
|
// and writer. It is safe to be called from multiple goroutines. Machine
|
||||||
|
// readable output is simply logged for this UI.
|
||||||
|
type BasicUi struct {
|
||||||
|
Reader io.Reader
|
||||||
|
Writer io.Writer
|
||||||
|
ErrorWriter io.Writer
|
||||||
|
l sync.Mutex
|
||||||
|
interrupted bool
|
||||||
|
scanner *bufio.Scanner
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Ui = new(BasicUi)
|
||||||
|
|
||||||
|
func (rw *BasicUi) Ask(query string) (string, error) {
|
||||||
|
rw.l.Lock()
|
||||||
|
defer rw.l.Unlock()
|
||||||
|
|
||||||
|
if rw.interrupted {
|
||||||
|
return "", errors.New("interrupted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rw.scanner == nil {
|
||||||
|
rw.scanner = bufio.NewScanner(rw.Reader)
|
||||||
|
}
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer signal.Stop(sigCh)
|
||||||
|
|
||||||
|
logger.Info("ask", query)
|
||||||
|
if query != "" {
|
||||||
|
if _, err := fmt.Fprint(rw.Writer, query+" "); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(chan string, 1)
|
||||||
|
go func() {
|
||||||
|
var line string
|
||||||
|
if rw.scanner.Scan() {
|
||||||
|
line = rw.scanner.Text()
|
||||||
|
}
|
||||||
|
if err := rw.scanner.Err(); err != nil {
|
||||||
|
logger.Error("scan failure", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result <- line
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case line := <-result:
|
||||||
|
return line, nil
|
||||||
|
case <-sigCh:
|
||||||
|
// Print a newline so that any further output starts properly
|
||||||
|
// on a new line.
|
||||||
|
fmt.Fprintln(rw.Writer)
|
||||||
|
|
||||||
|
// Mark that we were interrupted so future Ask calls fail.
|
||||||
|
rw.interrupted = true
|
||||||
|
|
||||||
|
return "", errors.New("interrupted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *BasicUi) Detail(message string) { rw.Say(message) }
|
||||||
|
func (rw *BasicUi) Info(message string) { rw.Say(message) }
|
||||||
|
func (rw *BasicUi) Output(message string) { rw.Say(message) }
|
||||||
|
func (rw *BasicUi) Success(message string) { rw.Say(message) }
|
||||||
|
func (rw *BasicUi) Warn(message string) { rw.Say(message) }
|
||||||
|
|
||||||
|
func (rw *BasicUi) Say(message string) {
|
||||||
|
rw.Message(message, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *BasicUi) Message(message string, opts *Options) {
|
||||||
|
rw.l.Lock()
|
||||||
|
defer rw.l.Unlock()
|
||||||
|
|
||||||
|
if opts == nil {
|
||||||
|
opts = &Options{Channel: UiOutput, NewLine: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("write message", "content", message, "options", opts)
|
||||||
|
target := rw.Writer
|
||||||
|
if opts.Channel == UiError {
|
||||||
|
if rw.ErrorWriter == nil {
|
||||||
|
logger.Error("error writer unset using writer")
|
||||||
|
} else {
|
||||||
|
target = rw.ErrorWriter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suffix := ""
|
||||||
|
if opts.NewLine {
|
||||||
|
suffix = "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := fmt.Fprint(target, message+suffix)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("write failure", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *BasicUi) Error(message string) {
|
||||||
|
rw.Message(message, &Options{Channel: UiError, NewLine: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *BasicUi) Machine(t string, args ...string) {
|
||||||
|
logger.Info("machine readable", "category", t, "args", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MachineReadableUi is a UI that only outputs machine-readable output
|
||||||
|
// to the given Writer.
|
||||||
|
type MachineReadableUi struct {
|
||||||
|
Writer io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Ui = new(MachineReadableUi)
|
||||||
|
|
||||||
|
func (u *MachineReadableUi) Ask(query string) (string, error) {
|
||||||
|
return "", errors.New("machine-readable UI can't ask")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MachineReadableUi) Detail(message string) {
|
||||||
|
u.Machine("ui", "detail", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MachineReadableUi) Info(message string) {
|
||||||
|
u.Machine("ui", "info", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MachineReadableUi) Output(message string) {
|
||||||
|
u.Machine("ui", "output", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MachineReadableUi) Success(message string) {
|
||||||
|
u.Machine("ui", "success", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MachineReadableUi) Warn(message string) {
|
||||||
|
u.Machine("ui", "warn", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MachineReadableUi) Say(message string) {
|
||||||
|
u.Machine("ui", "say", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MachineReadableUi) Message(message string, opts *Options) {
|
||||||
|
u.Machine("ui", "message", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MachineReadableUi) Error(message string) {
|
||||||
|
u.Machine("ui", "error", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Do we want to update this to match Vagrant machine style?
|
||||||
|
func (u *MachineReadableUi) Machine(category string, args ...string) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
// Determine if we have a target, and set it
|
||||||
|
target := ""
|
||||||
|
commaIdx := strings.Index(category, ",")
|
||||||
|
if commaIdx > -1 {
|
||||||
|
target = category[0:commaIdx]
|
||||||
|
category = category[commaIdx+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the args
|
||||||
|
for i, v := range args {
|
||||||
|
args[i] = strings.Replace(v, ",", "%!(VAGRANT_COMMA)", -1)
|
||||||
|
args[i] = strings.Replace(args[i], "\r", "\\r", -1)
|
||||||
|
args[i] = strings.Replace(args[i], "\n", "\\n", -1)
|
||||||
|
}
|
||||||
|
argsString := strings.Join(args, ",")
|
||||||
|
|
||||||
|
_, err := fmt.Fprintf(u.Writer, "%d,%s,%s,%s\n", now.Unix(), target, category, argsString)
|
||||||
|
if err != nil {
|
||||||
|
if err == syscall.EPIPE || strings.Contains(err.Error(), "broken pipe") {
|
||||||
|
// Ignore epipe errors because that just means that the file
|
||||||
|
// is probably closed or going to /dev/null or something.
|
||||||
|
} else {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoopUi struct{}
|
||||||
|
|
||||||
|
var _ Ui = new(NoopUi)
|
||||||
|
|
||||||
|
func (*NoopUi) Ask(string) (string, error) { return "", errors.New("this is a noop ui") }
|
||||||
|
func (*NoopUi) Detail(string) { return }
|
||||||
|
func (*NoopUi) Info(string) { return }
|
||||||
|
func (*NoopUi) Error(string) { return }
|
||||||
|
func (*NoopUi) Machine(string, ...string) { return }
|
||||||
|
func (*NoopUi) Message(string, *Options) { return }
|
||||||
|
func (*NoopUi) Output(string) { return }
|
||||||
|
func (*NoopUi) Say(string) { return }
|
||||||
|
func (*NoopUi) Success(string) { return }
|
||||||
|
func (*NoopUi) Warn(string) { return }
|
||||||
|
|
||||||
|
// ColoredUi is a UI that is colored using terminal colors.
|
||||||
|
type ColoredUi struct {
|
||||||
|
Color UiColor
|
||||||
|
ErrorColor UiColor
|
||||||
|
SuccessColor UiColor
|
||||||
|
WarnColor UiColor
|
||||||
|
Ui Ui
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Ui = new(ColoredUi)
|
||||||
|
|
||||||
|
func (u *ColoredUi) Ask(query string) (string, error) {
|
||||||
|
return u.Ui.Ask(u.colorize(query, u.Color, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ColoredUi) Detail(message string) {
|
||||||
|
u.Say(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ColoredUi) Info(message string) {
|
||||||
|
u.Say(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ColoredUi) Error(message string) {
|
||||||
|
color := u.ErrorColor
|
||||||
|
if color == 0 {
|
||||||
|
color = UiColorRed
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Ui.Error(u.colorize(message, color, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ColoredUi) Machine(t string, args ...string) {
|
||||||
|
// Don't colorize machine-readable output
|
||||||
|
u.Ui.Machine(t, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ColoredUi) Message(message string, opts *Options) {
|
||||||
|
u.Ui.Message(u.colorize(message, u.Color, false), opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ColoredUi) Output(message string) {
|
||||||
|
u.Say(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ColoredUi) Say(message string) {
|
||||||
|
u.Ui.Say(u.colorize(message, u.Color, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ColoredUi) Success(message string) {
|
||||||
|
u.Ui.Say(u.colorize(message, u.SuccessColor, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ColoredUi) Warn(message string) {
|
||||||
|
u.Ui.Say(u.colorize(message, u.WarnColor, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ColoredUi) colorize(message string, color UiColor, bold bool) string {
|
||||||
|
if !u.supportsColors() {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
attr := 0
|
||||||
|
if bold {
|
||||||
|
attr = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("\033[%d;%dm%s\033[0m", attr, color, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ColoredUi) supportsColors() bool {
|
||||||
|
// Never use colors if we have this environmental variable
|
||||||
|
if os.Getenv("VAGRANT_NO_COLOR") != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, on non-Windows machine, just assume it does
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows, if we appear to be in Cygwin, then it does
|
||||||
|
cygwin := os.Getenv("CYGWIN") != "" ||
|
||||||
|
os.Getenv("OSTYPE") == "cygwin" ||
|
||||||
|
os.Getenv("TERM") == "cygwin"
|
||||||
|
|
||||||
|
return cygwin
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetedUi is a UI that wraps another UI implementation and modifies
|
||||||
|
// the output to indicate a specific target. Specifically, all Say output
|
||||||
|
// is prefixed with the target name. Message output is not prefixed but
|
||||||
|
// is offset by the length of the target so that output is lined up properly
|
||||||
|
// with Say output. Machine-readable output has the proper target set.
|
||||||
|
type TargetedUi struct {
|
||||||
|
Target string
|
||||||
|
Ui Ui
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Ui = new(TargetedUi)
|
||||||
|
|
||||||
|
func (u *TargetedUi) Ask(query string) (string, error) {
|
||||||
|
return u.Ui.Ask(u.prefixLines(true, query))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TargetedUi) Detail(message string) {
|
||||||
|
u.Ui.Detail(u.prefixLines(true, message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TargetedUi) Info(message string) {
|
||||||
|
u.Ui.Info(u.prefixLines(true, message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TargetedUi) Output(message string) {
|
||||||
|
u.Ui.Output(u.prefixLines(true, message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TargetedUi) Success(message string) {
|
||||||
|
u.Ui.Success(u.prefixLines(true, message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TargetedUi) Warn(message string) {
|
||||||
|
u.Ui.Warn(u.prefixLines(true, message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TargetedUi) Say(message string) {
|
||||||
|
u.Ui.Say(u.prefixLines(true, message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TargetedUi) Message(message string, opts *Options) {
|
||||||
|
u.Ui.Message(u.prefixLines(false, message), opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TargetedUi) Error(message string) {
|
||||||
|
u.Ui.Error(u.prefixLines(true, message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TargetedUi) Machine(t string, args ...string) {
|
||||||
|
// Prefix in the target, then pass through
|
||||||
|
u.Ui.Machine(fmt.Sprintf("%s,%s", u.Target, t), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TargetedUi) prefixLines(arrow bool, message string) string {
|
||||||
|
arrowText := "==>"
|
||||||
|
if !arrow {
|
||||||
|
arrowText = strings.Repeat(" ", len(arrowText))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result bytes.Buffer
|
||||||
|
|
||||||
|
for _, line := range strings.Split(message, "\n") {
|
||||||
|
result.WriteString(fmt.Sprintf("%s %s: %s\n", arrowText, u.Target, line))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimRightFunc(result.String(), unicode.IsSpace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimestampedUi is a UI that wraps another UI implementation and prefixes
|
||||||
|
// prefixes each message with an RFC3339 timestamp
|
||||||
|
type TimestampedUi struct {
|
||||||
|
Ui Ui
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Ui = new(TimestampedUi)
|
||||||
|
|
||||||
|
func (u *TimestampedUi) Ask(query string) (string, error) {
|
||||||
|
return u.Ui.Ask(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TimestampedUi) Detail(message string) {
|
||||||
|
u.Ui.Detail(u.timestampLine(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TimestampedUi) Info(message string) {
|
||||||
|
u.Ui.Info(u.timestampLine(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TimestampedUi) Output(message string) {
|
||||||
|
u.Ui.Output(u.timestampLine(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TimestampedUi) Success(message string) {
|
||||||
|
u.Ui.Success(u.timestampLine(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TimestampedUi) Warn(message string) {
|
||||||
|
u.Ui.Warn(u.timestampLine(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TimestampedUi) Say(message string) {
|
||||||
|
u.Ui.Say(u.timestampLine(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TimestampedUi) Message(message string, opts *Options) {
|
||||||
|
u.Ui.Message(u.timestampLine(message), opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TimestampedUi) Error(message string) {
|
||||||
|
u.Ui.Error(u.timestampLine(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TimestampedUi) Machine(message string, args ...string) {
|
||||||
|
u.Ui.Machine(message, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TimestampedUi) timestampLine(string string) string {
|
||||||
|
return fmt.Sprintf("%v: %v", time.Now().Format(time.RFC3339), string)
|
||||||
|
}
|
|
@ -0,0 +1,290 @@
|
||||||
|
package vagrant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This reads the output from the bytes.Buffer in our test object
|
||||||
|
// and then resets the buffer.
|
||||||
|
func readWriter(ui *BasicUi) (result string) {
|
||||||
|
buffer := ui.Writer.(*bytes.Buffer)
|
||||||
|
result = buffer.String()
|
||||||
|
buffer.Reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the input Reader then add some input to it.
|
||||||
|
func writeReader(ui *BasicUi, input string) {
|
||||||
|
buffer := ui.Reader.(*bytes.Buffer)
|
||||||
|
buffer.WriteString(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readErrorWriter(ui *BasicUi) (result string) {
|
||||||
|
buffer := ui.ErrorWriter.(*bytes.Buffer)
|
||||||
|
result = buffer.String()
|
||||||
|
buffer.Reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUi() *BasicUi {
|
||||||
|
return &BasicUi{
|
||||||
|
Reader: new(bytes.Buffer),
|
||||||
|
Writer: new(bytes.Buffer),
|
||||||
|
ErrorWriter: new(bytes.Buffer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColoredUi(t *testing.T) {
|
||||||
|
bufferUi := testUi()
|
||||||
|
ui := &ColoredUi{UiColorBlue, UiColorRed, UiColorGreen,
|
||||||
|
UiColorYellow, bufferUi}
|
||||||
|
|
||||||
|
if !ui.supportsColors() {
|
||||||
|
t.Skip("skipping for ui without color support")
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("foo")
|
||||||
|
result := readWriter(bufferUi)
|
||||||
|
if result != "\033[1;34mfoo\033[0m\n" {
|
||||||
|
t.Fatalf("invalid output: %s", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Message("foo", nil)
|
||||||
|
result = readWriter(bufferUi)
|
||||||
|
if result != "\033[0;34mfoo\033[0m\n" {
|
||||||
|
t.Fatalf("invalid output: %s", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Error("foo")
|
||||||
|
result = readWriter(bufferUi)
|
||||||
|
if result != "" {
|
||||||
|
t.Fatalf("invalid output: %s", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = readErrorWriter(bufferUi)
|
||||||
|
if result != "\033[1;31mfoo\033[0m\n" {
|
||||||
|
t.Fatalf("invalid output: %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColoredUi_noColorEnv(t *testing.T) {
|
||||||
|
bufferUi := testUi()
|
||||||
|
ui := &ColoredUi{UiColorBlue, UiColorRed, UiColorGreen,
|
||||||
|
UiColorYellow, bufferUi}
|
||||||
|
|
||||||
|
// Set the env var to get rid of the color
|
||||||
|
oldenv := os.Getenv("VAGRANT_NO_COLOR")
|
||||||
|
os.Setenv("VAGRANT_NO_COLOR", "1")
|
||||||
|
defer os.Setenv("VAGRANT_NO_COLOR", oldenv)
|
||||||
|
|
||||||
|
ui.Say("foo")
|
||||||
|
result := readWriter(bufferUi)
|
||||||
|
if result != "foo\n" {
|
||||||
|
t.Fatalf("invalid output: %s", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Message("foo", nil)
|
||||||
|
result = readWriter(bufferUi)
|
||||||
|
if result != "foo\n" {
|
||||||
|
t.Fatalf("invalid output: %s", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Error("foo")
|
||||||
|
result = readErrorWriter(bufferUi)
|
||||||
|
if result != "foo\n" {
|
||||||
|
t.Fatalf("invalid output: %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetedUi(t *testing.T) {
|
||||||
|
bufferUi := testUi()
|
||||||
|
targetedUi := &TargetedUi{
|
||||||
|
Target: "foo",
|
||||||
|
Ui: bufferUi,
|
||||||
|
}
|
||||||
|
|
||||||
|
var actual, expected string
|
||||||
|
targetedUi.Say("foo")
|
||||||
|
actual = readWriter(bufferUi)
|
||||||
|
expected = "==> foo: foo\n"
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetedUi.Message("foo", nil)
|
||||||
|
actual = readWriter(bufferUi)
|
||||||
|
expected = " foo: foo\n"
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetedUi.Error("bar")
|
||||||
|
actual = readErrorWriter(bufferUi)
|
||||||
|
expected = "==> foo: bar\n"
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetedUi.Say("foo\nbar")
|
||||||
|
actual = readWriter(bufferUi)
|
||||||
|
expected = "==> foo: foo\n==> foo: bar\n"
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColoredUi_ImplUi(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &ColoredUi{}
|
||||||
|
if _, ok := raw.(Ui); !ok {
|
||||||
|
t.Fatalf("ColoredUi must implement Ui")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetedUi_ImplUi(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &TargetedUi{}
|
||||||
|
if _, ok := raw.(Ui); !ok {
|
||||||
|
t.Fatalf("TargetedUi must implement Ui")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicUi_ImplUi(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &BasicUi{}
|
||||||
|
if _, ok := raw.(Ui); !ok {
|
||||||
|
t.Fatalf("BasicUi must implement Ui")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicUi_Error(t *testing.T) {
|
||||||
|
bufferUi := testUi()
|
||||||
|
|
||||||
|
var actual, expected string
|
||||||
|
bufferUi.Error("foo")
|
||||||
|
actual = readErrorWriter(bufferUi)
|
||||||
|
expected = "foo\n"
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferUi.ErrorWriter = nil
|
||||||
|
bufferUi.Error("5")
|
||||||
|
actual = readWriter(bufferUi)
|
||||||
|
expected = "5\n"
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicUi_Say(t *testing.T) {
|
||||||
|
bufferUi := testUi()
|
||||||
|
|
||||||
|
var actual, expected string
|
||||||
|
|
||||||
|
bufferUi.Say("foo")
|
||||||
|
actual = readWriter(bufferUi)
|
||||||
|
expected = "foo\n"
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferUi.Say("5")
|
||||||
|
actual = readWriter(bufferUi)
|
||||||
|
expected = "5\n"
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicUi_Ask(t *testing.T) {
|
||||||
|
|
||||||
|
var actual, expected string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var testCases = []struct {
|
||||||
|
Prompt, Input, Answer string
|
||||||
|
}{
|
||||||
|
{"[c]ontinue or [a]bort", "c\n", "c"},
|
||||||
|
{"[c]ontinue or [a]bort", "c", "c"},
|
||||||
|
// Empty input shouldn't give an error
|
||||||
|
{"Name", "Joe Bloggs\n", "Joe Bloggs"},
|
||||||
|
{"Name", "Joe Bloggs", "Joe Bloggs"},
|
||||||
|
{"Name", "\n", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
// Because of the internal bufio we can't easily reset the input, so create a new one each time
|
||||||
|
bufferUi := testUi()
|
||||||
|
writeReader(bufferUi, testCase.Input)
|
||||||
|
|
||||||
|
actual, err = bufferUi.Ask(testCase.Prompt)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actual != testCase.Answer {
|
||||||
|
t.Fatalf("bad answer: %#v", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual = readWriter(bufferUi)
|
||||||
|
expected = testCase.Prompt + " "
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad prompt: %#v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMachineReadableUi_ImplUi(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &MachineReadableUi{}
|
||||||
|
if _, ok := raw.(Ui); !ok {
|
||||||
|
t.Fatalf("MachineReadableUi must implement Ui")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMachineReadableUi(t *testing.T) {
|
||||||
|
var data, expected string
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
ui := &MachineReadableUi{Writer: buf}
|
||||||
|
|
||||||
|
// No target
|
||||||
|
ui.Machine("foo", "bar", "baz")
|
||||||
|
data = strings.SplitN(buf.String(), ",", 2)[1]
|
||||||
|
expected = ",foo,bar,baz\n"
|
||||||
|
if data != expected {
|
||||||
|
t.Fatalf("bad: %s", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target
|
||||||
|
buf.Reset()
|
||||||
|
ui.Machine("mitchellh,foo", "bar", "baz")
|
||||||
|
data = strings.SplitN(buf.String(), ",", 2)[1]
|
||||||
|
expected = "mitchellh,foo,bar,baz\n"
|
||||||
|
if data != expected {
|
||||||
|
t.Fatalf("bad: %s", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commas
|
||||||
|
buf.Reset()
|
||||||
|
ui.Machine("foo", "foo,bar")
|
||||||
|
data = strings.SplitN(buf.String(), ",", 2)[1]
|
||||||
|
expected = ",foo,foo%!(VAGRANT_COMMA)bar\n"
|
||||||
|
if data != expected {
|
||||||
|
t.Fatalf("bad: %s", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New lines
|
||||||
|
buf.Reset()
|
||||||
|
ui.Machine("foo", "foo\n")
|
||||||
|
data = strings.SplitN(buf.String(), ",", 2)[1]
|
||||||
|
expected = ",foo,foo\\n\n"
|
||||||
|
if data != expected {
|
||||||
|
t.Fatalf("bad: %#v", data)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
module github.com/hashicorp/vagrant
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/LK4D4/joincontext v0.0.0-20171026170139-1724345da6d5
|
||||||
|
github.com/dylanmei/iso8601 v0.1.0 // indirect
|
||||||
|
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1
|
||||||
|
github.com/golang/protobuf v1.3.0
|
||||||
|
github.com/hashicorp/go-hclog v0.8.0
|
||||||
|
github.com/hashicorp/go-plugin v0.0.0-20190220160451-3f118e8ee104
|
||||||
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
|
github.com/masterzen/winrm v0.0.0-20190308153735-1d17eaf15943
|
||||||
|
github.com/mitchellh/iochan v1.0.0
|
||||||
|
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||||
|
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db
|
||||||
|
github.com/pkg/errors v0.8.1 // indirect
|
||||||
|
github.com/pkg/sftp v1.10.0
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
|
||||||
|
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4
|
||||||
|
google.golang.org/grpc v1.19.0
|
||||||
|
)
|
|
@ -0,0 +1,91 @@
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4 h1:pSm8mp0T2OH2CPmPDPtwHPr3VAQaOwVF/JbllOPP4xA=
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022 h1:y8Gs8CzNfDF5AZvjr+5UyGQvQEBL7pwo+v+wX6q9JI8=
|
||||||
|
github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4=
|
||||||
|
github.com/LK4D4/joincontext v0.0.0-20171026170139-1724345da6d5 h1:U7q69tqXiCf6m097GRlNQB0/6SI1qWIOHYHhCEvDxF4=
|
||||||
|
github.com/LK4D4/joincontext v0.0.0-20171026170139-1724345da6d5/go.mod h1:nxQPcNPR/34g+HcK2hEsF99O+GJgIkW/OmPl8wtzhmk=
|
||||||
|
github.com/antchfx/xpath v0.0.0-20190129040759-c8489ed3251e h1:ptBAamGVd6CfRsUtyHD+goy2JGhv1QC32v3gqM8mYAM=
|
||||||
|
github.com/antchfx/xpath v0.0.0-20190129040759-c8489ed3251e/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||||
|
github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0 h1:JaCC8jz0zdMLk2m+qCCVLLLM/PL93p84w4pK3aJWj60=
|
||||||
|
github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0/go.mod h1:LzD22aAzDP8/dyiCKFp31He4m2GPjl0AFyzDtZzUu9M=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dylanmei/iso8601 v0.1.0 h1:812NGQDBcqquTfH5Yeo7lwR0nzx/cKdsmf3qMjPURUI=
|
||||||
|
github.com/dylanmei/iso8601 v0.1.0/go.mod h1:w9KhXSgIyROl1DefbMYIE7UVSIvELTbMrCfx+QkYnoQ=
|
||||||
|
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1 h1:r1oACdS2XYiAWcfF8BJXkoU8l1J71KehGR+d99yWEDA=
|
||||||
|
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1/go.mod h1:lcy9/2gH1jn/VCLouHA6tOEwLoNVd4GW6zhuKLmHC2Y=
|
||||||
|
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||||
|
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
|
||||||
|
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
||||||
|
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
|
||||||
|
github.com/hashicorp/go-hclog v0.8.0 h1:z3ollgGRg8RjfJH6UVBaG54R70GFd++QOkvnJH3VSBY=
|
||||||
|
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||||
|
github.com/hashicorp/go-plugin v0.0.0-20190220160451-3f118e8ee104 h1:9iQ/zrTOJqzP+kH37s6xNb6T1RysiT7fnDD3DJbspVw=
|
||||||
|
github.com/hashicorp/go-plugin v0.0.0-20190220160451-3f118e8ee104/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
|
||||||
|
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
|
||||||
|
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||||
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9 h1:SmVbOZFWAlyQshuMfOkiAx1f5oUTsOGG5IXplAEYeeM=
|
||||||
|
github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc=
|
||||||
|
github.com/masterzen/winrm v0.0.0-20190308153735-1d17eaf15943 h1:Bteu9XN1gkBePnKr0v1edkUo2LJRsmK5ne2FrC6yVW4=
|
||||||
|
github.com/masterzen/winrm v0.0.0-20190308153735-1d17eaf15943/go.mod h1:bsMsaiOA3CXjbJxW0a94G4PfPDj9zUmH5JoFuJ9P4o0=
|
||||||
|
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg=
|
||||||
|
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||||
|
github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
|
||||||
|
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||||
|
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||||
|
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||||
|
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||||
|
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||||
|
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db h1:9uViuKtx1jrlXLBW/pMnhOfzn3iSEdLase/But/IZRU=
|
||||||
|
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/sftp v1.10.0 h1:DGA1KlA9esU6WcicH+P8PxFZOl15O6GYtab1cIJdOlE=
|
||||||
|
github.com/pkg/sftp v1.10.0/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU=
|
||||||
|
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw=
|
||||||
|
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||||
|
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
@ -1,4 +1,5 @@
|
||||||
require "log4r"
|
require "log4r"
|
||||||
|
require "log4r/logger"
|
||||||
require "vagrant/util/credential_scrubber"
|
require "vagrant/util/credential_scrubber"
|
||||||
# Update the default formatter within the log4r library to ensure
|
# Update the default formatter within the log4r library to ensure
|
||||||
# sensitive values are being properly scrubbed from logger data
|
# sensitive values are being properly scrubbed from logger data
|
||||||
|
@ -53,6 +54,8 @@ if ENV["VAGRANT_LOG"] && ENV["VAGRANT_LOG"] != ""
|
||||||
# rest-client to write using the `<<` operator.
|
# rest-client to write using the `<<` operator.
|
||||||
# See https://github.com/rest-client/rest-client/issues/34#issuecomment-290858
|
# See https://github.com/rest-client/rest-client/issues/34#issuecomment-290858
|
||||||
# for more information
|
# for more information
|
||||||
|
Log4r::PatternFormatter::DirectiveTable["l"] =
|
||||||
|
'"[" + ' + Log4r::PatternFormatter::DirectiveTable["l"] + ' + "]"'
|
||||||
class VagrantLogger < Log4r::Logger
|
class VagrantLogger < Log4r::Logger
|
||||||
def << (msg)
|
def << (msg)
|
||||||
debug(msg.strip)
|
debug(msg.strip)
|
||||||
|
@ -61,13 +64,10 @@ if ENV["VAGRANT_LOG"] && ENV["VAGRANT_LOG"] != ""
|
||||||
logger = VagrantLogger.new("vagrant")
|
logger = VagrantLogger.new("vagrant")
|
||||||
logger.outputters = Log4r::Outputter.stderr
|
logger.outputters = Log4r::Outputter.stderr
|
||||||
logger.level = level
|
logger.level = level
|
||||||
base_formatter = Log4r::BasicFormatter.new
|
base_formatter = Log4r::PatternFormatter.new(
|
||||||
if ENV["VAGRANT_LOG_TIMESTAMP"]
|
pattern: "%d %-7l %C: %m",
|
||||||
base_formatter = Log4r::PatternFormatter.new(
|
date_pattern: ENV["VAGRANT_LOG_TIMESTAMP"] ? "%FT%T.%L%z" : " "
|
||||||
pattern: "%d [%5l] %m",
|
)
|
||||||
date_pattern: "%F %T"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
# Vagrant Cloud gem uses RestClient to make HTTP requests, so
|
# Vagrant Cloud gem uses RestClient to make HTTP requests, so
|
||||||
# log them if debug is enabled and use Vagrants logger
|
# log them if debug is enabled and use Vagrants logger
|
||||||
require 'rest_client'
|
require 'rest_client'
|
||||||
|
@ -118,6 +118,7 @@ module Vagrant
|
||||||
autoload :Driver, 'vagrant/driver'
|
autoload :Driver, 'vagrant/driver'
|
||||||
autoload :Environment, 'vagrant/environment'
|
autoload :Environment, 'vagrant/environment'
|
||||||
autoload :Errors, 'vagrant/errors'
|
autoload :Errors, 'vagrant/errors'
|
||||||
|
autoload :GoPlugin, 'vagrant/go_plugin'
|
||||||
autoload :Guest, 'vagrant/guest'
|
autoload :Guest, 'vagrant/guest'
|
||||||
autoload :Host, 'vagrant/host'
|
autoload :Host, 'vagrant/host'
|
||||||
autoload :Machine, 'vagrant/machine'
|
autoload :Machine, 'vagrant/machine'
|
||||||
|
|
|
@ -221,5 +221,17 @@ module Vagrant
|
||||||
"#{@name}-#{@version}-#{@provider}" <=>
|
"#{@name}-#{@version}-#{@provider}" <=>
|
||||||
"#{other.name}-#{other.version}-#{other.provider}"
|
"#{other.name}-#{other.version}-#{other.provider}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def to_json(*args)
|
||||||
|
{
|
||||||
|
name: name,
|
||||||
|
provider: provider,
|
||||||
|
version: version,
|
||||||
|
directory: directory.to_s,
|
||||||
|
metadata: metadata,
|
||||||
|
metadata_url: metadata_url
|
||||||
|
}.to_json(*args)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,10 @@ module Vagrant
|
||||||
def method_missing(name, *args, &block)
|
def method_missing(name, *args, &block)
|
||||||
DummyConfig.new
|
DummyConfig.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_json(*_)
|
||||||
|
"null"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -112,6 +112,14 @@ module Vagrant
|
||||||
@keys = state["keys"] if state.key?("keys")
|
@keys = state["keys"] if state.key?("keys")
|
||||||
@missing_key_calls = state["missing_key_calls"] if state.key?("missing_key_calls")
|
@missing_key_calls = state["missing_key_calls"] if state.key?("missing_key_calls")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_json(*args)
|
||||||
|
Hash[
|
||||||
|
@keys.find_all { |k,v|
|
||||||
|
!k.to_s.start_with?("_")
|
||||||
|
}
|
||||||
|
].to_json(*args)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -175,6 +175,15 @@ module Vagrant
|
||||||
# Load any global plugins
|
# Load any global plugins
|
||||||
Vagrant::Plugin::Manager.instance.load_plugins(plugins)
|
Vagrant::Plugin::Manager.instance.load_plugins(plugins)
|
||||||
|
|
||||||
|
# Load any available go-plugins
|
||||||
|
Util::Experimental.guard_with(:go_plugin) do
|
||||||
|
begin
|
||||||
|
Vagrant::GoPlugin::Manager.instance.globalize!
|
||||||
|
rescue LoadError => err
|
||||||
|
@logger.warn("go plugin support is not available: #{err}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
plugins = process_configured_plugins
|
plugins = process_configured_plugins
|
||||||
|
|
||||||
# Call the hooks that does not require configurations to be loaded
|
# Call the hooks that does not require configurations to be loaded
|
||||||
|
@ -918,6 +927,26 @@ module Vagrant
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def to_json(*args)
|
||||||
|
{
|
||||||
|
cwd: cwd,
|
||||||
|
data_dir: data_dir,
|
||||||
|
vagrantfile_name: vagrantfile_name,
|
||||||
|
home_path: home_path,
|
||||||
|
local_data_path: local_data_path,
|
||||||
|
tmp_path: tmp_path,
|
||||||
|
aliases_path: aliases_path,
|
||||||
|
boxes_path: boxes_path,
|
||||||
|
gems_path: gems_path,
|
||||||
|
default_private_key_path: default_private_key_path,
|
||||||
|
root_path: root_path,
|
||||||
|
primary_machine_name: primary_machine_name,
|
||||||
|
machine_names: machine_names,
|
||||||
|
active_machines: Hash[active_machines]
|
||||||
|
}.to_json(*args)
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# Attempt to guess the configured provider in use. Will fallback
|
# Attempt to guess the configured provider in use. Will fallback
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
module Vagrant
|
||||||
|
autoload :Proto, "vagrant/go_plugin/vagrant_proto/vagrant_services_pb"
|
||||||
|
|
||||||
|
module GoPlugin
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
INSTALL_DIRECTORY = Vagrant.user_data_path.join("go-plugins").to_s.freeze
|
||||||
|
|
||||||
|
autoload :CapabilityPlugin, "vagrant/go_plugin/capability_plugin"
|
||||||
|
autoload :ConfigPlugin, "vagrant/go_plugin/config_plugin"
|
||||||
|
autoload :Core, "vagrant/go_plugin/core"
|
||||||
|
autoload :GRPCPlugin, "vagrant/go_plugin/core"
|
||||||
|
autoload :Interface, "vagrant/go_plugin/interface"
|
||||||
|
autoload :Manager, "vagrant/go_plugin/manager"
|
||||||
|
autoload :ProviderPlugin, "vagrant/go_plugin/provider_plugin"
|
||||||
|
autoload :SyncedFolderPlugin, "vagrant/go_plugin/synced_folder_plugin"
|
||||||
|
|
||||||
|
# @return [Interface]
|
||||||
|
def self.interface
|
||||||
|
unless @_interface
|
||||||
|
@_interface = Interface.new
|
||||||
|
end
|
||||||
|
@_interface
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,108 @@
|
||||||
|
require "vagrant/go_plugin/core"
|
||||||
|
|
||||||
|
module Vagrant
|
||||||
|
module GoPlugin
|
||||||
|
# Contains all capability functionality for go-plugin
|
||||||
|
module CapabilityPlugin
|
||||||
|
extend Vagrant::Util::Logger
|
||||||
|
|
||||||
|
# Wrapper class for go-plugin defined capabilities
|
||||||
|
class Capability
|
||||||
|
include GRPCPlugin
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fetch any defined guest capabilites for given plugin and register
|
||||||
|
# capabilities within given plugin class
|
||||||
|
#
|
||||||
|
# @param [Vagrant::Proto::GuestCapabilities::Stub] client Plugin client
|
||||||
|
# @param [Class] plugin_klass Plugin class to register capabilities
|
||||||
|
# @param [Symbol] plugin_type Type of plugin
|
||||||
|
def self.generate_guest_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
logger.debug("checking for guest capabilities in #{plugin_type} plugin #{plugin_klass}")
|
||||||
|
result = client.guest_capabilities(Vagrant::Proto::Empty.new)
|
||||||
|
return if result.capabilities.empty?
|
||||||
|
logger.debug("guest capabilities support detected in #{plugin_type} plugin #{plugin_klass}")
|
||||||
|
result.capabilities.each do |cap|
|
||||||
|
cap_klass = Class.new(Capability).tap do |k|
|
||||||
|
k.define_singleton_method(cap.name) { |machine, *args|
|
||||||
|
response = plugin_client.guest_capability(
|
||||||
|
Vagrant::Proto::GuestCapabilityRequest.new(
|
||||||
|
machine: JSON.dump(machine), arguments: JSON.dump(args),
|
||||||
|
capability: Vagrant::Proto::SystemCapability.new(
|
||||||
|
name: cap.name, platform: cap.platform))).result
|
||||||
|
result = JSON.load(response)
|
||||||
|
if result.is_a?(Hash)
|
||||||
|
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
|
||||||
|
end
|
||||||
|
result
|
||||||
|
}
|
||||||
|
end
|
||||||
|
cap_klass.plugin_client = client
|
||||||
|
plugin_klass.guest_capability(cap.platform.to_sym, cap.name.to_sym) { cap_klass }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fetch any defined host capabilites for given plugin and register
|
||||||
|
# capabilities within given plugin class
|
||||||
|
#
|
||||||
|
# @param [Vagrant::Proto::HostCapabilities::Stub] client Plugin client
|
||||||
|
# @param [Class] plugin_klass Plugin class to register capabilities
|
||||||
|
# @param [Symbol] plugin_type Type of plugin
|
||||||
|
def self.generate_host_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
logger.debug("checking for host capabilities in #{plugin_type} plugin #{plugin_klass}")
|
||||||
|
result = client.host_capabilities(Vagrant::Proto::Empty.new)
|
||||||
|
return if result.capabilities.empty?
|
||||||
|
logger.debug("host capabilities support detected in #{plugin_type} plugin #{plugin_klass}")
|
||||||
|
result.capabilities.each do |cap|
|
||||||
|
cap_klass = Class.new(Capability).tap do |k|
|
||||||
|
k.define_singleton_method(cap.name) { |environment, *args|
|
||||||
|
response = plugin_client.host_capability(
|
||||||
|
Vagrant::Proto::HostCapabilityRequest.new(
|
||||||
|
environment: JSON.dump(environment), arguments: JSON.dump(args),
|
||||||
|
capability: Vagrant::Proto::SystemCapability.new(
|
||||||
|
name: cap.name, platform: cap.platform))).result
|
||||||
|
result = JSON.load(response)
|
||||||
|
if result.is_a?(Hash)
|
||||||
|
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
|
||||||
|
end
|
||||||
|
result
|
||||||
|
}
|
||||||
|
end
|
||||||
|
cap_klass.plugin_client = client
|
||||||
|
plugin_klass.host_capability(cap.platform.to_sym, cap.name.to_sym) { cap_klass }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fetch any defined provider capabilites for given plugin and register
|
||||||
|
# capabilities within given plugin class
|
||||||
|
#
|
||||||
|
# @param [Vagrant::Proto::ProviderCapabilities::Stub] client Plugin client
|
||||||
|
# @param [Class] plugin_klass Plugin class to register capabilities
|
||||||
|
# @param [Symbol] plugin_type Type of plugin
|
||||||
|
def self.generate_provider_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
logger.debug("checking for provider capabilities in #{plugin_type} plugin #{plugin_klass}")
|
||||||
|
result = client.provider_capabilities(Vagrant::Proto::Empty.new)
|
||||||
|
return if result.capabilities.empty?
|
||||||
|
logger.debug("provider capabilities support detected in #{plugin_type} plugin #{plugin_klass}")
|
||||||
|
result.capabilities.each do |cap|
|
||||||
|
cap_klass = Class.new(Capability).tap do |k|
|
||||||
|
k.define_singleton_method(cap.name) { |machine, *args|
|
||||||
|
response = plugin_client.provider_capability(
|
||||||
|
Vagrant::Proto::ProviderCapabilityRequest.new(
|
||||||
|
machine: JSON.dump(machine), arguments: JSON.dump(args),
|
||||||
|
capability: Vagrant::Proto::ProviderCapability.new(
|
||||||
|
name: cap.name, provider: cap.provider))).result
|
||||||
|
result = JSON.load(response)
|
||||||
|
if result.is_a?(Hash)
|
||||||
|
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
|
||||||
|
end
|
||||||
|
result
|
||||||
|
}
|
||||||
|
end
|
||||||
|
cap_klass.plugin_client = client
|
||||||
|
plugin_klass.provider_capability(cap.provider.to_sym, cap.name.to_sym) { cap_klass }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,64 @@
|
||||||
|
require "vagrant/go_plugin/core"
|
||||||
|
|
||||||
|
module Vagrant
|
||||||
|
module GoPlugin
|
||||||
|
# Contains all configuration functionality for go-plugin
|
||||||
|
module ConfigPlugin
|
||||||
|
# Generate configuration for the parent class
|
||||||
|
#
|
||||||
|
# @param [Vagrant::Proto::Config::Stub] client Plugin client
|
||||||
|
# @param [String] parent_name Parent plugin name
|
||||||
|
# @param [Class] parent_klass Parent class to register config
|
||||||
|
# @param [Symbol] parent_type Type of parent class (:provider, :synced_folder, etc)
|
||||||
|
def self.generate_config(client, parent_name, parent_klass, parent_type)
|
||||||
|
config_attrs = client.config_attributes(Vagrant::Proto::Empty.new).items
|
||||||
|
config_klass = Class.new(Config).tap do |c|
|
||||||
|
c.class_eval("def parent_name; '#{parent_name}'; end")
|
||||||
|
end
|
||||||
|
config_klass.plugin_client = client
|
||||||
|
Array(config_attrs).each do |att|
|
||||||
|
config_klass.instance_eval("attr_accessor :#{att}")
|
||||||
|
end
|
||||||
|
parent_klass.config(parent_name, parent_type) { config_klass }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Config plugin class used with go-plugin
|
||||||
|
class Config < Vagrant.plugin("2", :config)
|
||||||
|
include GRPCPlugin
|
||||||
|
|
||||||
|
# Finalize the current configuration
|
||||||
|
def finalize!
|
||||||
|
data = local_data
|
||||||
|
response = plugin_client.config_finalize(Vagrant::Proto::Configuration.new(
|
||||||
|
data: JSON.dump(data)))
|
||||||
|
result = JSON.load(response.data)
|
||||||
|
if result && result.is_a?(Hash)
|
||||||
|
new_data = Vagrant::Util::HashWithIndifferentAccess.new(result)
|
||||||
|
new_data.each do |key, value|
|
||||||
|
next if data[key] == value
|
||||||
|
instance_variable_set("@#{key}", value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validate configuration
|
||||||
|
#
|
||||||
|
# @param [Vagrant::Machine] machine Guest machine
|
||||||
|
# @return [Array<String>] list of errors
|
||||||
|
def validate(machine)
|
||||||
|
result = plugin_client.config_validate(Vagrant::Proto::Configuration.new(
|
||||||
|
machine: JSON.dump(machine),
|
||||||
|
data: JSON.dump(local_data)))
|
||||||
|
{parent_name => result.items}
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Hash] currently defined instance variables
|
||||||
|
def local_data
|
||||||
|
Vagrant::Util::HashWithIndifferentAccess.
|
||||||
|
new(instance_variables_hash)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,82 @@
|
||||||
|
require "ffi"
|
||||||
|
|
||||||
|
module Vagrant
|
||||||
|
module GoPlugin
|
||||||
|
# Base module for generic setup of module/class
|
||||||
|
module Core
|
||||||
|
# Loads FFI and core helpers into given module/class
|
||||||
|
def self.included(const)
|
||||||
|
const.class_eval do
|
||||||
|
include Vagrant::Util::Logger
|
||||||
|
extend FFI::Library
|
||||||
|
|
||||||
|
ffi_lib FFI::Platform::LIBC
|
||||||
|
ffi_lib File.expand_path("./go-plugin.so", File.dirname(__FILE__))
|
||||||
|
|
||||||
|
typedef :strptr, :plugin_result
|
||||||
|
|
||||||
|
# stdlib functions
|
||||||
|
if FFI::Platform.windows?
|
||||||
|
attach_function :free, :_free, [:pointer], :void
|
||||||
|
else
|
||||||
|
attach_function :free, [:pointer], :void
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load the result received from the extension. This will load
|
||||||
|
# the JSON result, raise an error if detected, and properly
|
||||||
|
# free the memory associated with the result.
|
||||||
|
def load_result(*args)
|
||||||
|
val, ptr = block_given? ? yield : args
|
||||||
|
FFI::AutoPointer.new(ptr, self.method(:free))
|
||||||
|
begin
|
||||||
|
result = JSON.load(val)
|
||||||
|
if !result.is_a?(Hash)
|
||||||
|
raise TypeError.new "Expected Hash but received `#{result.class}`"
|
||||||
|
end
|
||||||
|
if !result["error"].to_s.empty?
|
||||||
|
raise ArgumentError.new result["error"].to_s
|
||||||
|
end
|
||||||
|
result = result["result"]
|
||||||
|
if result.is_a?(Hash)
|
||||||
|
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
|
||||||
|
end
|
||||||
|
result
|
||||||
|
rescue => e
|
||||||
|
# TODO: Customize to provide formatted output on error
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Simple module to load into plugin wrapper classes
|
||||||
|
# to provide expected functionality
|
||||||
|
module GRPCPlugin
|
||||||
|
module ClassMethods
|
||||||
|
def plugin_client
|
||||||
|
@_plugin_client
|
||||||
|
end
|
||||||
|
|
||||||
|
def plugin_client=(c)
|
||||||
|
if @_plugin_client
|
||||||
|
raise ArgumentError, "Plugin client has already been set"
|
||||||
|
end
|
||||||
|
@_plugin_client = c
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module InstanceMethods
|
||||||
|
def plugin_client
|
||||||
|
self.class.plugin_client
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.included(klass)
|
||||||
|
klass.include(Vagrant::Util::Logger)
|
||||||
|
klass.include(InstanceMethods)
|
||||||
|
klass.extend(ClassMethods)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,187 @@
|
||||||
|
require "log4r"
|
||||||
|
require "vagrant/go_plugin/core"
|
||||||
|
|
||||||
|
require "vagrant/go_plugin/vagrant_proto/vagrant_services_pb"
|
||||||
|
|
||||||
|
module RubyLogger
|
||||||
|
include Vagrant::Util::Logger
|
||||||
|
end
|
||||||
|
|
||||||
|
module GRPC
|
||||||
|
extend Vagrant::Util::Logger
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
module Vagrant
|
||||||
|
module GoPlugin
|
||||||
|
# Interface for go-plugin integration
|
||||||
|
class Interface
|
||||||
|
include Core
|
||||||
|
|
||||||
|
# go plugin functions
|
||||||
|
typedef :bool, :enable_logger
|
||||||
|
typedef :bool, :timestamps
|
||||||
|
typedef :string, :log_level
|
||||||
|
typedef :string, :plugin_directory
|
||||||
|
|
||||||
|
attach_function :_setup, :Setup, [:enable_logger, :timestamps, :log_level], :bool
|
||||||
|
attach_function :_teardown, :Teardown, [], :void
|
||||||
|
attach_function :_reset, :Reset, [], :void
|
||||||
|
attach_function :_load_plugins, :LoadPlugins, [:plugin_directory], :bool
|
||||||
|
attach_function :_list_providers, :ListProviders, [], :plugin_result
|
||||||
|
attach_function :_list_synced_folders, :ListSyncedFolders, [], :plugin_result
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
Vagrant::Proto.instance_eval do
|
||||||
|
::GRPC.extend(Vagrant::Util::Logger)
|
||||||
|
end
|
||||||
|
setup
|
||||||
|
end
|
||||||
|
|
||||||
|
# List of provider plugins currently available
|
||||||
|
#
|
||||||
|
# @return [Hash<String,Hash<Info>>]
|
||||||
|
def list_providers
|
||||||
|
load_result { _list_providers } || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# List of synced folder plugins currently available
|
||||||
|
#
|
||||||
|
# @return [Hash<String,Hash<Info>>]
|
||||||
|
def list_synced_folders
|
||||||
|
load_result { _list_synced_folders } || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load any plugins found at the given directory
|
||||||
|
#
|
||||||
|
# @param [String, Pathname] path Directory to load
|
||||||
|
def load_plugins(path)
|
||||||
|
logger.debug("loading plugins from path: #{path}")
|
||||||
|
if !File.directory?(path.to_s)
|
||||||
|
raise ArgumentError, "Directory expected for plugin loading"
|
||||||
|
end
|
||||||
|
_load_plugins(path.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Register all available plugins
|
||||||
|
def register_plugins
|
||||||
|
logger.debug("registering provider plugins")
|
||||||
|
load_providers
|
||||||
|
logger.debug("registering synced folder plugins")
|
||||||
|
load_synced_folders
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load the plugins found at the given directory
|
||||||
|
#
|
||||||
|
# @param [String] plugin_directory Directory containing go-plugins
|
||||||
|
def setup
|
||||||
|
if !@setup
|
||||||
|
@setup = true
|
||||||
|
Kernel.at_exit { Vagrant::GoPlugin.interface.teardown }
|
||||||
|
logger.debug("running go-plugin interface setup")
|
||||||
|
_setup(!Vagrant.log_level.to_s.empty?,
|
||||||
|
!!ENV["VAGRANT_LOG_TIMESTAMP"],
|
||||||
|
Vagrant.log_level.to_s)
|
||||||
|
else
|
||||||
|
logger.warn("go-plugin interface already setup")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset
|
||||||
|
logger.debug("running go-plugin interface reset")
|
||||||
|
_reset
|
||||||
|
logger.debug("completed go-plugin interface reset")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Teardown any plugins that may be currently active
|
||||||
|
def teardown
|
||||||
|
logger.debug("starting teardown of go-plugin interface")
|
||||||
|
_teardown
|
||||||
|
logger.debug("teardown of go-plugin interface complete")
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Boolean] go plugins have been setup
|
||||||
|
def configured?
|
||||||
|
!!@setup
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load any detected provider plugins
|
||||||
|
def load_providers
|
||||||
|
if !@_providers_loaded
|
||||||
|
@_providers_loaded
|
||||||
|
logger.debug("provider go-plugins have not been loaded... loading")
|
||||||
|
list_providers.each do |p_name, p_details|
|
||||||
|
logger.debug("loading go-plugin provider #{p_name}. details - #{p_details}")
|
||||||
|
client = Vagrant::Proto::Provider::Stub.new(
|
||||||
|
"#{p_details[:network]}://#{p_details[:address]}",
|
||||||
|
:this_channel_is_insecure)
|
||||||
|
# Create new provider class wrapper
|
||||||
|
provider_klass = Class.new(ProviderPlugin::Provider)
|
||||||
|
provider_klass.plugin_client = client
|
||||||
|
# Create new plugin to register the provider
|
||||||
|
plugin_klass = Class.new(Vagrant.plugin("2"))
|
||||||
|
# Define the plugin
|
||||||
|
plugin_klass.class_eval do
|
||||||
|
name "#{p_name} Provider"
|
||||||
|
description p_details[:description]
|
||||||
|
end
|
||||||
|
# Register the provider
|
||||||
|
plugin_klass.provider(p_name.to_sym, priority: p_details.fetch(:priority, 0)) do
|
||||||
|
provider_klass
|
||||||
|
end
|
||||||
|
# Setup any configuration support
|
||||||
|
ConfigPlugin.generate_config(client, p_name, plugin_klass, :provider)
|
||||||
|
# Register any guest capabilities
|
||||||
|
CapabilityPlugin.generate_guest_capabilities(client, plugin_klass, :provider)
|
||||||
|
# Register any host capabilities
|
||||||
|
CapabilityPlugin.generate_host_capabilities(client, plugin_klass, :provider)
|
||||||
|
# Register any provider capabilities
|
||||||
|
CapabilityPlugin.generate_provider_capabilities(client, plugin_klass, :provider)
|
||||||
|
logger.debug("completed loading provider go-plugin #{p_name}")
|
||||||
|
logger.info("loaded go-plugin provider - #{p_name}")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
logger.warn("provider go-plugins have already been loaded. ignoring load request.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load any detected synced folder plugins
|
||||||
|
def load_synced_folders
|
||||||
|
if !@_synced_folders_loaded
|
||||||
|
@_synced_folders_loaded = true
|
||||||
|
logger.debug("synced folder go-plugins have not been loaded... loading")
|
||||||
|
Array(list_synced_folders).each do |f_name, f_details|
|
||||||
|
logger.debug("loading go-plugin synced folder #{f_name}. details - #{f_details}")
|
||||||
|
client = Vagrant::Proto::SyncedFolder::Stub.new(
|
||||||
|
"#{p_details[:network]}://#{p_details[:address]}",
|
||||||
|
:this_channel_is_insecure)
|
||||||
|
# Create new synced folder class wrapper
|
||||||
|
folder_klass = Class.new(SyncedFolderPlugin::SyncedFolder)
|
||||||
|
folder_klass.plugin_client = client
|
||||||
|
# Create new plugin to register the synced folder
|
||||||
|
plugin_klass = Class.new(Vagrant.plugin("2"))
|
||||||
|
# Define the plugin
|
||||||
|
plugin_klass.class_eval do
|
||||||
|
name "#{f_name} Synced Folder"
|
||||||
|
description f_details[:description]
|
||||||
|
end
|
||||||
|
# Register the synced folder
|
||||||
|
plugin_klass.synced_folder(f_name.to_sym, priority: f_details.fetch(:priority, 10)) do
|
||||||
|
folder_klass
|
||||||
|
end
|
||||||
|
# Register any guest capabilities
|
||||||
|
CapabilityPlugin.generate_guest_capabilities(client, plugin_klass, :synced_folder)
|
||||||
|
# Register any host capabilities
|
||||||
|
CapabilityPlugin.generate_host_capabilities(client, plugin_klass, :synced_folder)
|
||||||
|
# Register any provider capabilities
|
||||||
|
CapabilityPlugin.generate_provider_capabilities(client, plugin_klass, :synced_folder)
|
||||||
|
logger.debug("completed loading synced folder go-plugin #{f_name}")
|
||||||
|
logger.info("loaded go-plugin synced folder - #{f_name}")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
logger.warn("synced folder go-plugins have already been loaded. ignoring load request.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,87 @@
|
||||||
|
require "zip"
|
||||||
|
require "vagrant/plugin/state_file"
|
||||||
|
|
||||||
|
module Vagrant
|
||||||
|
module GoPlugin
|
||||||
|
class Manager
|
||||||
|
include Util::Logger
|
||||||
|
|
||||||
|
# @return [Manager]
|
||||||
|
def self.instance
|
||||||
|
if !@instance
|
||||||
|
@instance = self.new
|
||||||
|
end
|
||||||
|
@instance
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [StateFile] user defined plugins
|
||||||
|
attr_reader :user_file
|
||||||
|
# @return [StateFile, nil] project local defined plugins
|
||||||
|
attr_reader :local_file
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
FileUtils.mkdir_p(INSTALL_DIRECTORY)
|
||||||
|
FileUtils.mkdir_p(Vagrant.user_data_path.join("tmp").to_s)
|
||||||
|
@user_file = Plugin::StateFile.new(Vagrant.user_data_path.join("plugins.json"))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load global plugins
|
||||||
|
def globalize!
|
||||||
|
Dir.glob(File.join(INSTALL_DIRECTORY, "*")).each do |entry|
|
||||||
|
next if !File.directory?(entry)
|
||||||
|
logger.debug("loading go plugins from directory: #{entry}")
|
||||||
|
GoPlugin.interface.load_plugins(entry)
|
||||||
|
end
|
||||||
|
GoPlugin.interface.register_plugins
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load local plugins
|
||||||
|
def localize!
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# Install a go plugin
|
||||||
|
#
|
||||||
|
# @param [String] plugin_name Name of plugin
|
||||||
|
# @param [String] remote_source Location of plugin for download
|
||||||
|
# @param [Hash] options Currently unused
|
||||||
|
def install_plugin(plugin_name, remote_source, options={})
|
||||||
|
install_dir = File.join(INSTALL_DIRECTORY, plugin_name)
|
||||||
|
FileUtils.mkdir_p(install_dir)
|
||||||
|
Dir.mktmpdir("go-plugin", Vagrant.user_data_path.join("tmp").to_s) do |dir|
|
||||||
|
dest_file = File.join(dir, "plugin.zip")
|
||||||
|
logger.debug("downloading go plugin #{plugin_name} from #{remote_source}")
|
||||||
|
Util::Downloader.new(remote_source, dest_file).download!
|
||||||
|
logger.debug("extracting go plugin #{plugin_name} from #{dest_file}")
|
||||||
|
Zip::File.open(dest_file) do |zfile|
|
||||||
|
zfile.each do |entry|
|
||||||
|
install_path = File.join(install_dir, entry.name)
|
||||||
|
if File.file?(install_path)
|
||||||
|
logger.warn("removing existing plugin path for unpacking - #{install_path}")
|
||||||
|
File.delete(install_path)
|
||||||
|
end
|
||||||
|
entry.extract(install_path)
|
||||||
|
FileUtils.chmod(0755, install_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
user_file.add_go_plugin(plugin_name, source: remote_source)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Uninstall a go plugin
|
||||||
|
#
|
||||||
|
# @param [String] plugin_name Name of plugin
|
||||||
|
# @param [Hash] options Currently unused
|
||||||
|
def uninstall_plugin(plugin_name, options={})
|
||||||
|
plugin_path = File.join(INSTALL_DIRECTORY, plugin_name)
|
||||||
|
if !File.directory?(plugin_path)
|
||||||
|
logger.warn("Plugin directory does not exist for #{plugin_name} - #{plugin_path}")
|
||||||
|
else
|
||||||
|
logger.debug("deleting go plugin from path #{plugin_path}")
|
||||||
|
FileUtils.rm_rf(plugin_path)
|
||||||
|
end
|
||||||
|
user_file.remove_go_plugin(plugin_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,171 @@
|
||||||
|
require "vagrant/go_plugin/core"
|
||||||
|
|
||||||
|
module Vagrant
|
||||||
|
module GoPlugin
|
||||||
|
module ProviderPlugin
|
||||||
|
# Helper class for wrapping actions in a go-plugin into
|
||||||
|
# something which can be used by Vagrant::Action::Builder
|
||||||
|
class Action
|
||||||
|
include GRPCPlugin
|
||||||
|
|
||||||
|
# @return [String] action name associated to this class
|
||||||
|
def self.action_name
|
||||||
|
@action_name
|
||||||
|
end
|
||||||
|
|
||||||
|
# Set the action name for this class
|
||||||
|
#
|
||||||
|
# @param [String] n action name
|
||||||
|
# @return [String]
|
||||||
|
# @note can only be set once
|
||||||
|
def self.action_name=(n)
|
||||||
|
if @action_name
|
||||||
|
raise ArgumentError.new("Class action name has already been set")
|
||||||
|
end
|
||||||
|
@action_name = n.to_s.dup.freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(app, env)
|
||||||
|
@app = app
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run the action
|
||||||
|
def call(env)
|
||||||
|
if env.is_a?(Hash) && !env.is_a?(Vagrant::Util::HashWithIndifferentAccess)
|
||||||
|
env = Vagrant::Util::HashWithIndifferentAccess.new(env)
|
||||||
|
end
|
||||||
|
machine = env.fetch(:machine, {})
|
||||||
|
response = plugin_client.run_action(
|
||||||
|
Vagrant::Proto::ExecuteAction.new(
|
||||||
|
name: self.class.action_name,
|
||||||
|
data: JSON.dump(env),
|
||||||
|
machine: JSON.dump(machine)))
|
||||||
|
result = JSON.load(response.result)
|
||||||
|
if result.is_a?(Hash)
|
||||||
|
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
|
||||||
|
result.each_pair do |k, v|
|
||||||
|
env[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@app.call(env)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper class used to provide a wrapper around a go-plugin
|
||||||
|
# provider so that it can be interacted with normally within
|
||||||
|
# Vagrant
|
||||||
|
class Provider < Vagrant.plugin("2", :provider)
|
||||||
|
include GRPCPlugin
|
||||||
|
|
||||||
|
# @return [Vagrant::Machine]
|
||||||
|
attr_reader :machine
|
||||||
|
|
||||||
|
def initialize(machine)
|
||||||
|
@machine = machine
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String] name of the provider plugin for this class
|
||||||
|
def name
|
||||||
|
if !@_name
|
||||||
|
@_name = plugin_client.name(Vagrant::Proto::Empty.new).name
|
||||||
|
end
|
||||||
|
@_name
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get callable action by name
|
||||||
|
#
|
||||||
|
# @param [Symbol] name name of the action
|
||||||
|
# @return [Class] callable action class
|
||||||
|
def action(name)
|
||||||
|
result = plugin_client.action(
|
||||||
|
Vagrant::Proto::GenericAction.new(
|
||||||
|
name: name.to_s,
|
||||||
|
machine: JSON.dump(machine)))
|
||||||
|
klasses = result.items.map do |klass_name|
|
||||||
|
if klass_name.start_with?("self::")
|
||||||
|
action_name = klass_name.split("::", 2).last
|
||||||
|
klass = Class.new(Action)
|
||||||
|
klass.plugin_client = plugin_client
|
||||||
|
klass.action_name = action_name
|
||||||
|
klass.class_eval do
|
||||||
|
def self.name
|
||||||
|
action_name.capitalize.tr("_", "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
klass
|
||||||
|
else
|
||||||
|
klass_name.split("::").inject(Object) do |memo, const|
|
||||||
|
if memo.const_defined?(const)
|
||||||
|
memo.const_get(const)
|
||||||
|
else
|
||||||
|
raise NameError, "Unknown action class `#{klass_name}`"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Vagrant::Action::Builder.new.tap do |builder|
|
||||||
|
klasses.each do |action_class|
|
||||||
|
builder.use action_class
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Execute capability with given name
|
||||||
|
#
|
||||||
|
# @param [Symbol] cap_name Name of the capability
|
||||||
|
# @return [Object]
|
||||||
|
def capability(cap_name, *args)
|
||||||
|
r = plugin_client.provider_capability(
|
||||||
|
Vagrant::Proto::ProviderCapabilityRequest.new(
|
||||||
|
capability: Vagrant::Proto::ProviderCapability.new(
|
||||||
|
name: cap_name.to_s,
|
||||||
|
provider: name
|
||||||
|
),
|
||||||
|
machine: JSON.dump(machine),
|
||||||
|
arguments: JSON.dump(args)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = JSON.load(r.result)
|
||||||
|
if result.is_a?(Hash)
|
||||||
|
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Boolean] provider is installed
|
||||||
|
def is_installed?
|
||||||
|
plugin_client.is_installed(Vagrant::Proto::Machine.new(
|
||||||
|
machine: JSON.dump(machine))).result
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Boolean] provider is usable
|
||||||
|
def is_usable?
|
||||||
|
plugin_client.is_usable(Vagrant::Proto::Machine.new(
|
||||||
|
machine: JSON.dump(machine))).result
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [nil]
|
||||||
|
def machine_id_changed
|
||||||
|
plugin_client.machine_id_changed(Vagrant::Proto::Machine.new(
|
||||||
|
machine: JSON.dump(machine)))
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Hash] SSH information
|
||||||
|
def ssh_info
|
||||||
|
result = plugin_client.ssh_info(Vagrant::Proto::Machine.new(
|
||||||
|
machine: JSON.dump(machine))).to_hash
|
||||||
|
Vagrant::Util::HashWithIndifferentAccess.new(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Vagrant::MachineState]
|
||||||
|
def state
|
||||||
|
result = plugin_client.state(Vagrant::Proto::Machine.new(
|
||||||
|
machine: JSON.dump(machine)))
|
||||||
|
Vagrant::MachineState.new(result.id,
|
||||||
|
result.short_description, result.long_description)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,89 @@
|
||||||
|
require "vagrant/go_plugin/core"
|
||||||
|
|
||||||
|
module Vagrant
|
||||||
|
module GoPlugin
|
||||||
|
# Contains all synced folder functionality for go-plugin
|
||||||
|
module SyncedFolderPlugin
|
||||||
|
|
||||||
|
# Helper class used to provide a wrapper around a go-plugin
|
||||||
|
# synced folder so that it can be interacted with normally
|
||||||
|
# within Vagrant
|
||||||
|
class SyncedFolder < Vagrant.plugin("2", :synced_folder)
|
||||||
|
include GRPCPlugin
|
||||||
|
|
||||||
|
# Cleanup synced folders
|
||||||
|
#
|
||||||
|
# @param [Vagrant::Machine] machine Vagrant guest
|
||||||
|
# @param [Hash] opts Folder options
|
||||||
|
def cleanup(machine, opts)
|
||||||
|
plugin_client.cleanup(
|
||||||
|
Vagrant::Proto::SyncedFolders.new(
|
||||||
|
machine: JSON.dump(machine),
|
||||||
|
options: JSON.dump(opts),
|
||||||
|
folders: JSON.dump({})))
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Disable synced folders
|
||||||
|
#
|
||||||
|
# @param [Vagrant::Machine] machine Vagrant guest
|
||||||
|
# @param [Hash] folders Folders to enable
|
||||||
|
# @param [Hash] opts Folder options
|
||||||
|
def disable(machine, folders, opts)
|
||||||
|
plugin_client.disable(
|
||||||
|
Vagrant::Proto::SyncedFolders.new(
|
||||||
|
machine: JSON.dump(machine),
|
||||||
|
folders: JSON.dump(folders),
|
||||||
|
options: JSON.dump(opts)))
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Enable synced folders
|
||||||
|
#
|
||||||
|
# @param [Vagrant::Machine] machine Vagrant guest
|
||||||
|
# @param [Hash] folders Folders to enable
|
||||||
|
# @param [Hash] opts Folder options
|
||||||
|
def enable(machine, folders, opts)
|
||||||
|
plugin_client.enable(
|
||||||
|
Vagrant::Proto::SyncedFolders.new(
|
||||||
|
machine: JSON.dump(machine),
|
||||||
|
folders: JSON.dump(folders),
|
||||||
|
options: JSON.dump(options)))
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Prepare synced folders
|
||||||
|
#
|
||||||
|
# @param [Vagrant::Machine] machine Vagrant guest
|
||||||
|
# @param [Hash] folders Folders to enable
|
||||||
|
# @param [Hash] opts Folder options
|
||||||
|
def prepare(machine, folders, opts)
|
||||||
|
plugin_client.prepare(
|
||||||
|
Vagrant::Proto::SyncedFolders.new(
|
||||||
|
machine: JSON.dump(machine),
|
||||||
|
folders: JSON.dump(folders),
|
||||||
|
options: JSON.dump(options)))
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if plugin is usable
|
||||||
|
#
|
||||||
|
# @param [Vagrant::Machine] machine Vagrant guest
|
||||||
|
# @return [Boolean]
|
||||||
|
def usable?(machine, raise_error=false)
|
||||||
|
plugin_client.is_usable(
|
||||||
|
Vagrant::Proto::Machine.new(
|
||||||
|
machine: JSON.dump(machine))).result
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def name
|
||||||
|
if !@_name
|
||||||
|
@_name = plugin_client.name(Vagrant::Proto::Empty.new).name
|
||||||
|
end
|
||||||
|
@_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,122 @@
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# source: vagrant.proto
|
||||||
|
|
||||||
|
require 'google/protobuf'
|
||||||
|
|
||||||
|
Google::Protobuf::DescriptorPool.generated_pool.build do
|
||||||
|
add_file("vagrant.proto", :syntax => :proto3) do
|
||||||
|
add_message "vagrant.proto.Empty" do
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.Machine" do
|
||||||
|
optional :machine, :string, 1
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.Valid" do
|
||||||
|
optional :result, :bool, 1
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.Identifier" do
|
||||||
|
optional :name, :string, 1
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.PluginInfo" do
|
||||||
|
optional :description, :string, 1
|
||||||
|
optional :priority, :int64, 2
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.Content" do
|
||||||
|
optional :target, :string, 1
|
||||||
|
optional :value, :string, 2
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.WriteResponse" do
|
||||||
|
optional :length, :int32, 1
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.SystemCapability" do
|
||||||
|
optional :name, :string, 1
|
||||||
|
optional :platform, :string, 2
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.ProviderCapability" do
|
||||||
|
optional :name, :string, 1
|
||||||
|
optional :provider, :string, 2
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.SystemCapabilityList" do
|
||||||
|
repeated :capabilities, :message, 1, "vagrant.proto.SystemCapability"
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.ProviderCapabilityList" do
|
||||||
|
repeated :capabilities, :message, 1, "vagrant.proto.ProviderCapability"
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.GenericResponse" do
|
||||||
|
optional :result, :string, 1
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.GuestCapabilityRequest" do
|
||||||
|
optional :capability, :message, 1, "vagrant.proto.SystemCapability"
|
||||||
|
optional :machine, :string, 2
|
||||||
|
optional :arguments, :string, 3
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.HostCapabilityRequest" do
|
||||||
|
optional :capability, :message, 1, "vagrant.proto.SystemCapability"
|
||||||
|
optional :environment, :string, 2
|
||||||
|
optional :arguments, :string, 3
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.ProviderCapabilityRequest" do
|
||||||
|
optional :capability, :message, 1, "vagrant.proto.ProviderCapability"
|
||||||
|
optional :machine, :string, 2
|
||||||
|
optional :arguments, :string, 3
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.Configuration" do
|
||||||
|
optional :data, :string, 1
|
||||||
|
optional :machine, :string, 2
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.ListResponse" do
|
||||||
|
repeated :items, :string, 1
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.SyncedFolders" do
|
||||||
|
optional :machine, :string, 1
|
||||||
|
optional :folders, :string, 2
|
||||||
|
optional :options, :string, 3
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.GenericAction" do
|
||||||
|
optional :name, :string, 1
|
||||||
|
optional :machine, :string, 2
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.ExecuteAction" do
|
||||||
|
optional :name, :string, 1
|
||||||
|
optional :data, :string, 2
|
||||||
|
optional :machine, :string, 3
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.MachineSshInfo" do
|
||||||
|
optional :host, :string, 1
|
||||||
|
optional :port, :int64, 2
|
||||||
|
optional :private_key_path, :string, 3
|
||||||
|
optional :username, :string, 4
|
||||||
|
end
|
||||||
|
add_message "vagrant.proto.MachineState" do
|
||||||
|
optional :id, :string, 1
|
||||||
|
optional :short_description, :string, 2
|
||||||
|
optional :long_description, :string, 3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module Vagrant
|
||||||
|
module Proto
|
||||||
|
Empty = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Empty").msgclass
|
||||||
|
Machine = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Machine").msgclass
|
||||||
|
Valid = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Valid").msgclass
|
||||||
|
Identifier = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Identifier").msgclass
|
||||||
|
PluginInfo = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.PluginInfo").msgclass
|
||||||
|
Content = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Content").msgclass
|
||||||
|
WriteResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.WriteResponse").msgclass
|
||||||
|
SystemCapability = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.SystemCapability").msgclass
|
||||||
|
ProviderCapability = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ProviderCapability").msgclass
|
||||||
|
SystemCapabilityList = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.SystemCapabilityList").msgclass
|
||||||
|
ProviderCapabilityList = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ProviderCapabilityList").msgclass
|
||||||
|
GenericResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.GenericResponse").msgclass
|
||||||
|
GuestCapabilityRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.GuestCapabilityRequest").msgclass
|
||||||
|
HostCapabilityRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.HostCapabilityRequest").msgclass
|
||||||
|
ProviderCapabilityRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ProviderCapabilityRequest").msgclass
|
||||||
|
Configuration = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Configuration").msgclass
|
||||||
|
ListResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ListResponse").msgclass
|
||||||
|
SyncedFolders = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.SyncedFolders").msgclass
|
||||||
|
GenericAction = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.GenericAction").msgclass
|
||||||
|
ExecuteAction = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ExecuteAction").msgclass
|
||||||
|
MachineSshInfo = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.MachineSshInfo").msgclass
|
||||||
|
MachineState = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.MachineState").msgclass
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,223 @@
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# Source: vagrant.proto for package 'vagrant.proto'
|
||||||
|
|
||||||
|
require 'grpc'
|
||||||
|
require_relative 'vagrant_pb'
|
||||||
|
|
||||||
|
module Vagrant
|
||||||
|
module Proto
|
||||||
|
module IO
|
||||||
|
class Service
|
||||||
|
|
||||||
|
include GRPC::GenericService
|
||||||
|
|
||||||
|
self.marshal_class_method = :encode
|
||||||
|
self.unmarshal_class_method = :decode
|
||||||
|
self.service_name = 'vagrant.proto.IO'
|
||||||
|
|
||||||
|
rpc :Read, Identifier, Content
|
||||||
|
rpc :Write, Content, WriteResponse
|
||||||
|
end
|
||||||
|
|
||||||
|
Stub = Service.rpc_stub_class
|
||||||
|
end
|
||||||
|
module GuestCapabilities
|
||||||
|
class Service
|
||||||
|
|
||||||
|
include GRPC::GenericService
|
||||||
|
|
||||||
|
self.marshal_class_method = :encode
|
||||||
|
self.unmarshal_class_method = :decode
|
||||||
|
self.service_name = 'vagrant.proto.GuestCapabilities'
|
||||||
|
|
||||||
|
rpc :GuestCapabilities, Empty, SystemCapabilityList
|
||||||
|
rpc :GuestCapability, GuestCapabilityRequest, GenericResponse
|
||||||
|
# IO helpers for streaming (copied from Stream service)
|
||||||
|
rpc :Read, Identifier, Content
|
||||||
|
rpc :Write, Content, WriteResponse
|
||||||
|
end
|
||||||
|
|
||||||
|
Stub = Service.rpc_stub_class
|
||||||
|
end
|
||||||
|
module HostCapabilities
|
||||||
|
class Service
|
||||||
|
|
||||||
|
include GRPC::GenericService
|
||||||
|
|
||||||
|
self.marshal_class_method = :encode
|
||||||
|
self.unmarshal_class_method = :decode
|
||||||
|
self.service_name = 'vagrant.proto.HostCapabilities'
|
||||||
|
|
||||||
|
rpc :HostCapabilities, Empty, SystemCapabilityList
|
||||||
|
rpc :HostCapability, HostCapabilityRequest, GenericResponse
|
||||||
|
# IO helpers for streaming (copied from Stream service)
|
||||||
|
rpc :Read, Identifier, Content
|
||||||
|
rpc :Write, Content, WriteResponse
|
||||||
|
end
|
||||||
|
|
||||||
|
Stub = Service.rpc_stub_class
|
||||||
|
end
|
||||||
|
module ProviderCapabilities
|
||||||
|
class Service
|
||||||
|
|
||||||
|
include GRPC::GenericService
|
||||||
|
|
||||||
|
self.marshal_class_method = :encode
|
||||||
|
self.unmarshal_class_method = :decode
|
||||||
|
self.service_name = 'vagrant.proto.ProviderCapabilities'
|
||||||
|
|
||||||
|
rpc :ProviderCapabilities, Empty, ProviderCapabilityList
|
||||||
|
rpc :ProviderCapability, ProviderCapabilityRequest, GenericResponse
|
||||||
|
# IO helpers for streaming (copied from Stream service)
|
||||||
|
rpc :Read, Identifier, Content
|
||||||
|
rpc :Write, Content, WriteResponse
|
||||||
|
end
|
||||||
|
|
||||||
|
Stub = Service.rpc_stub_class
|
||||||
|
end
|
||||||
|
module Config
|
||||||
|
class Service
|
||||||
|
|
||||||
|
include GRPC::GenericService
|
||||||
|
|
||||||
|
self.marshal_class_method = :encode
|
||||||
|
self.unmarshal_class_method = :decode
|
||||||
|
self.service_name = 'vagrant.proto.Config'
|
||||||
|
|
||||||
|
rpc :ConfigAttributes, Empty, ListResponse
|
||||||
|
rpc :ConfigLoad, Configuration, Configuration
|
||||||
|
rpc :ConfigValidate, Configuration, ListResponse
|
||||||
|
rpc :ConfigFinalize, Configuration, Configuration
|
||||||
|
# IO helpers for streaming (copied from Stream service)
|
||||||
|
rpc :Read, Identifier, Content
|
||||||
|
rpc :Write, Content, WriteResponse
|
||||||
|
end
|
||||||
|
|
||||||
|
Stub = Service.rpc_stub_class
|
||||||
|
end
|
||||||
|
module SyncedFolder
|
||||||
|
class Service
|
||||||
|
|
||||||
|
include GRPC::GenericService
|
||||||
|
|
||||||
|
self.marshal_class_method = :encode
|
||||||
|
self.unmarshal_class_method = :decode
|
||||||
|
self.service_name = 'vagrant.proto.SyncedFolder'
|
||||||
|
|
||||||
|
rpc :Cleanup, SyncedFolders, Empty
|
||||||
|
rpc :Disable, SyncedFolders, Empty
|
||||||
|
rpc :Enable, SyncedFolders, Empty
|
||||||
|
rpc :Info, Empty, PluginInfo
|
||||||
|
rpc :IsUsable, Machine, Valid
|
||||||
|
rpc :Name, Empty, Identifier
|
||||||
|
rpc :Prepare, SyncedFolders, Empty
|
||||||
|
# IO helpers for streaming (copied from Stream service)
|
||||||
|
rpc :Read, Identifier, Content
|
||||||
|
rpc :Write, Content, WriteResponse
|
||||||
|
# Guest capabilities helpers (copied from GuestCapabilities service)
|
||||||
|
rpc :GuestCapabilities, Empty, SystemCapabilityList
|
||||||
|
rpc :GuestCapability, GuestCapabilityRequest, GenericResponse
|
||||||
|
# Host capabilities helpers (copied from GuestCapabilities service)
|
||||||
|
rpc :HostCapabilities, Empty, SystemCapabilityList
|
||||||
|
rpc :HostCapability, HostCapabilityRequest, GenericResponse
|
||||||
|
end
|
||||||
|
|
||||||
|
Stub = Service.rpc_stub_class
|
||||||
|
end
|
||||||
|
module Provider
|
||||||
|
class Service
|
||||||
|
|
||||||
|
include GRPC::GenericService
|
||||||
|
|
||||||
|
self.marshal_class_method = :encode
|
||||||
|
self.unmarshal_class_method = :decode
|
||||||
|
self.service_name = 'vagrant.proto.Provider'
|
||||||
|
|
||||||
|
rpc :Action, GenericAction, ListResponse
|
||||||
|
rpc :Info, Empty, PluginInfo
|
||||||
|
rpc :IsInstalled, Machine, Valid
|
||||||
|
rpc :IsUsable, Machine, Valid
|
||||||
|
rpc :MachineIdChanged, Machine, Machine
|
||||||
|
rpc :Name, Empty, Identifier
|
||||||
|
rpc :RunAction, ExecuteAction, GenericResponse
|
||||||
|
rpc :SshInfo, Machine, MachineSshInfo
|
||||||
|
rpc :State, Machine, MachineState
|
||||||
|
# IO helpers for streaming (copied from Stream service)
|
||||||
|
rpc :Read, Identifier, Content
|
||||||
|
rpc :Write, Content, WriteResponse
|
||||||
|
# Config helpers (copied from Config service)
|
||||||
|
rpc :ConfigAttributes, Empty, ListResponse
|
||||||
|
rpc :ConfigLoad, Configuration, Configuration
|
||||||
|
rpc :ConfigValidate, Configuration, ListResponse
|
||||||
|
rpc :ConfigFinalize, Configuration, Configuration
|
||||||
|
# Guest capabilities helpers (copied from GuestCapabilities service)
|
||||||
|
rpc :GuestCapabilities, Empty, SystemCapabilityList
|
||||||
|
rpc :GuestCapability, GuestCapabilityRequest, GenericResponse
|
||||||
|
# Host capabilities helpers (copied from HostCapabilities service)
|
||||||
|
rpc :HostCapabilities, Empty, SystemCapabilityList
|
||||||
|
rpc :HostCapability, HostCapabilityRequest, GenericResponse
|
||||||
|
# Provider capabilities helpers (copied from ProviderCapabilities service)
|
||||||
|
rpc :ProviderCapabilities, Empty, ProviderCapabilityList
|
||||||
|
rpc :ProviderCapability, ProviderCapabilityRequest, GenericResponse
|
||||||
|
end
|
||||||
|
|
||||||
|
Stub = Service.rpc_stub_class
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
require 'logger'
|
||||||
|
|
||||||
|
# DebugIsTruncated extends the default Logger to truncate debug messages
|
||||||
|
class DebugIsTruncated < Logger
|
||||||
|
def debug(s)
|
||||||
|
super(truncate(s, 1024))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
|
||||||
|
#
|
||||||
|
# 'Once upon a time in a world far far away'.truncate(27)
|
||||||
|
# # => "Once upon a time in a wo..."
|
||||||
|
#
|
||||||
|
# Pass a string or regexp <tt>:separator</tt> to truncate +text+ at a natural break:
|
||||||
|
#
|
||||||
|
# 'Once upon a time in a world far far away'.truncate(27, separator: ' ')
|
||||||
|
# # => "Once upon a time in a..."
|
||||||
|
#
|
||||||
|
# 'Once upon a time in a world far far away'.truncate(27, separator: /\s/)
|
||||||
|
# # => "Once upon a time in a..."
|
||||||
|
#
|
||||||
|
# The last characters will be replaced with the <tt>:omission</tt> string (defaults to "...")
|
||||||
|
# for a total length not exceeding <tt>length</tt>:
|
||||||
|
#
|
||||||
|
# 'And they found that many people were sleeping better.'.truncate(25, omission: '... (continued)')
|
||||||
|
# # => "And they f... (continued)"
|
||||||
|
def truncate(s, truncate_at, options = {})
|
||||||
|
return s unless s.length > truncate_at
|
||||||
|
omission = options[:omission] || '...'
|
||||||
|
with_extra_room = truncate_at - omission.length
|
||||||
|
stop = \
|
||||||
|
if options[:separator]
|
||||||
|
rindex(options[:separator], with_extra_room) || with_extra_room
|
||||||
|
else
|
||||||
|
with_extra_room
|
||||||
|
end
|
||||||
|
"#{s[0, stop]}#{omission}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# RubyLogger defines a logger for gRPC based on the standard ruby logger.
|
||||||
|
module RubyLogger
|
||||||
|
def logger
|
||||||
|
LOGGER
|
||||||
|
end
|
||||||
|
|
||||||
|
LOGGER = DebugIsTruncated.new(STDOUT)
|
||||||
|
LOGGER.level = Logger::DEBUG
|
||||||
|
end
|
||||||
|
|
||||||
|
# GRPC is the general RPC module
|
||||||
|
module GRPC
|
||||||
|
# Inject the noop #logger if no module-level logger method has been injected.
|
||||||
|
extend RubyLogger
|
||||||
|
end
|
|
@ -578,6 +578,20 @@ module Vagrant
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def to_json(*args)
|
||||||
|
{
|
||||||
|
box: box,
|
||||||
|
config: config,
|
||||||
|
data_dir: data_dir,
|
||||||
|
environment: env,
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
provider_config: provider_config,
|
||||||
|
provider_name: provider_name
|
||||||
|
}.to_json(*args)
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# Returns the path to the file that stores the UID.
|
# Returns the path to the file that stores the UID.
|
||||||
|
|
|
@ -28,6 +28,7 @@ module Vagrant
|
||||||
|
|
||||||
@data["version"] ||= "1"
|
@data["version"] ||= "1"
|
||||||
@data["installed"] ||= {}
|
@data["installed"] ||= {}
|
||||||
|
@data["go_plugin"] ||= {}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add a plugin that is installed to the state file.
|
# Add a plugin that is installed to the state file.
|
||||||
|
@ -47,6 +48,43 @@ module Vagrant
|
||||||
save!
|
save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Add a go plugin that is installed to the state file
|
||||||
|
#
|
||||||
|
# @param [String, Symbol] name Plugin name
|
||||||
|
def add_go_plugin(name, **opts)
|
||||||
|
@data["go_plugin"][name] = {
|
||||||
|
"source" => opts[:source]
|
||||||
|
}
|
||||||
|
|
||||||
|
save!
|
||||||
|
end
|
||||||
|
|
||||||
|
# Remove a go plugin that is installed from the state file
|
||||||
|
#
|
||||||
|
# @param [String, Symbol] name Name of the plugin
|
||||||
|
def remove_go_plugin(name)
|
||||||
|
@data["go_plugin"].delete(name.to_s)
|
||||||
|
|
||||||
|
save!
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if go plugin is installed from the state file
|
||||||
|
#
|
||||||
|
# @param [String, Symbol] name Plugin name
|
||||||
|
# @return [Boolean]
|
||||||
|
def has_go_plugin?(name)
|
||||||
|
@data["go_plugin"].key?(name.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This returns a hash of installed go plugins according to the state
|
||||||
|
# file. Note that this may _not_ directly match over to actually
|
||||||
|
# installed plugins
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def installed_go_plugins
|
||||||
|
@data["go_plugin"]
|
||||||
|
end
|
||||||
|
|
||||||
# Adds a RubyGems index source to look up gems.
|
# Adds a RubyGems index source to look up gems.
|
||||||
#
|
#
|
||||||
# @param [String] url URL of the source.
|
# @param [String] url URL of the source.
|
||||||
|
|
|
@ -138,6 +138,12 @@ module Vagrant
|
||||||
@__finalized = true
|
@__finalized = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class Set < ::Set
|
||||||
|
def to_json(*args)
|
||||||
|
self.to_a.to_json(*args)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,9 @@ module Vagrant
|
||||||
autoload :DeepMerge, 'vagrant/util/deep_merge'
|
autoload :DeepMerge, 'vagrant/util/deep_merge'
|
||||||
autoload :Env, 'vagrant/util/env'
|
autoload :Env, 'vagrant/util/env'
|
||||||
autoload :HashWithIndifferentAccess, 'vagrant/util/hash_with_indifferent_access'
|
autoload :HashWithIndifferentAccess, 'vagrant/util/hash_with_indifferent_access'
|
||||||
|
autoload :Experimental, 'vagrant/util/experimental'
|
||||||
autoload :GuestInspection, 'vagrant/util/guest_inspection'
|
autoload :GuestInspection, 'vagrant/util/guest_inspection'
|
||||||
|
autoload :Logger, 'vagrant/util/logger'
|
||||||
autoload :LoggingFormatter, 'vagrant/util/logging_formatter'
|
autoload :LoggingFormatter, 'vagrant/util/logging_formatter'
|
||||||
autoload :Platform, 'vagrant/util/platform'
|
autoload :Platform, 'vagrant/util/platform'
|
||||||
autoload :Retryable, 'vagrant/util/retryable'
|
autoload :Retryable, 'vagrant/util/retryable'
|
||||||
|
|
|
@ -14,6 +14,9 @@ module Vagrant
|
||||||
super(&block)
|
super(&block)
|
||||||
|
|
||||||
hash.each do |key, value|
|
hash.each do |key, value|
|
||||||
|
if value.is_a?(Hash) && !value.is_a?(self.class)
|
||||||
|
value = self.class.new(value)
|
||||||
|
end
|
||||||
self[convert_key(key)] = value
|
self[convert_key(key)] = value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
require "log4r"
|
||||||
|
|
||||||
|
module Vagrant
|
||||||
|
module Util
|
||||||
|
# Adds a logger method which provides automatically
|
||||||
|
# namespaced logger instance
|
||||||
|
module Logger
|
||||||
|
|
||||||
|
# @return [Log4r::Logger]
|
||||||
|
def logger
|
||||||
|
if !@_logger
|
||||||
|
name = (self.is_a?(Module) ? self : self.class).name.downcase
|
||||||
|
if !name.start_with?("vagrant")
|
||||||
|
name = "vagrant::root::#{name}"
|
||||||
|
end
|
||||||
|
@_logger = Log4r::Logger.new(name)
|
||||||
|
end
|
||||||
|
@_logger
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -74,6 +74,23 @@ module VagrantPlugins
|
||||||
autoload :RepairPluginsLocal, action_root.join("repair_plugins")
|
autoload :RepairPluginsLocal, action_root.join("repair_plugins")
|
||||||
autoload :UninstallPlugin, action_root.join("uninstall_plugin")
|
autoload :UninstallPlugin, action_root.join("uninstall_plugin")
|
||||||
autoload :UpdateGems, action_root.join("update_gems")
|
autoload :UpdateGems, action_root.join("update_gems")
|
||||||
|
|
||||||
|
Vagrant::Util::Experimental.guard_with(:go_plugin) do
|
||||||
|
autoload :UninstallGoPlugin, action_root.join("uninstall_go_plugin")
|
||||||
|
autoload :InstallGoPlugin, action_root.join("install_go_plugin")
|
||||||
|
|
||||||
|
def self.action_go_install
|
||||||
|
Vagrant::Action::Builder.new.tap do |b|
|
||||||
|
b.use InstallGoPlugin
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.action_go_uninstall
|
||||||
|
Vagrant::Action::Builder.new.tap do |b|
|
||||||
|
b.use UninstallGoPlugin
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
require "log4r"
|
||||||
|
require "vagrant/go_plugin/manager"
|
||||||
|
|
||||||
|
module VagrantPlugins
|
||||||
|
module CommandPlugin
|
||||||
|
module Action
|
||||||
|
class InstallGoPlugin
|
||||||
|
def initialize(app, env)
|
||||||
|
@app = app
|
||||||
|
@logger = Log4r::Logger.new("vagrant::plugins::plugincommand::installgoplugin")
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
plugin_name = env[:plugin_name]
|
||||||
|
plugin_source = env[:plugin_source]
|
||||||
|
|
||||||
|
manager = Vagrant::GoPlugin::Manager.instance
|
||||||
|
|
||||||
|
env[:ui].info(I18n.t("vagrant.commands.plugin.installing",
|
||||||
|
name: plugin_name))
|
||||||
|
|
||||||
|
manager.install_plugin(plugin_name, plugin_source)
|
||||||
|
|
||||||
|
# Tell the user
|
||||||
|
env[:ui].success(I18n.t("vagrant.commands.plugin.installed",
|
||||||
|
name: plugin_name,
|
||||||
|
version: plugin_source))
|
||||||
|
|
||||||
|
# Continue
|
||||||
|
@app.call(env)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,22 @@
|
||||||
|
module VagrantPlugins
|
||||||
|
module CommandPlugin
|
||||||
|
module Action
|
||||||
|
class UninstallGoPlugin
|
||||||
|
def initialize(app, env)
|
||||||
|
@app = app
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
# Remove it!
|
||||||
|
env[:ui].info(I18n.t("vagrant.commands.plugin.uninstalling",
|
||||||
|
name: env[:plugin_name]))
|
||||||
|
|
||||||
|
manager = Vagrant::GoPlugin::Manager.instance
|
||||||
|
manager.uninstall_plugin(env[:plugin_name])
|
||||||
|
|
||||||
|
@app.call(env)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,43 @@
|
||||||
|
require 'optparse'
|
||||||
|
|
||||||
|
require_relative "base"
|
||||||
|
|
||||||
|
module VagrantPlugins
|
||||||
|
module CommandPlugin
|
||||||
|
module Command
|
||||||
|
class GoInstall < Base
|
||||||
|
|
||||||
|
def execute
|
||||||
|
options = { verbose: false }
|
||||||
|
|
||||||
|
opts = OptionParser.new do |o|
|
||||||
|
o.banner = "Usage: vagrant plugin goinstall <name> <source>"
|
||||||
|
o.separator ""
|
||||||
|
|
||||||
|
o.on("--verbose", "Enable verbose output for plugin installation") do |v|
|
||||||
|
options[:verbose] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parse the options
|
||||||
|
argv = parse_options(opts)
|
||||||
|
return if !argv
|
||||||
|
|
||||||
|
if argv.length != 2
|
||||||
|
raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
plugin_name, plugin_source = argv
|
||||||
|
|
||||||
|
action(Action.action_go_install,
|
||||||
|
plugin_name: plugin_name,
|
||||||
|
plugin_source: plugin_source
|
||||||
|
)
|
||||||
|
|
||||||
|
# Success, exit status 0
|
||||||
|
0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,31 @@
|
||||||
|
require 'optparse'
|
||||||
|
|
||||||
|
require_relative "base"
|
||||||
|
|
||||||
|
module VagrantPlugins
|
||||||
|
module CommandPlugin
|
||||||
|
module Command
|
||||||
|
class GoUninstall < Base
|
||||||
|
def execute
|
||||||
|
options = {}
|
||||||
|
opts = OptionParser.new do |o|
|
||||||
|
o.banner = "Usage: vagrant plugin gouninstall <name> [<name2> <name3> ...] [-h]"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parse the options
|
||||||
|
argv = parse_options(opts)
|
||||||
|
return if !argv
|
||||||
|
raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length < 1
|
||||||
|
|
||||||
|
# Uninstall the plugins
|
||||||
|
argv.each do |entry|
|
||||||
|
action(Action.action_go_uninstall, plugin_name: entry)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Success, exit status 0
|
||||||
|
0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -48,6 +48,18 @@ module VagrantPlugins
|
||||||
require_relative "uninstall"
|
require_relative "uninstall"
|
||||||
Uninstall
|
Uninstall
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Vagrant::Util::Experimental.guard_with(:go_plugin) do
|
||||||
|
@subcommands.register(:goinstall) do
|
||||||
|
require_relative "go_install"
|
||||||
|
GoInstall
|
||||||
|
end
|
||||||
|
|
||||||
|
@subcommands.register(:gouninstall) do
|
||||||
|
require_relative "go_uninstall"
|
||||||
|
GoUninstall
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
|
|
|
@ -30,6 +30,9 @@ describe Vagrant::CLI do
|
||||||
describe "#execute" do
|
describe "#execute" do
|
||||||
let(:triggers) { double("triggers") }
|
let(:triggers) { double("triggers") }
|
||||||
|
|
||||||
|
before { allow(Vagrant::Util::Experimental).to receive(:feature_enabled?) }
|
||||||
|
|
||||||
|
|
||||||
it "invokes help and exits with 1 if invalid command" do
|
it "invokes help and exits with 1 if invalid command" do
|
||||||
subject = described_class.new(["i-dont-exist"], env)
|
subject = described_class.new(["i-dont-exist"], env)
|
||||||
expect(subject).to receive(:help).once
|
expect(subject).to receive(:help).once
|
||||||
|
@ -59,7 +62,7 @@ describe Vagrant::CLI do
|
||||||
|
|
||||||
it "fires triggers, if enabled" do
|
it "fires triggers, if enabled" do
|
||||||
allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).
|
allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).
|
||||||
with("typed_triggers").and_return("true")
|
with("typed_triggers").and_return(true)
|
||||||
allow(triggers).to receive(:fire_triggers)
|
allow(triggers).to receive(:fire_triggers)
|
||||||
|
|
||||||
commands[:destroy] = [command_lambda("destroy", 42), {}]
|
commands[:destroy] = [command_lambda("destroy", 42), {}]
|
||||||
|
@ -76,7 +79,7 @@ describe Vagrant::CLI do
|
||||||
|
|
||||||
it "does not fire triggers if disabled" do
|
it "does not fire triggers if disabled" do
|
||||||
allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).
|
allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).
|
||||||
with("typed_triggers").and_return("false")
|
with("typed_triggers").and_return(false)
|
||||||
|
|
||||||
commands[:destroy] = [command_lambda("destroy", 42), {}]
|
commands[:destroy] = [command_lambda("destroy", 42), {}]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
require_relative "../../base"
|
||||||
|
|
||||||
|
describe Vagrant::GoPlugin::CapabilityPlugin do
|
||||||
|
let(:client) { double("client") }
|
||||||
|
|
||||||
|
describe Vagrant::GoPlugin::CapabilityPlugin::Capability do
|
||||||
|
it "should be a GRPCPlugin" do
|
||||||
|
expect(described_class.ancestors).to include(Vagrant::GoPlugin::GRPCPlugin)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".generate_guest_capabilities" do
|
||||||
|
let(:caps) {
|
||||||
|
[{platform: "dummy", name: "stub_cap"},
|
||||||
|
{platform: "dummy", name: "other_cap"}]}
|
||||||
|
let(:cap_response) {
|
||||||
|
Vagrant::Proto::SystemCapabilityList.new(
|
||||||
|
capabilities: caps.map { |i|
|
||||||
|
Vagrant::Proto::SystemCapability.new(i)})}
|
||||||
|
let(:plugin_klass) { double("plugin_klass") }
|
||||||
|
let(:plugin_type) { :testing }
|
||||||
|
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(client).to receive(:guest_capabilities).
|
||||||
|
and_return(cap_response)
|
||||||
|
allow(plugin_klass).to receive(:guest_capability)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should generate two new capability classes" do
|
||||||
|
expect(Class).to receive(:new).twice.
|
||||||
|
with(Vagrant::GoPlugin::CapabilityPlugin::Capability).
|
||||||
|
and_call_original
|
||||||
|
described_class.generate_guest_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should create capability name methods" do
|
||||||
|
c1 = Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability)
|
||||||
|
c2 = Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability)
|
||||||
|
expect(Class).to receive(:new).and_return(c1)
|
||||||
|
expect(Class).to receive(:new).and_return(c2)
|
||||||
|
described_class.generate_guest_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
expect(c1).to respond_to(:stub_cap)
|
||||||
|
expect(c2).to respond_to(:other_cap)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should register guest capability" do
|
||||||
|
expect(plugin_klass).to receive(:guest_capability).with(:dummy, :stub_cap)
|
||||||
|
expect(plugin_klass).to receive(:guest_capability).with(:dummy, :other_cap)
|
||||||
|
described_class.generate_guest_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "generated capability" do
|
||||||
|
let(:cap_class) { Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability) }
|
||||||
|
let(:response) { double(result: result) }
|
||||||
|
let(:result) { nil }
|
||||||
|
|
||||||
|
before {
|
||||||
|
expect(Class).to receive(:new).and_return(cap_class)
|
||||||
|
allow(Class).to receive(:new).and_call_original
|
||||||
|
described_class.generate_guest_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "should call guest_capability on the plugin client" do
|
||||||
|
expect(client).to receive(:guest_capability).and_return(response)
|
||||||
|
cap_class.stub_cap(nil, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when result is a hash" do
|
||||||
|
let(:result) { "{}" }
|
||||||
|
|
||||||
|
it "should convert hash to hash with indifferent access" do
|
||||||
|
expect(client).to receive(:guest_capability).and_return(response)
|
||||||
|
expect(cap_class.stub_cap(nil, nil)).to be_a(Vagrant::Util::HashWithIndifferentAccess)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".generate_host_capabilities" do
|
||||||
|
let(:caps) {
|
||||||
|
[{platform: "dummy", name: "stub_cap"},
|
||||||
|
{platform: "dummy", name: "other_cap"}]}
|
||||||
|
let(:cap_response) {
|
||||||
|
Vagrant::Proto::SystemCapabilityList.new(
|
||||||
|
capabilities: caps.map { |i|
|
||||||
|
Vagrant::Proto::SystemCapability.new(i)})}
|
||||||
|
let(:plugin_klass) { double("plugin_klass") }
|
||||||
|
let(:plugin_type) { :testing }
|
||||||
|
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(client).to receive(:host_capabilities).
|
||||||
|
and_return(cap_response)
|
||||||
|
allow(plugin_klass).to receive(:host_capability)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should generate two new capability classes" do
|
||||||
|
expect(Class).to receive(:new).twice.
|
||||||
|
with(Vagrant::GoPlugin::CapabilityPlugin::Capability).
|
||||||
|
and_call_original
|
||||||
|
described_class.generate_host_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should create capability name methods" do
|
||||||
|
c1 = Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability)
|
||||||
|
c2 = Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability)
|
||||||
|
expect(Class).to receive(:new).and_return(c1)
|
||||||
|
expect(Class).to receive(:new).and_return(c2)
|
||||||
|
described_class.generate_host_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
expect(c1).to respond_to(:stub_cap)
|
||||||
|
expect(c2).to respond_to(:other_cap)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should register host capability" do
|
||||||
|
expect(plugin_klass).to receive(:host_capability).with(:dummy, :stub_cap)
|
||||||
|
expect(plugin_klass).to receive(:host_capability).with(:dummy, :other_cap)
|
||||||
|
described_class.generate_host_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "generated capability" do
|
||||||
|
let(:cap_class) { Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability) }
|
||||||
|
let(:response) { double(result: result) }
|
||||||
|
let(:result) { nil }
|
||||||
|
|
||||||
|
before {
|
||||||
|
expect(Class).to receive(:new).and_return(cap_class)
|
||||||
|
allow(Class).to receive(:new).and_call_original
|
||||||
|
described_class.generate_host_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "should call guest_capability on the plugin client" do
|
||||||
|
expect(client).to receive(:host_capability).and_return(response)
|
||||||
|
cap_class.stub_cap(nil, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when result is a hash" do
|
||||||
|
let(:result) { "{}" }
|
||||||
|
|
||||||
|
it "should convert hash to hash with indifferent access" do
|
||||||
|
expect(client).to receive(:host_capability).and_return(response)
|
||||||
|
expect(cap_class.stub_cap(nil, nil)).to be_a(Vagrant::Util::HashWithIndifferentAccess)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".generate_provider_capabilities" do
|
||||||
|
let(:caps) {
|
||||||
|
[{provider: "dummy", name: "stub_cap"},
|
||||||
|
{provider: "dummy", name: "other_cap"}]}
|
||||||
|
let(:cap_response) {
|
||||||
|
Vagrant::Proto::ProviderCapabilityList.new(
|
||||||
|
capabilities: caps.map { |i|
|
||||||
|
Vagrant::Proto::ProviderCapability.new(i)})}
|
||||||
|
let(:plugin_klass) { double("plugin_klass") }
|
||||||
|
let(:plugin_type) { :testing }
|
||||||
|
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(client).to receive(:provider_capabilities).
|
||||||
|
and_return(cap_response)
|
||||||
|
allow(plugin_klass).to receive(:provider_capability)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should generate two new capability classes" do
|
||||||
|
expect(Class).to receive(:new).twice.
|
||||||
|
with(Vagrant::GoPlugin::CapabilityPlugin::Capability).
|
||||||
|
and_call_original
|
||||||
|
described_class.generate_provider_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should create capability name methods" do
|
||||||
|
c1 = Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability)
|
||||||
|
c2 = Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability)
|
||||||
|
expect(Class).to receive(:new).and_return(c1)
|
||||||
|
expect(Class).to receive(:new).and_return(c2)
|
||||||
|
described_class.generate_provider_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
expect(c1).to respond_to(:stub_cap)
|
||||||
|
expect(c2).to respond_to(:other_cap)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should register provider capability" do
|
||||||
|
expect(plugin_klass).to receive(:provider_capability).with(:dummy, :stub_cap)
|
||||||
|
expect(plugin_klass).to receive(:provider_capability).with(:dummy, :other_cap)
|
||||||
|
described_class.generate_provider_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "generated capability" do
|
||||||
|
let(:cap_class) { Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability) }
|
||||||
|
let(:response) { double(result: result) }
|
||||||
|
let(:result) { nil }
|
||||||
|
|
||||||
|
before {
|
||||||
|
expect(Class).to receive(:new).and_return(cap_class)
|
||||||
|
allow(Class).to receive(:new).and_call_original
|
||||||
|
described_class.generate_provider_capabilities(client, plugin_klass, plugin_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "should call guest_capability on the plugin client" do
|
||||||
|
expect(client).to receive(:provider_capability).and_return(response)
|
||||||
|
cap_class.stub_cap(nil, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when result is a hash" do
|
||||||
|
let(:result) { "{}" }
|
||||||
|
|
||||||
|
it "should convert hash to hash with indifferent access" do
|
||||||
|
expect(client).to receive(:provider_capability).and_return(response)
|
||||||
|
expect(cap_class.stub_cap(nil, nil)).to be_a(Vagrant::Util::HashWithIndifferentAccess)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,141 @@
|
||||||
|
require_relative "../../base"
|
||||||
|
|
||||||
|
describe Vagrant::GoPlugin::ConfigPlugin do
|
||||||
|
let(:client) { double("client") }
|
||||||
|
|
||||||
|
describe ".generate_config" do
|
||||||
|
let(:parent_name) { "test" }
|
||||||
|
let(:parent_klass) { double("parent_klass") }
|
||||||
|
let(:parent_type) { "dummy" }
|
||||||
|
let(:attributes_response) {
|
||||||
|
double("attributes_response",
|
||||||
|
items: attributes)
|
||||||
|
}
|
||||||
|
let(:attributes) { [] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(parent_klass).to receive(:config)
|
||||||
|
allow(client).to receive(:config_attributes).
|
||||||
|
and_return(attributes_response)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should register a new config class" do
|
||||||
|
expect(parent_klass).to receive(:config).
|
||||||
|
with(parent_name, parent_type) { |&block|
|
||||||
|
expect(block.call.ancestors).to include(Vagrant::GoPlugin::ConfigPlugin::Config)
|
||||||
|
}
|
||||||
|
described_class.generate_config(client, parent_name, parent_klass, parent_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should register the client within the class" do
|
||||||
|
expect(parent_klass).to receive(:config).
|
||||||
|
with(parent_name, parent_type) { |&block|
|
||||||
|
expect(block.call.plugin_client).to eq(client)
|
||||||
|
}
|
||||||
|
described_class.generate_config(client, parent_name, parent_klass, parent_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when attributes are provided" do
|
||||||
|
let(:attributes) { ["a_one", "a_two"] }
|
||||||
|
|
||||||
|
it "should add accessor instance methods to the class" do
|
||||||
|
expect(parent_klass).to receive(:config).
|
||||||
|
with(parent_name, parent_type) { |&block|
|
||||||
|
expect(block.call.instance_methods).to include(:a_one)
|
||||||
|
expect(block.call.instance_methods).to include(:a_two)
|
||||||
|
}
|
||||||
|
described_class.generate_config(client, parent_name, parent_klass, parent_type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Vagrant::GoPlugin::ConfigPlugin::Config do
|
||||||
|
let(:machine) { double("machine") }
|
||||||
|
let(:subject) {
|
||||||
|
c = Class.new(described_class)
|
||||||
|
c.plugin_client = client
|
||||||
|
c.new
|
||||||
|
}
|
||||||
|
|
||||||
|
describe "#validate" do
|
||||||
|
let(:response) { double("response", items: errors) }
|
||||||
|
let(:errors) { ["errors"] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(machine).to receive(:to_json).and_return("{}")
|
||||||
|
allow(client).to receive(:config_validate).and_return(response)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should call client to validate" do
|
||||||
|
expect(client).to receive(:config_validate).
|
||||||
|
with(instance_of(Vagrant::Proto::Configuration)).
|
||||||
|
and_return(response)
|
||||||
|
subject.validate(machine)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return list of validation errors" do
|
||||||
|
expect(subject.validate(machine)).to eq(errors)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#finalize!" do
|
||||||
|
let(:response) { double("response", data: result) }
|
||||||
|
let(:result) { "null" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(client).to receive(:config_finalize).and_return(response)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return the config instance" do
|
||||||
|
expect(subject.finalize!).to eq(subject)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should call client to finalize" do
|
||||||
|
expect(client).to receive(:config_finalize).
|
||||||
|
with(instance_of(Vagrant::Proto::Configuration)).
|
||||||
|
and_return(response)
|
||||||
|
subject.finalize!
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when configuration data is returned" do
|
||||||
|
let(:result) {
|
||||||
|
{attr1: true, attr2: "value"}.to_json
|
||||||
|
}
|
||||||
|
|
||||||
|
it "should create accessor methods to configuration data" do
|
||||||
|
subject.finalize!
|
||||||
|
expect(subject).to respond_to(:attr1)
|
||||||
|
expect(subject).to respond_to(:attr2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return data from reader methods" do
|
||||||
|
subject.finalize!
|
||||||
|
expect(subject.attr1).to eq(true)
|
||||||
|
expect(subject.attr2).to eq("value")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#local_data" do
|
||||||
|
it "should return an empty hash" do
|
||||||
|
expect(subject.local_data).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with config attributes set" do
|
||||||
|
let(:response) { double("response", data: result) }
|
||||||
|
let(:result) { {attr1: true, attr2: "value"}.to_json }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(client).to receive(:config_finalize).and_return(response)
|
||||||
|
subject.finalize!
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return data values" do
|
||||||
|
result = subject.local_data
|
||||||
|
expect(result[:attr1]).to eq(true)
|
||||||
|
expect(result[:attr2]).to eq("value")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,41 @@
|
||||||
|
require_relative "../../base"
|
||||||
|
|
||||||
|
describe Vagrant::GoPlugin::GRPCPlugin do
|
||||||
|
let(:client) { double("client") }
|
||||||
|
let(:subject) { Class.new.tap { |c| c.include(described_class) } }
|
||||||
|
|
||||||
|
describe ".plugin_client=" do
|
||||||
|
it "should set the plugin client" do
|
||||||
|
expect(subject.plugin_client = client).to eq(client)
|
||||||
|
expect(subject.plugin_client).to eq(client)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should error if plugin is already set" do
|
||||||
|
subject.plugin_client = client
|
||||||
|
expect { subject.plugin_client = client }.
|
||||||
|
to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".plugin_client" do
|
||||||
|
it "should return nil when client has not been set" do
|
||||||
|
expect(subject.plugin_client).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return client when it has been set" do
|
||||||
|
subject.plugin_client = client
|
||||||
|
expect(subject.plugin_client).to eq(client)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#plugin_client" do
|
||||||
|
it "should be nil when client has not been set" do
|
||||||
|
expect(subject.new.plugin_client).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return client when client has been set" do
|
||||||
|
subject.plugin_client = client
|
||||||
|
expect(subject.new.plugin_client).to eq(client)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,56 @@
|
||||||
|
require_relative "../../base"
|
||||||
|
|
||||||
|
describe Vagrant::GoPlugin::Interface do
|
||||||
|
before do
|
||||||
|
allow_any_instance_of(described_class).to receive(:_setup)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#load_plugins" do
|
||||||
|
let(:path) { double("path", to_s: "path") }
|
||||||
|
|
||||||
|
it "should raise error if path is not a directory" do
|
||||||
|
expect(File).to receive(:directory?).with(path.to_s).and_return(false)
|
||||||
|
expect { subject.load_plugins(path) }.to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should load plugins if path is a directory" do
|
||||||
|
expect(File).to receive(:directory?).with(path.to_s).and_return(true)
|
||||||
|
expect(subject).to receive(:_load_plugins).with(path.to_s)
|
||||||
|
subject.load_plugins(path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#register_plugins" do
|
||||||
|
it "should load Provider and SyncedFolder plugins" do
|
||||||
|
expect(subject).to receive(:load_providers)
|
||||||
|
expect(subject).to receive(:load_synced_folders)
|
||||||
|
subject.register_plugins
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#setup" do
|
||||||
|
after { subject }
|
||||||
|
|
||||||
|
it "should register at_exit action" do
|
||||||
|
expect(Kernel).to receive(:at_exit)
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should run the setup action" do
|
||||||
|
expect_any_instance_of(described_class).to receive(:_setup)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should only run the setup process once" do
|
||||||
|
expect_any_instance_of(described_class).to receive(:_setup).once
|
||||||
|
expect(subject.logger).to receive(:warn)
|
||||||
|
subject.setup
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#teardown" do
|
||||||
|
it "should run the teardown action" do
|
||||||
|
expect(subject).to receive(:_teardown)
|
||||||
|
subject.teardown
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,148 @@
|
||||||
|
require_relative "../../base"
|
||||||
|
|
||||||
|
describe Vagrant::GoPlugin::Manager do
|
||||||
|
include_context "unit"
|
||||||
|
|
||||||
|
let(:env) do
|
||||||
|
test_env.vagrantfile("")
|
||||||
|
test_env.create_vagrant_env
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(FileUtils).to receive(:mkdir_p)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".instance" do
|
||||||
|
it "should return instance of manager" do
|
||||||
|
expect(described_class.instance).to be_a(described_class)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should cache instance" do
|
||||||
|
expect(described_class.instance).to be(described_class.instance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".new" do
|
||||||
|
it "should create the installation directory" do
|
||||||
|
expect(FileUtils).to receive(:mkdir_p).with(Vagrant::GoPlugin::INSTALL_DIRECTORY)
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should create installation temporary directory" do
|
||||||
|
expect(FileUtils).to receive(:mkdir_p).with(/tmp$/)
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should generate user state file" do
|
||||||
|
expect(subject.user_file).to be_a(Vagrant::Plugin::StateFile)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#globalize!" do
|
||||||
|
let(:entries) { [double("entry1"), double("entry2")] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(File).to receive(:directory?).and_return(false)
|
||||||
|
allow(Dir).to receive(:glob).and_return(entries)
|
||||||
|
allow(Vagrant::GoPlugin).to receive_message_chain(:interface, :register_plugins)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when entries are not directories" do
|
||||||
|
before { allow(File).to receive(:directory?).and_return(false) }
|
||||||
|
|
||||||
|
it "should not load any plugins" do
|
||||||
|
interface = double("interface", register_plugins: nil)
|
||||||
|
allow(Vagrant::GoPlugin).to receive(:interface).and_return(interface)
|
||||||
|
expect(interface).not_to receive(:load_plugins)
|
||||||
|
subject.globalize!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when entries are directories" do
|
||||||
|
before { allow(File).to receive(:directory?).and_return(true) }
|
||||||
|
|
||||||
|
it "should load all entries" do
|
||||||
|
expect(Vagrant::GoPlugin).to receive_message_chain(:interface, :load_plugins).with(entries.first)
|
||||||
|
expect(Vagrant::GoPlugin).to receive_message_chain(:interface, :load_plugins).with(entries.last)
|
||||||
|
subject.globalize!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should register plugins after loading" do
|
||||||
|
expect(Vagrant::GoPlugin).to receive_message_chain(:interface, :register_plugins)
|
||||||
|
subject.globalize!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#localize!" do
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#install_plugin" do
|
||||||
|
let(:plugin_name) { "test_plugin_name" }
|
||||||
|
let(:remote_source) { double("remote_source") }
|
||||||
|
let(:downloader) { double("downloader", download!: nil) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(FileUtils).to receive(:mkdir_p)
|
||||||
|
allow(Dir).to receive(:mktmpdir)
|
||||||
|
allow(Vagrant::Util::Downloader).to receive(:new).and_return(downloader)
|
||||||
|
allow(Zip::File).to receive(:open)
|
||||||
|
end
|
||||||
|
|
||||||
|
after { subject.install_plugin(plugin_name, remote_source) }
|
||||||
|
|
||||||
|
it "should create plugin directory for plugin name" do
|
||||||
|
expect(FileUtils).to receive(:mkdir_p).with(/test_plugin_name$/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should create a temporary directory to download and unpack" do
|
||||||
|
expect(Dir).to receive(:mktmpdir).with(/go-plugin/, any_args)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should download the remote file" do
|
||||||
|
expect(Dir).to receive(:mktmpdir).with(any_args).and_yield("tmpdir")
|
||||||
|
expect(downloader).to receive(:download!)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should unzip the downloaded file" do
|
||||||
|
expect(Dir).to receive(:mktmpdir).with(any_args).and_yield("tmpdir")
|
||||||
|
expect(Zip::File).to receive(:open).with(/plugin.zip/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should add the plugin to the user file" do
|
||||||
|
expect(subject.user_file).to receive(:add_go_plugin).and_call_original
|
||||||
|
expect(subject.user_file.has_go_plugin?("test_plugin_name")).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#uninstall_plugin" do
|
||||||
|
let(:plugin_name) { "test_plugin_name" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(File).to receive(:directory?).and_call_original
|
||||||
|
allow(FileUtils).to receive(:rm_rf)
|
||||||
|
end
|
||||||
|
|
||||||
|
after { subject.uninstall_plugin(plugin_name) }
|
||||||
|
|
||||||
|
it "should remove plugin path when installed" do
|
||||||
|
expect(File).to receive(:directory?).with(/test_plugin_name/).and_return(true)
|
||||||
|
expect(FileUtils).to receive(:rm_rf).with(/test_plugin_name/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should not remove plugin path when not installed" do
|
||||||
|
expect(File).to receive(:directory?).with(/test_plugin_name/).and_return(false)
|
||||||
|
expect(FileUtils).not_to receive(:rm_rf).with(/test_plugin_name/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should have plugin name removed from user file when installed" do
|
||||||
|
expect(File).to receive(:directory?).with(/test_plugin_name/).and_return(true)
|
||||||
|
expect(subject.user_file).to receive(:remove_go_plugin).with(plugin_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should have plugin name removed from user file when not installed" do
|
||||||
|
expect(File).to receive(:directory?).with(/test_plugin_name/).and_return(false)
|
||||||
|
expect(subject.user_file).to receive(:remove_go_plugin).with(plugin_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,273 @@
|
||||||
|
require_relative "../../base"
|
||||||
|
|
||||||
|
describe Vagrant::GoPlugin::ProviderPlugin do
|
||||||
|
let(:client) { double("client") }
|
||||||
|
|
||||||
|
describe Vagrant::GoPlugin::ProviderPlugin::Action do
|
||||||
|
let(:described_class) { Class.new(Vagrant::GoPlugin::ProviderPlugin::Action) }
|
||||||
|
|
||||||
|
it "should be a GRPCPlugin" do
|
||||||
|
expect(described_class.ancestors).to include(Vagrant::GoPlugin::GRPCPlugin)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".action_name" do
|
||||||
|
it "should return nil when unset" do
|
||||||
|
expect(described_class.action_name).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return defined action name when set" do
|
||||||
|
described_class.action_name = "test_action"
|
||||||
|
expect(described_class.action_name).to eq("test_action")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".action_name=" do
|
||||||
|
it "should convert action name to string" do
|
||||||
|
described_class.action_name = :test_action
|
||||||
|
expect(described_class.action_name).to be_a(String)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should set the action name" do
|
||||||
|
described_class.action_name = "test_action"
|
||||||
|
expect(described_class.action_name).to eq("test_action")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should error if action name is already set" do
|
||||||
|
described_class.action_name = "test_action"
|
||||||
|
expect { described_class.action_name = "test_action" }.
|
||||||
|
to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should freeze action name" do
|
||||||
|
described_class.action_name = "test_action"
|
||||||
|
expect(described_class.action_name).to be_frozen
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#call" do
|
||||||
|
let(:app) { double("app") }
|
||||||
|
let(:env) { {"key1" => "value1", "key2" => "value2"} }
|
||||||
|
let(:response_env) {
|
||||||
|
{"key1" => "value1", "key2" => "value2"}
|
||||||
|
}
|
||||||
|
let(:response) { double("response", result: response_env.to_json) }
|
||||||
|
|
||||||
|
let(:subject) {
|
||||||
|
described_class.plugin_client = client
|
||||||
|
described_class.new(app, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
before do
|
||||||
|
described_class.action_name = "test_action"
|
||||||
|
allow(app).to receive(:call)
|
||||||
|
allow(client).to receive(:run_action).and_return(response)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should call the next item in the middleware" do
|
||||||
|
expect(app).to receive(:call)
|
||||||
|
subject.call(env)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should call the client" do
|
||||||
|
expect(client).to receive(:run_action).and_return(response)
|
||||||
|
subject.call(env)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when new data is provided in result" do
|
||||||
|
let(:response_env) { {"new_data" => "value"} }
|
||||||
|
|
||||||
|
it "should include new data when calling next action" do
|
||||||
|
expect(app).to receive(:call).
|
||||||
|
with(hash_including("new_data" => "value", "key1" => "value1"))
|
||||||
|
subject.call(env)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Vagrant::GoPlugin::ProviderPlugin::Provider do
|
||||||
|
let(:described_class) {
|
||||||
|
Class.new(Vagrant::GoPlugin::ProviderPlugin::Provider).tap { |c|
|
||||||
|
c.plugin_client = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let(:machine) { double("machine") }
|
||||||
|
let(:subject) { described_class.new(machine) }
|
||||||
|
|
||||||
|
it "should be a GRPCPlugin" do
|
||||||
|
expect(described_class.ancestors).to include(Vagrant::GoPlugin::GRPCPlugin)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#name" do
|
||||||
|
let(:name) { double("name") }
|
||||||
|
let(:response) { double("response", name: name) }
|
||||||
|
|
||||||
|
before { allow(client).to receive(:name).and_return(response) }
|
||||||
|
|
||||||
|
it "should request name from the client" do
|
||||||
|
expect(client).to receive(:name).and_return(response)
|
||||||
|
subject.name
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return the plugin name" do
|
||||||
|
expect(subject.name).to eq(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should only call the client once" do
|
||||||
|
expect(client).to receive(:name).once.and_return(response)
|
||||||
|
subject.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#action" do
|
||||||
|
let(:action_name) { "test_action" }
|
||||||
|
let(:response) { double("response", items: actions) }
|
||||||
|
let(:actions) { ["self::TestAction"] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(client).to receive(:action).and_return(response)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return a builder instance" do
|
||||||
|
expect(subject.action(action_name)).to be_a(Vagrant::Action::Builder)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should call the plugin client" do
|
||||||
|
expect(client).to receive(:action).
|
||||||
|
with(instance_of(Vagrant::Proto::GenericAction)).
|
||||||
|
and_return(response)
|
||||||
|
subject.action(action_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should create a new custom action class" do
|
||||||
|
builder = subject.action(action_name)
|
||||||
|
action = builder.stack.first.first
|
||||||
|
expect(action.ancestors).to include(Vagrant::GoPlugin::ProviderPlugin::Action)
|
||||||
|
expect(action.action_name).to eq("TestAction")
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when given non-local action name" do
|
||||||
|
let(:actions) { ["Vagrant::Action::Builtin::Call"] }
|
||||||
|
|
||||||
|
it "should load the existing action class" do
|
||||||
|
builder = subject.action(action_name)
|
||||||
|
action = builder.stack.first.first
|
||||||
|
expect(action).to eq(Vagrant::Action::Builtin::Call)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#capability" do
|
||||||
|
let(:cap_name) { "test_cap" }
|
||||||
|
let(:response) { double("response", result: result.to_json) }
|
||||||
|
let(:result) { nil }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(subject).to receive(:name).and_return("dummy_provider")
|
||||||
|
allow(client).to receive(:provider_capability).and_return(response)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should call the plugin client" do
|
||||||
|
expect(client).to receive(:provider_capability).
|
||||||
|
with(instance_of(Vagrant::Proto::ProviderCapabilityRequest))
|
||||||
|
.and_return(response)
|
||||||
|
subject.capability(cap_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should deserialize result" do
|
||||||
|
expect(subject.capability(cap_name)).to eq(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when hash value is returned" do
|
||||||
|
let(:result) { {key: "value"} }
|
||||||
|
|
||||||
|
it "should return indifferent access hash" do
|
||||||
|
r = subject.capability(cap_name)
|
||||||
|
expect(r[:key]).to eq(result[:key])
|
||||||
|
expect(r).to be_a(Vagrant::Util::HashWithIndifferentAccess)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when arguments are provided" do
|
||||||
|
let(:args) { ["arg1", {"key" => "value"}] }
|
||||||
|
|
||||||
|
it "should serialize arguments when sent to plugin client" do
|
||||||
|
expect(client).to receive(:provider_capability) do |req|
|
||||||
|
expect(req.arguments).to eq(args.to_json)
|
||||||
|
response
|
||||||
|
end
|
||||||
|
subject.capability(cap_name, *args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#is_installed?" do
|
||||||
|
it "should call plugin client" do
|
||||||
|
expect(client).to receive(:is_installed).and_return(double("response", result: true))
|
||||||
|
expect(subject.is_installed?).to eq(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#is_usable?" do
|
||||||
|
it "should call plugin client" do
|
||||||
|
expect(client).to receive(:is_usable).and_return(double("response", result: true))
|
||||||
|
expect(subject.is_usable?).to eq(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#machine_id_changed" do
|
||||||
|
it "should call plugin client" do
|
||||||
|
expect(client).to receive(:machine_id_changed).and_return(double("response", result: true))
|
||||||
|
expect(subject.machine_id_changed).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#ssh_info" do
|
||||||
|
let(:response) {
|
||||||
|
Vagrant::Proto::MachineSshInfo.new(
|
||||||
|
host: "localhost",
|
||||||
|
port: 2222,
|
||||||
|
private_key_path: "/key/path",
|
||||||
|
username: "vagrant"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
before { allow(client).to receive(:ssh_info).and_return(response) }
|
||||||
|
|
||||||
|
it "should return hash with indifferent access result" do
|
||||||
|
expect(subject.ssh_info).to be_a(Vagrant::Util::HashWithIndifferentAccess)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should include ssh information" do
|
||||||
|
result = subject.ssh_info
|
||||||
|
expect(result[:host]).to eq(response.host)
|
||||||
|
expect(result[:port]).to eq(response.port)
|
||||||
|
expect(result[:private_key_path]).to eq(response.private_key_path)
|
||||||
|
expect(result[:username]).to eq(response.username)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#state" do
|
||||||
|
let(:response) {
|
||||||
|
Vagrant::Proto::MachineState.new(
|
||||||
|
id: "machine-id",
|
||||||
|
short_description: "running",
|
||||||
|
long_description: "it's really running"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
before { allow(client).to receive(:state).and_return(response) }
|
||||||
|
|
||||||
|
it "should return a MachineState instance" do
|
||||||
|
expect(subject.state).to be_a(Vagrant::MachineState)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should set the state attributes" do
|
||||||
|
result = subject.state
|
||||||
|
expect(result.id).to eq(response.id)
|
||||||
|
expect(result.short_description).to eq(response.short_description)
|
||||||
|
expect(result.long_description).to eq(response.long_description)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,73 @@
|
||||||
|
require_relative "../../base"
|
||||||
|
|
||||||
|
describe Vagrant::GoPlugin::SyncedFolderPlugin::SyncedFolder do
|
||||||
|
let(:subject) {
|
||||||
|
Class.new(described_class).tap { |c|
|
||||||
|
c.plugin_client = client } }
|
||||||
|
let(:client) { double("client") }
|
||||||
|
let(:machine) { double("machine", to_json: "{}") }
|
||||||
|
let(:folders) { double("folders", to_json: "{}") }
|
||||||
|
let(:options) { double("options", to_json: "{}") }
|
||||||
|
|
||||||
|
it "should be a GRPCPlugin" do
|
||||||
|
expect(subject).to be_a(Vagrant::GoPlugin::GRPCPlugin)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#cleanup" do
|
||||||
|
it "should call plugin client" do
|
||||||
|
expect(client).to receive(:cleanup).
|
||||||
|
with(instance_of(Vagrant::Proto::SyncedFolders))
|
||||||
|
subject.cleanup(machine, options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#disable" do
|
||||||
|
it "should call plugin client" do
|
||||||
|
expect(client).to receive(:disable).
|
||||||
|
with(instance_of(Vagrant::Proto::SyncedFolders))
|
||||||
|
subject.disable(machine, folders, options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#enable" do
|
||||||
|
it "should call plugin client" do
|
||||||
|
expect(client).to receive(:enable).
|
||||||
|
with(instance_of(Vagrant::Proto::SyncedFolders))
|
||||||
|
subject.enable(machine, folders, options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#prepare" do
|
||||||
|
it "should call plugin client" do
|
||||||
|
expect(client).to receive(:prepare).
|
||||||
|
with(instance_of(Vagrant::Proto::SyncedFolders))
|
||||||
|
subject.prepare(machine, folders, options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#usable?" do
|
||||||
|
let(:response) { Vagrant::Proto::Valid.new(result: true) }
|
||||||
|
|
||||||
|
it "should call the plugin client" do
|
||||||
|
expect(client).to receive(:is_usable).
|
||||||
|
with(instance_of(Vagrant::Proto::Machine)).
|
||||||
|
and_return(response)
|
||||||
|
expect(subject.usable?).to eq(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#name" do
|
||||||
|
let(:response) { Vagrnat::Proto::Identifier.new(name: "dummy") }
|
||||||
|
|
||||||
|
it "should call the plugin client" do
|
||||||
|
expect(client).to receive(:name).and_return(response)
|
||||||
|
expect(subject.name).to eq(response.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should only call the plugin client once" do
|
||||||
|
expect(client).to receive(:name).once.and_return(response)
|
||||||
|
expect(subject.name).to eq(response.name)
|
||||||
|
expect(subject.name).to eq(response.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
require_relative "../base"
|
||||||
|
|
||||||
|
describe Vagrant::GoPlugin do
|
||||||
|
describe "INSTALL_DIRECTORY constant" do
|
||||||
|
let(:subject) { described_class.const_get(:INSTALL_DIRECTORY) }
|
||||||
|
|
||||||
|
it "should be a String" do
|
||||||
|
expect(subject).to be_a(String)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should be frozen" do
|
||||||
|
expect(subject).to be_frozen
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should be within the user data path" do
|
||||||
|
expect(subject).to start_with(Vagrant.user_data_path.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".interface" do
|
||||||
|
it "should return an interface instance" do
|
||||||
|
expect(described_class.interface).to be_a(Vagrant::GoPlugin::Interface)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should cache the interface instance" do
|
||||||
|
expect(described_class.interface).to be(described_class.interface)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -118,4 +118,56 @@ describe Vagrant::Plugin::StateFile do
|
||||||
to raise_error(Vagrant::Errors::PluginStateFileParseError)
|
to raise_error(Vagrant::Errors::PluginStateFileParseError)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "go plugin usage" do
|
||||||
|
describe "#add_go_plugin" do
|
||||||
|
it "should add plugin to list of installed go plugins" do
|
||||||
|
subject.add_go_plugin("foo", source: "http://localhost/foo.zip")
|
||||||
|
expect(subject.installed_go_plugins).to include("foo")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should update source when added again" do
|
||||||
|
subject.add_go_plugin("foo", source: "http://localhost/foo.zip")
|
||||||
|
expect(subject.installed_go_plugins["foo"]["source"]).to eq("http://localhost/foo.zip")
|
||||||
|
subject.add_go_plugin("foo", source: "http://localhost/foo1.zip")
|
||||||
|
expect(subject.installed_go_plugins["foo"]["source"]).to eq("http://localhost/foo1.zip")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#remove_go_plugin" do
|
||||||
|
before do
|
||||||
|
subject.add_go_plugin("foo", source: "http://localhost/foo.zip")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should remove the installed plugin" do
|
||||||
|
subject.remove_go_plugin("foo")
|
||||||
|
expect(subject.installed_go_plugins).not_to include("foo")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should remove plugin not installed" do
|
||||||
|
subject.remove_go_plugin("foo")
|
||||||
|
expect(subject.installed_go_plugins).not_to include("foo")
|
||||||
|
subject.remove_go_plugin("foo")
|
||||||
|
expect(subject.installed_go_plugins).not_to include("foo")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#has_go_plugin?" do
|
||||||
|
before do
|
||||||
|
subject.add_go_plugin("foo", source: "http://localhost/foo.zip")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return true when plugin is installed" do
|
||||||
|
expect(subject.has_go_plugin?("foo")).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return false when plugin is not installed" do
|
||||||
|
expect(subject.has_go_plugin?("fee")).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should allow symbol names" do
|
||||||
|
expect(subject.has_go_plugin?(:foo)).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,6 +13,7 @@ Gem::Specification.new do |s|
|
||||||
s.description = "Vagrant is a tool for building and distributing virtualized development environments."
|
s.description = "Vagrant is a tool for building and distributing virtualized development environments."
|
||||||
|
|
||||||
s.required_ruby_version = "~> 2.2", "< 2.7"
|
s.required_ruby_version = "~> 2.2", "< 2.7"
|
||||||
|
|
||||||
s.required_rubygems_version = ">= 1.3.6"
|
s.required_rubygems_version = ">= 1.3.6"
|
||||||
s.rubyforge_project = "vagrant"
|
s.rubyforge_project = "vagrant"
|
||||||
|
|
||||||
|
@ -22,6 +23,8 @@ Gem::Specification.new do |s|
|
||||||
s.add_dependency "erubis", "~> 2.7.0"
|
s.add_dependency "erubis", "~> 2.7.0"
|
||||||
s.add_dependency "i18n", "~> 1.1.1"
|
s.add_dependency "i18n", "~> 1.1.1"
|
||||||
s.add_dependency "listen", "~> 3.1.5"
|
s.add_dependency "listen", "~> 3.1.5"
|
||||||
|
s.add_dependency "grpc", "~> 1.19.0"
|
||||||
|
s.add_dependency "grpc-tools", "~> 1.19.0"
|
||||||
s.add_dependency "hashicorp-checkpoint", "~> 0.1.5"
|
s.add_dependency "hashicorp-checkpoint", "~> 0.1.5"
|
||||||
s.add_dependency "log4r", "~> 1.1.9", "< 1.1.11"
|
s.add_dependency "log4r", "~> 1.1.9", "< 1.1.11"
|
||||||
s.add_dependency "net-ssh", "~> 5.1.0"
|
s.add_dependency "net-ssh", "~> 5.1.0"
|
||||||
|
@ -49,6 +52,7 @@ Gem::Specification.new do |s|
|
||||||
s.add_development_dependency "rspec-its", "~> 1.2.0"
|
s.add_development_dependency "rspec-its", "~> 1.2.0"
|
||||||
s.add_development_dependency "webmock", "~> 2.3.1"
|
s.add_development_dependency "webmock", "~> 2.3.1"
|
||||||
s.add_development_dependency "fake_ftp", "~> 0.1.1"
|
s.add_development_dependency "fake_ftp", "~> 0.1.1"
|
||||||
|
s.add_development_dependency "rake-compiler"
|
||||||
|
|
||||||
# The following block of code determines the files that should be included
|
# The following block of code determines the files that should be included
|
||||||
# in the gem. It does this by reading all the files in the directory where
|
# in the gem. It does this by reading all the files in the directory where
|
||||||
|
|
|
@ -54,3 +54,7 @@ Enabling this feature allows all provisioners to specify `before` and `after`
|
||||||
options. These options allow provisioners to be configured to run before or after
|
options. These options allow provisioners to be configured to run before or after
|
||||||
any given "root" provisioner. more information about these options can be found
|
any given "root" provisioner. more information about these options can be found
|
||||||
on the [base provisioner documentation page](/docs/provisioning/basic_usage.html)
|
on the [base provisioner documentation page](/docs/provisioning/basic_usage.html)
|
||||||
|
|
||||||
|
### `go_plugin`
|
||||||
|
|
||||||
|
Enabling this feature turns on support for [go-plugin](https://github.com/hashicorp/go-plugin) based plugin for Vagrant.
|
||||||
|
|
Loading…
Reference in New Issue