This commit is contained in:
sophia 2021-04-06 17:03:30 -05:00 committed by Paul Hinze
parent 20fbe05c23
commit c3ee750db1
No known key found for this signature in database
GPG Key ID: B69DEDF2D55501C0
427 changed files with 61620 additions and 2 deletions

6
.gitignore vendored
View File

@ -13,6 +13,11 @@ boxes/*
/website/build
/vagrant-spec.config.rb
test/vagrant-spec/.vagrant/
.vagrant/
/vagrant
/pkg
data.db
vagrant-restore.db.lock
# Bundler/Rubygems
*.gem
@ -40,6 +45,7 @@ doc/
.idea/*
*.iml
.project
.vscode
# Ruby Managers
.rbenv

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "vendor/proto/api-common-protos"]
path = vendor/proto/api-common-protos
url = https://github.com/googleapis/api-common-protos

41
Dockerfile Normal file
View File

@ -0,0 +1,41 @@
# syntax = docker.mirror.hashicorp.services/docker/dockerfile:experimental
FROM docker.mirror.hashicorp.services/golang:alpine AS builder
RUN apk add --no-cache git gcc libc-dev openssh
RUN mkdir -p /tmp/wp-prime
COPY go.sum /tmp/wp-prime
COPY go.mod /tmp/wp-prime
WORKDIR /tmp/wp-prime
RUN mkdir -p -m 0600 ~/.ssh \
&& ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts
RUN git config --global url.ssh://git@github.com/.insteadOf https://github.com/
RUN --mount=type=ssh --mount=type=secret,id=ssh.config --mount=type=secret,id=ssh.key \
GIT_SSH_COMMAND="ssh -o \"ControlMaster auto\" -F \"/run/secrets/ssh.config\"" \
go mod download
COPY . /tmp/wp-src
WORKDIR /tmp/wp-src
RUN apk add --no-cache make
RUN go get github.com/kevinburke/go-bindata/...
RUN --mount=type=cache,target=/root/.cache/go-build make bin
FROM docker.mirror.hashicorp.services/alpine
COPY --from=builder /tmp/wp-src/vagrant /usr/bin/vagrant
VOLUME ["/data"]
RUN addgroup vagrant && \
adduser -S -G vagrant vagrant && \
mkdir /data/ && \
chown -R vagrant:vagrant /data
USER vagrant
ENTRYPOINT ["/usr/bin/vagrant"]

58
Makefile Normal file
View File

@ -0,0 +1,58 @@
# A lot of this Makefile right now is temporary since we have a private
# repo so that we can more sanely create
ASSETFS_PATH?=internal/server/gen/bindata_ui.go
GIT_COMMIT=$$(git rev-parse --short HEAD)
GIT_DIRTY=$$(test -n "`git status --porcelain`" && echo "+CHANGES" || true)
GIT_DESCRIBE=$$(git describe --tags --always --match "v*")
GIT_IMPORT="github.com/hashicorp/vagrant/internal/version"
GOLDFLAGS="-X $(GIT_IMPORT).GitCommit=$(GIT_COMMIT)$(GIT_DIRTY) -X $(GIT_IMPORT).GitDescribe=$(GIT_DESCRIBE)"
CGO_ENABLED?=0
.PHONY: bin
bin: # bin creates the binaries for Vagrant for the current platform
CGO_ENABLED=$(CGO_ENABLED) go build -ldflags $(GOLDFLAGS) -tags assetsembedded -o ./vagrant ./cmd/vagrant
.PHONY: bin/windows
bin/windows: # create windows binaries
GOOS=linux GOARCH=amd64 go build -o ./internal/assets/ceb/ceb ./cmd/vagrant-entrypoint
cd internal/assets && go-bindata -pkg assets -o prod.go -tags assetsembedded ./ceb
GOOS=windows GOARCH=amd64 CGO_ENABLED=$(CGO_ENABLED) go build -ldflags $(GOLDFLAGS) -tags assetsembedded -o ./vagrant.exe ./cmd/vagrant
.PHONY: bin/linux
bin/linux: # create Linux binaries
GOOS=linux GOARCH=amd64 $(MAKE) bin
.PHONY: bin/darwin
bin/darwin: # create Darwin binaries
GOOS=darwin GOARCH=amd64 $(MAKE) bin
.PHONY: test
test: # run tests
go test ./...
.PHONY: format
format: # format go code
gofmt -s -w ./
.PHONY: docker/mitchellh
docker/mitchellh:
DOCKER_BUILDKIT=1 docker build \
--ssh default \
--secret id=ssh.config,src="${HOME}/.ssh/config" \
--secret id=ssh.key,src="${HOME}/.ssh/config" \
-t vagrant:latest \
.
# This currently assumes you have run `ember build` in the ui/ directory
static-assets:
@go-bindata -pkg gen -prefix dist -o $(ASSETFS_PATH) ./ui/dist/...
@gofmt -s -w $(ASSETFS_PATH)
.PHONY: gen/doc
gen/doc:
@rm -rf ./doc/* 2> /dev/null
protoc -I=. \
-I=./vendor/proto/api-common-protos/ \
--doc_out=./doc --doc_opt=html,index.html \
./internal/server/proto/server.proto

10
builtin/README.md Normal file
View File

@ -0,0 +1,10 @@
# Built-in Plugins
This directory contains all the "built-in" plugins. These are real plugins,
they dogfood the full plugin SDK, do not depend on any internal packages,
and they are executed via subprocess just like a real plugin would be.
The difference is that these plugins are linked directly into the single
command binary. We do this currently for ease of development of the project.
In future we will split these out into standalone repositories and
binaries.

112
builtin/myplugin/command.go Normal file
View File

@ -0,0 +1,112 @@
package myplugin
import (
"strings"
"time"
"github.com/DavidGamba/go-getoptions/option"
"github.com/hashicorp/vagrant-plugin-sdk/component"
plugincore "github.com/hashicorp/vagrant-plugin-sdk/core"
"github.com/hashicorp/vagrant-plugin-sdk/docs"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
)
type CommandConfig struct {
}
// Command is the Command implementation for myplugin.
type Command struct {
config CommandConfig
}
func (c *Command) ConfigSet(v interface{}) error {
return nil
}
func (c *Command) CommandFunc() interface{} {
return nil
}
func (c *Command) Config() (interface{}, error) {
return &c.config, nil
}
func (c *Command) Documentation() (*docs.Documentation, error) {
doc, err := docs.New(docs.FromConfig(&CommandConfig{}))
if err != nil {
return nil, err
}
return doc, nil
}
// SynopsisFunc implements component.Command
func (c *Command) SynopsisFunc() interface{} {
return c.Synopsis
}
// HelpFunc implements component.Command
func (c *Command) HelpFunc() interface{} {
return c.Help
}
// FlagsFunc implements component.Command
func (c *Command) FlagsFunc() interface{} {
return c.Flags
}
// ExecuteFunc implements component.Command
func (c *Command) ExecuteFunc() interface{} {
return c.Execute
}
func (c *Command) Synopsis() string {
return "I don't really do anything"
}
func (c *Command) Help() string {
return "Output some project information!"
}
func (c *Command) Flags() []*option.Option {
booltest := option.New("booltest", option.BoolType)
booltest.Description = "a test flag for bools"
booltest.DefaultStr = "true"
booltest.Aliases = append(booltest.Aliases, "bt")
stringflag := option.New("stringflag", option.StringType)
stringflag.Description = "a test flag for strings"
stringflag.DefaultStr = "message"
stringflag.Aliases = append(stringflag.Aliases, "sf")
return []*option.Option{booltest, stringflag}
}
func (c *Command) Execute(trm terminal.UI, env plugincore.Project) int64 {
mn, _ := env.MachineNames()
trm.Output("\nMachines in this project")
trm.Output(strings.Join(mn[:], "\n"))
cwd, _ := env.CWD()
datadir, _ := env.DataDir()
vagrantfileName, _ := env.VagrantfileName()
home, _ := env.Home()
localDataPath, _ := env.LocalData()
defaultPrivateKeyPath, _ := env.DefaultPrivateKey()
trm.Output("\nEnvironment information")
trm.Output("Working directory: " + cwd)
trm.Output("Data directory: " + datadir)
trm.Output("Vagrantfile name: " + vagrantfileName)
trm.Output("Home directory: " + home)
trm.Output("Local data directory: " + localDataPath)
trm.Output("Default private key path: " + defaultPrivateKeyPath)
time.Sleep(1 * time.Second)
return 0
}
var (
_ component.Command = (*Command)(nil)
)

12
builtin/myplugin/main.go Normal file
View File

@ -0,0 +1,12 @@
package myplugin
import (
sdk "github.com/hashicorp/vagrant-plugin-sdk"
)
//go:generate protoc -I ../../.. --go_opt=plugins=grpc --go_out=../../.. vagrant-agogo/builtin/myplugin/plugin.proto
// Options are the SDK options to use for instantiation.
var Options = []sdk.Option{
sdk.WithComponents(&Provider{}, &Command{}),
}

View File

@ -0,0 +1,139 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.23.0
// protoc v3.13.0
// source: vagrant-agogo/builtin/myplugin/plugin.proto
package myplugin
import (
proto "github.com/golang/protobuf/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type UpResult struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *UpResult) Reset() {
*x = UpResult{}
if protoimpl.UnsafeEnabled {
mi := &file_vagrant_agogo_builtin_myplugin_plugin_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UpResult) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpResult) ProtoMessage() {}
func (x *UpResult) ProtoReflect() protoreflect.Message {
mi := &file_vagrant_agogo_builtin_myplugin_plugin_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpResult.ProtoReflect.Descriptor instead.
func (*UpResult) Descriptor() ([]byte, []int) {
return file_vagrant_agogo_builtin_myplugin_plugin_proto_rawDescGZIP(), []int{0}
}
var File_vagrant_agogo_builtin_myplugin_plugin_proto protoreflect.FileDescriptor
var file_vagrant_agogo_builtin_myplugin_plugin_proto_rawDesc = []byte{
0x0a, 0x2b, 0x76, 0x61, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x2d, 0x61, 0x67, 0x6f, 0x67, 0x6f, 0x2f,
0x62, 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x2f, 0x6d, 0x79, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e,
0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x6d,
0x79, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 0x0a, 0x0a, 0x08, 0x55, 0x70, 0x52, 0x65, 0x73,
0x75, 0x6c, 0x74, 0x42, 0x20, 0x5a, 0x1e, 0x76, 0x61, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x2d, 0x61,
0x67, 0x6f, 0x67, 0x6f, 0x2f, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x2f, 0x6d, 0x79, 0x70,
0x6c, 0x75, 0x67, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_vagrant_agogo_builtin_myplugin_plugin_proto_rawDescOnce sync.Once
file_vagrant_agogo_builtin_myplugin_plugin_proto_rawDescData = file_vagrant_agogo_builtin_myplugin_plugin_proto_rawDesc
)
func file_vagrant_agogo_builtin_myplugin_plugin_proto_rawDescGZIP() []byte {
file_vagrant_agogo_builtin_myplugin_plugin_proto_rawDescOnce.Do(func() {
file_vagrant_agogo_builtin_myplugin_plugin_proto_rawDescData = protoimpl.X.CompressGZIP(file_vagrant_agogo_builtin_myplugin_plugin_proto_rawDescData)
})
return file_vagrant_agogo_builtin_myplugin_plugin_proto_rawDescData
}
var file_vagrant_agogo_builtin_myplugin_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_vagrant_agogo_builtin_myplugin_plugin_proto_goTypes = []interface{}{
(*UpResult)(nil), // 0: myplugin.UpResult
}
var file_vagrant_agogo_builtin_myplugin_plugin_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_vagrant_agogo_builtin_myplugin_plugin_proto_init() }
func file_vagrant_agogo_builtin_myplugin_plugin_proto_init() {
if File_vagrant_agogo_builtin_myplugin_plugin_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_vagrant_agogo_builtin_myplugin_plugin_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UpResult); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_vagrant_agogo_builtin_myplugin_plugin_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_vagrant_agogo_builtin_myplugin_plugin_proto_goTypes,
DependencyIndexes: file_vagrant_agogo_builtin_myplugin_plugin_proto_depIdxs,
MessageInfos: file_vagrant_agogo_builtin_myplugin_plugin_proto_msgTypes,
}.Build()
File_vagrant_agogo_builtin_myplugin_plugin_proto = out.File
file_vagrant_agogo_builtin_myplugin_plugin_proto_rawDesc = nil
file_vagrant_agogo_builtin_myplugin_plugin_proto_goTypes = nil
file_vagrant_agogo_builtin_myplugin_plugin_proto_depIdxs = nil
}

View File

@ -0,0 +1,8 @@
syntax = "proto3";
package myplugin;
option go_package = "vagrant-agogo/builtin/myplugin";
message UpResult {}

View File

@ -0,0 +1,74 @@
package myplugin
import (
"context"
"github.com/hashicorp/vagrant-plugin-sdk/component"
"github.com/hashicorp/vagrant-plugin-sdk/docs"
"github.com/hashicorp/vagrant-plugin-sdk/multistep"
)
type ProviderConfig struct {
}
// Provider is the Provider implementation for myplugin.
type Provider struct {
config ProviderConfig
}
// Config implements Configurable
func (p *Provider) Config() (interface{}, error) {
return &p.config, nil
}
func (b *Provider) Documentation() (*docs.Documentation, error) {
doc, err := docs.New(docs.FromConfig(&ProviderConfig{}))
if err != nil {
return nil, err
}
return doc, nil
}
// UsableFunc implements component.Provider
func (p *Provider) UsableFunc() interface{} {
return p.Usable
}
// InstalledFunc implements component.Provider
func (p *Provider) InstalledFunc() interface{} {
return p.Installed
}
// InitFunc implements component.Provider
func (p *Provider) InitFunc() interface{} {
return p.Init
}
// ActionUpFunc implements component.Provider
func (p *Provider) ActionUpFunc() interface{} {
return p.ActionUp
}
// TODO
func (p *Provider) Usable() (bool, error) {
return true, nil
}
func (p *Provider) Installed(context.Context) (bool, error) {
return true, nil
}
// TODO
func (p *Provider) Init() (bool, error) {
return true, nil
}
// TODO: Take an implementation of core.Machine as an input
func (c *Provider) ActionUp(ctx context.Context, statebag *multistep.BasicStateBag) (*UpResult, error) {
return &UpResult{}, nil
}
var (
_ component.Provider = (*Provider)(nil)
_ component.Configurable = (*Provider)(nil)
)

View File

@ -0,0 +1,74 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"strings"
"unicode"
"github.com/hashicorp/go-hclog"
"google.golang.org/grpc/status"
"github.com/hashicorp/vagrant/internal/ceb"
"github.com/hashicorp/vagrant/internal/pkg/signalcontext"
)
func main() {
os.Exit(realMain())
}
func realMain() int {
flag.Usage = usage
flag.Parse()
args := flag.Args()
if len(args) == 0 {
usage()
return 1
}
// TODO(mitchellh): proper log setup
log := hclog.L()
hclog.L().SetLevel(hclog.Trace)
// Create a context that is cancelled on interrupt
ctx, closer := signalcontext.WithInterrupt(context.Background(), log)
defer closer()
// Run our core logic
err := ceb.Run(ctx,
ceb.WithEnvDefaults(),
ceb.WithExec(args))
if err != nil {
fmt.Fprintf(flag.CommandLine.Output(),
"Error initializing Vagrant entrypoint: %s\n", formatError(err))
return 1
}
return 0
}
func formatError(err error) string {
if s, ok := status.FromError(err); ok {
return s.Message()
}
return err.Error()
}
func usage() {
fmt.Fprintf(flag.CommandLine.Output(),
strings.TrimLeftFunc(usageText, unicode.IsSpace),
os.Args[0])
flag.PrintDefaults()
}
const usageText = `
Usage: %[1]s [cmd] [args...]
This the custom entrypoint to support Vagrant. It will re-execute any
command given after configuring the environment for usage with Vagrant.
`

15
cmd/vagrant/main.go Normal file
View File

@ -0,0 +1,15 @@
package main
import (
"os"
"path/filepath"
"github.com/hashicorp/vagrant/internal/cli"
)
func main() {
// Make args[0] just the name of the executable since it is used in logs.
os.Args[0] = filepath.Base(os.Args[0])
os.Exit(cli.Main(os.Args))
}

114
go.mod Normal file
View File

@ -0,0 +1,114 @@
module github.com/hashicorp/vagrant
go 1.13
require (
github.com/Azure/azure-sdk-for-go v42.3.0+incompatible
github.com/Azure/go-autorest/autorest v0.10.2
github.com/Azure/go-autorest/autorest/adal v0.8.3 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2
github.com/Azure/go-autorest/autorest/to v0.3.0
github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect
github.com/DavidGamba/go-getoptions v0.23.0
github.com/LK4D4/joincontext v0.0.0-20171026170139-1724345da6d5 // indirect
github.com/adrg/xdg v0.2.1
github.com/apex/log v1.1.2
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
github.com/aws/aws-sdk-go v1.33.6
github.com/bmatcuk/doublestar v1.1.5
github.com/boltdb/bolt v1.3.1
github.com/buildpacks/pack v0.11.1
github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054
github.com/containerd/console v1.0.1
github.com/creack/pty v1.1.11
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/docker/cli v0.0.0-20200312141509-ef2f64abbd37
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v1.4.2-0.20200221181110-62bd5a33f707
github.com/docker/go-connections v0.4.0
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/elazarl/go-bindata-assetfs v1.0.1
github.com/fatih/color v1.9.0
github.com/go-git/go-git/v5 v5.1.0
github.com/go-openapi/runtime v0.19.15
github.com/go-openapi/strfmt v0.19.5
github.com/go-ozzo/ozzo-validation/v4 v4.2.1
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator v9.31.0+incompatible
github.com/gofrs/flock v0.8.0
github.com/golang/protobuf v1.4.2
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gorilla/handlers v1.4.2
github.com/hashicorp/go-argmapper v0.0.0-20200721221215-04ae500ede3b
github.com/hashicorp/go-getter v1.4.1
github.com/hashicorp/go-hclog v0.14.1
github.com/hashicorp/go-memdb v1.2.0
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-plugin v1.3.0
github.com/hashicorp/go-version v1.2.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl/v2 v2.7.1-0.20201023000745-3de61ecba298
github.com/hashicorp/horizon v0.0.0-20201027182500-45298493f49e
github.com/hashicorp/nomad/api v0.0.0-20200814140818-42de70466a9d
github.com/hashicorp/vagrant-plugin-sdk v0.0.0-20201216193437-46ae3967665b
github.com/hashicorp/waypoint-hzn v0.0.0-20201008221232-97cd4d9120b9
github.com/hashicorp/waypoint-plugin-sdk v0.0.0-20201107013852-c3b6eb26185d
github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce // indirect
github.com/imdario/mergo v0.3.11
github.com/improbable-eng/grpc-web v0.13.0
github.com/kr/text v0.2.0
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mitchellh/cli v1.1.2
github.com/mitchellh/copystructure v1.0.0
github.com/mitchellh/go-glint v0.0.0-20201015034436-f80573c636de
github.com/mitchellh/go-grpc-net-conn v0.0.0-20200407005438-c00174eff6c8
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/go-testing-interface v1.14.1
github.com/mitchellh/go-wordwrap v1.0.1
github.com/mitchellh/mapstructure v1.3.3
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/mr-tron/base58 v1.2.0
github.com/natefinch/atomic v0.0.0-20200526193002-18c0533a5b09
github.com/netlify/open-api v0.15.0
github.com/oklog/run v1.1.0
github.com/oklog/ulid v1.3.1
github.com/oklog/ulid/v2 v2.0.2
github.com/olekukonko/tablewriter v0.0.4
github.com/pkg/errors v0.9.1
github.com/posener/complete v1.2.3
github.com/rs/cors v1.7.0 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/slack-go/slack v0.6.5
github.com/stretchr/testify v1.6.1
github.com/zclconf/go-cty v1.5.1
github.com/zclconf/go-cty-yaml v1.0.2
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
golang.org/x/net v0.0.0-20201021035429-f5854403a974
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f
google.golang.org/api v0.20.0
google.golang.org/genproto v0.0.0-20201002142447-3860012362da
google.golang.org/grpc v1.32.0
google.golang.org/protobuf v1.25.0
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/yaml.v2 v2.3.0
k8s.io/api v0.18.0
k8s.io/apimachinery v0.18.0
k8s.io/client-go v0.18.0
)
// NOTE(mitchellh): I'm keeping these commented and in here because during
// development at the moment it is common to be working on these libs too.
// replace github.com/hashicorp/go-argmapper => ../go-argmapper
// replace github.com/hashicorp/horizon => ../horizon
replace github.com/hashicorp/vagrant-plugin-sdk => ../vagrant-plugin-sdk
replace (
// v0.3.11 panics for some reason on our tests
github.com/imdario/mergo => github.com/imdario/mergo v0.3.9
// https://github.com/ory/dockertest/issues/208
golang.org/x/sys => golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6
)

1747
go.sum Normal file

File diff suppressed because it is too large Load Diff

2
internal/assets/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
ceb/ceb
prod.go

35
internal/assets/dev.go Normal file
View File

@ -0,0 +1,35 @@
//go:generate go-bindata -dev -pkg assets -o dev_assets.go -tags !assetsembedded ceb
package assets
import (
"os"
"path/filepath"
)
var rootDir string
func init() {
dir, err := os.Getwd()
if err != nil {
panic(err)
}
for dir != "/" {
path := filepath.Join(dir, "internal/assets")
if _, err := os.Stat(path); err == nil {
rootDir = path
return
}
nextDir := filepath.Dir(dir)
if nextDir == dir {
break
}
dir = nextDir
}
// Uuuuhhh...
rootDir = "./internal/assets"
}

View File

@ -0,0 +1,235 @@
// Code generated by go-bindata. DO NOT EDIT.
// sources:
// ceb/ceb (58.133MB)
// +build !assetsembedded
package assets
import (
"crypto/sha256"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// bindataRead reads the given file from disk. It returns an error on failure.
func bindataRead(path, name string) ([]byte, error) {
buf, err := ioutil.ReadFile(path)
if err != nil {
err = fmt.Errorf("Error reading asset %s at %s: %w", name, path, err)
}
return buf, err
}
type asset struct {
bytes []byte
info os.FileInfo
digest [sha256.Size]byte
}
// cebCeb reads file data from disk. It returns an error on failure.
func cebCeb() (*asset, error) {
path := filepath.Join(rootDir, "ceb/ceb")
name := "ceb/ceb"
bytes, err := bindataRead(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %w", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
canonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[canonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// AssetString returns the asset contents as a string (instead of a []byte).
func AssetString(name string) (string, error) {
data, err := Asset(name)
return string(data), err
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// MustAssetString is like AssetString but panics when Asset would return an
// error. It simplifies safe initialization of global variables.
func MustAssetString(name string) string {
return string(MustAsset(name))
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
canonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[canonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetDigest returns the digest of the file with the given name. It returns an
// error if the asset could not be found or the digest could not be loaded.
func AssetDigest(name string) ([sha256.Size]byte, error) {
canonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[canonicalName]; ok {
a, err := f()
if err != nil {
return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err)
}
return a.digest, nil
}
return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name)
}
// Digests returns a map of all known files and their checksums.
func Digests() (map[string][sha256.Size]byte, error) {
mp := make(map[string][sha256.Size]byte, len(_bindata))
for name := range _bindata {
a, err := _bindata[name]()
if err != nil {
return nil, err
}
mp[name] = a.digest
}
return mp, nil
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"ceb/ceb": cebCeb,
}
// AssetDebug is true if the assets were built with the debug flag enabled.
const AssetDebug = false
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"},
// AssetDir("data/img") would return []string{"a.png", "b.png"},
// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
canonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(canonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{
"ceb": {nil, map[string]*bintree{
"ceb": {cebCeb, map[string]*bintree{}},
}},
}}
// RestoreAsset restores an asset under the given directory.
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
}
// RestoreAssets restores an asset under the given directory recursively.
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
canonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...)
}

580
internal/cli/base.go Normal file
View File

@ -0,0 +1,580 @@
package cli
import (
"context"
"errors"
"io"
"regexp"
"strings"
"github.com/DavidGamba/go-getoptions"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vagrant-plugin-sdk/helper/path"
"github.com/hashicorp/vagrant-plugin-sdk/helper/paths"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"github.com/hashicorp/vagrant/internal/clicontext"
clientpkg "github.com/hashicorp/vagrant/internal/client"
"github.com/hashicorp/vagrant/internal/clierrors"
"github.com/hashicorp/vagrant/internal/config"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
"github.com/hashicorp/vagrant/internal/serverclient"
)
// baseCommand is embedded in all commands to provide common logic and data.
//
// The unexported values are not available until after Init is called. Some
// values are only available in certain circumstances, read the documentation
// for the field to determine if that is the case.
type baseCommand struct {
// Ctx is the base context for the command. It is up to commands to
// utilize this context so that cancellation works in a timely manner.
Ctx context.Context
// Log is the logger to use.
Log hclog.Logger
// LogOutput is the writer that Log points to. You SHOULD NOT use
// this directly. We have access to this so you can use
// hclog.OutputResettable if necessary.
LogOutput io.Writer
//---------------------------------------------------------------
// The fields below are only available after calling Init.
// cfg is the parsed configuration
cfg *config.Config
// UI is used to write to the CLI.
ui terminal.UI
// client for performing operations
basis *clientpkg.Basis
project *clientpkg.Project
machines []*clientpkg.Machine
// clientContext is set to the context information for the current
// connection. This might not exist in the contextStorage yet if this
// is from an env var or flags.
clientContext *clicontext.Config
// contextStorage is for CLI contexts.
contextStorage *clicontext.Storage
//---------------------------------------------------------------
// Internal fields that should not be accessed directly
// flagPlain is whether the output should be in plain mode.
flagPlain bool
// flagLabels are set via -label if flagSetOperation is set.
flagLabels map[string]string
// flagRemote is whether to execute using a remote runner or use
// a local runner.
flagRemote bool
// flagRemoteSource are the remote data source overrides for jobs.
flagRemoteSource map[string]string
// flagBasis is the basis to work within.
flagBasis string
// flagMachine is the machine to target.
flagMachine string
// flagConnection contains manual flag-based connection info.
flagConnection clicontext.Config
// args that were present after parsing flags
args []string
// options passed in at the global level
globalOptions []Option
}
// Close cleans up any resources that the command created. This should be
// defered by any CLI command that embeds baseCommand in the Run command.
func (c *baseCommand) Close() error {
if closer, ok := c.ui.(io.Closer); ok && closer != nil {
closer.Close()
}
if c.basis != nil {
c.basis.Close()
}
return nil
}
func BaseCommand(ctx context.Context, log hclog.Logger, logOutput io.Writer, opts ...Option) (*baseCommand, error) {
bc := &baseCommand{
Ctx: ctx,
Log: log,
LogOutput: logOutput,
}
// Get just enough base configuration to
// allow setting up our client connection
c := &baseConfig{
Client: true,
Flags: bc.flagSet(flagSetConnection, nil),
}
// Apply any options that were passed. These
// should at least include the arguments so
// we can extract the flags properly
for _, opt := range opts {
opt(c)
}
if c.UI == nil {
c.UI = terminal.ConsoleUI(context.Background())
}
// Allow parser to not fail on unknown arguments
c.Flags.SetUnknownMode(getoptions.Pass)
if _, err := c.Flags.Parse(c.Args); err != nil {
c.UI.Output(clierrors.Humanize(err), terminal.WithErrorStyle())
return nil, err
}
// Setup our basis path
homeConfigPath, err := paths.VagrantHome()
if err != nil {
bc.ui.Output(clierrors.Humanize(err), terminal.WithErrorStyle())
return nil, err
}
bc.Log.Debug("home configuration directory", "path", homeConfigPath.String())
// Setup our base directory for context management
contextStorage, err := clicontext.NewStorage(
clicontext.WithDir(homeConfigPath.Join("context")))
if err != nil {
bc.ui.Output(clierrors.Humanize(err), terminal.WithErrorStyle())
return nil, err
}
bc.contextStorage = contextStorage
// We use our flag-based connection info if the user set an addr.
var flagConnection *clicontext.Config
if v := bc.flagConnection; v.Server.Address != "" {
flagConnection = &v
}
// Get the context we'll use. The ordering here is purposeful and creates
// the following precedence: (1) context (2) env (3) flags where the
// later values override the former.
connectOpts := []serverclient.ConnectOption{
serverclient.FromContext(bc.contextStorage, ""),
serverclient.FromEnv(),
serverclient.FromContextConfig(flagConnection),
}
bc.clientContext, err = serverclient.ContextConfig(connectOpts...)
if err != nil {
return nil, err
}
// Start building our client options
basisOpts := []clientpkg.Option{
clientpkg.WithLogger(bc.Log),
clientpkg.WithClientConnect(connectOpts...),
clientpkg.WithBasis(
&vagrant_server.Basis{
Name: homeConfigPath.String(),
Path: homeConfigPath.String(),
},
),
}
if !bc.flagRemote {
basisOpts = append(basisOpts, clientpkg.WithLocal())
}
if bc.ui != nil {
basisOpts = append(basisOpts, clientpkg.WithUI(bc.ui))
}
basis, err := clientpkg.New(context.Background(), basisOpts...)
if err != nil {
return nil, err
}
bc.basis = basis
return bc, err
}
// Init initializes the command by parsing flags, parsing the configuration,
// setting up the project, etc. You can control what is done by using the
// options.
//
// Init should be called FIRST within the Run function implementation. Many
// options will affect behavior of other functions that can be called later.
func (c *baseCommand) Init(opts ...Option) error {
baseCfg := baseConfig{
Config: true,
Client: true,
}
for _, opt := range c.globalOptions {
opt(&baseCfg)
}
for _, opt := range opts {
opt(&baseCfg)
}
// Init our UI first so we can write output to the user immediately.
ui := baseCfg.UI
if ui == nil {
ui = terminal.ConsoleUI(c.Ctx)
}
c.ui = ui
// Parse flags
remainingArgs, err := baseCfg.Flags.Parse(baseCfg.Args)
if err != nil {
c.ui.Output(clierrors.Humanize(err), terminal.WithErrorStyle())
return err
}
c.args = remainingArgs
// Reset the UI to plain if that was set
if c.flagPlain {
c.ui = terminal.NonInteractiveUI(c.Ctx)
}
// TODO(spox): re-enable custom basis
// Set our basis reference if provided
// if c.flagBasis != "" {
// c.basis.SetRef(&vagrant_server.Ref_Basis{Name: c.flagBasis})
// }
// Determine if we are in a project and setup if so
cwd, err := path.NewPath(".").Abs()
if err != nil {
panic("cannot setup local directory")
}
if _, err := config.FindPath("", ""); err == nil {
c.project, err = c.basis.LoadProject(
&vagrant_server.Project{
Name: cwd.String(),
Path: cwd.String(),
Basis: c.basis.Ref(),
},
)
if err != nil {
return err
}
}
// Parse the configuration
c.cfg = &config.Config{}
// If we have an app target requirement, we have to get it from the args
// or the config.
if baseCfg.MachineTargetRequired && c.project != nil {
// If we have args, attempt to extract there first.
if len(c.args) > 0 {
match := reMachineTarget.FindStringSubmatch(c.args[0])
if match != nil {
// Set our machine
mach, err := c.project.LoadMachine(&vagrant_server.Machine{
Name: match[1],
Project: c.project.Ref(),
})
if err != nil {
return err
}
c.machines = append(c.machines, mach)
// Shift the args
c.args = c.args[1:]
// Explicitly set remote
c.flagRemote = true
}
}
// If we didn't get our ref, then we need to load config
if len(c.machines) == 0 {
baseCfg.Config = true
}
}
// If we're loading the config, then get it.
if baseCfg.Config {
cfg, err := c.initConfig(baseCfg.ConfigOptional)
if err != nil {
c.logError(c.Log, "failed to load configuration", err)
return err
}
// vagrantfile, err := c.basis.ParseVagrantfile()
// if err != nil {
// c.logError(c.Log, "failed to parse vagrantfile", err)
// return err
// }
// cfg.Project, err = cfg.LoadProject(vagrantfile, c.project.Ref())
// if err != nil {
// c.logError(c.Log, "failed to load project", err)
// return err
// }
c.cfg = cfg
if cfg != nil {
// If we require an app target and we still haven't set it,
// and the user provided it via the CLI, set it now. This code
// path is only reached if it wasn't set via the args either
// above.
if c.flagMachine == "" {
c.flagMachine = "default"
}
mach, err := c.project.LoadMachine(&vagrant_server.Machine{
Name: c.flagMachine,
Project: c.project.Ref(),
})
if err != nil {
return err
}
c.machines = append(c.machines, mach)
}
}
// Create our client
// if baseCfg.Client {
// var err error
// c.basis, err = c.initClient()
// if err != nil {
// c.logError(c.Log, "failed to create client", err)
// return err
// }
// }
// Validate remote vs. local operations.
if c.flagRemote && len(c.machines) == 0 {
if c.cfg == nil || c.cfg.Runner == nil || !c.cfg.Runner.Enabled {
err := errors.New(
"The `-remote` flag was specified but remote operations are not supported\n" +
"for this project.\n\n" +
"Remote operations must be manually enabled by using setting the 'runner.enabled'\n" +
"setting in your Vagrant configuration file. Please see the documentation\n" +
"on this setting for more information.")
c.logError(c.Log, "", err)
return err
}
}
// If this is a single app mode then make sure that we only have
// one app or that we have an app target.
if false && baseCfg.MachineTargetRequired {
if len(c.machines) == 0 {
if len(c.cfg.Project.Machines) != 1 {
c.ui.Output(errMachineModeSingle, terminal.WithErrorStyle())
return ErrSentinel
}
if c.project == nil {
c.project, err = c.basis.LoadProject(
&vagrant_server.Project{
Name: c.cfg.Project.Location,
Path: c.cfg.Project.Location,
Basis: c.basis.Ref(),
},
)
if err != nil {
return err
}
}
mach, err := c.project.LoadMachine(&vagrant_server.Machine{
Name: c.cfg.Project.Machines[0].Name,
Project: c.project.Ref(),
})
if err != nil {
return err
}
c.machines = append(c.machines, mach)
}
}
return nil
}
type Tasker interface {
UI() terminal.UI
Task(context.Context, *vagrant_server.Job_RunOp) (*vagrant_server.Job_RunResult, error)
//CreateTask() *vagrant_server.Task
}
// Do calls the callback based on the loaded scope. This automatically handles any
// parallelization, waiting, and error handling. Your code should be
// thread-safe.
//
// Based on the scope the callback may be executed multiple times. When scoped by
// machine, it will be run against each requested machine. When the scope is basis
// or project, it will only be run once.
//
// If any error is returned, the caller should just exit. The error handling
// including messaging to the user is handling by this function call.
//
// If you want to early exit all the running functions, you should use
// the callback closure properties to cancel the passed in context. This
// will stop any remaining callbacks and exit early.
func (c *baseCommand) Do(ctx context.Context, f func(context.Context, Tasker) error) (finalErr error) {
// Start with checking if we are running in a machine based scope
if len(c.machines) > 0 {
for _, m := range c.machines {
c.Log.Warn("running command on machine", "machine", m)
// If the context has been canceled, then bail
if err := ctx.Err(); err != nil {
return err
}
if err := f(ctx, m); err != nil {
if err != ErrSentinel {
finalErr = multierror.Append(finalErr, err)
}
}
}
return
}
// Now we can check if this is project scoped
if c.project != nil {
finalErr = f(ctx, c.project)
return
}
// And if we're still here, it's gotta be basis scoped
return f(ctx, c.basis)
}
// logError logs an error and outputs it to the UI.
func (c *baseCommand) logError(log hclog.Logger, prefix string, err error) {
if err == ErrSentinel {
return
}
log.Error(prefix, "error", err)
if prefix != "" {
prefix += ": "
}
c.ui.Output("%s%s", prefix, err, terminal.WithErrorStyle())
}
// flagSet creates the flags for this command. The callback should be used
// to configure the set with your own custom options.
func (c *baseCommand) flagSet(bit flagSetBit, f func(*getoptions.GetOpt)) *getoptions.GetOpt {
set := getoptions.New()
set.BoolVar(
&c.flagPlain,
"plain",
false,
set.Description("Plain output: no colors, no animation."),
)
set.StringVar(
&c.flagMachine,
"machine",
"",
set.Description("Machine to target. Certain commands require a single machine target for "+
"Vagrant configurations with multiple apps. If you have a single machine, "+
"then this can be ignored."),
)
set.StringVar(
&c.flagBasis,
"basis",
"default",
set.Description("Basis to operate within."),
)
if bit&flagSetOperation != 0 {
set.StringMapVar(
&c.flagLabels,
"label",
1,
MaxStringMapArgs,
set.Description("Labels to set for this operation. Can be specified multiple times."),
)
set.BoolVar(
&c.flagRemote,
"remote",
false,
set.Description("True to use a remote runner to execute. This defaults to false \n"+
"unless 'runner.default' is set in your configuration."),
)
set.StringMapVar(
&c.flagRemoteSource,
"remote-source",
1,
MaxStringMapArgs,
set.Description("Override configurations for how remote runners source data. "+
"This is specified to the data source type being used in your configuration. "+
"This is used for example to set a specific Git ref to run against."),
)
}
if bit&flagSetConnection != 0 {
set.StringVar(
&c.flagConnection.Server.Address,
"server-addr",
"",
set.Description("Address for the server."),
)
set.BoolVar(
&c.flagConnection.Server.Tls,
"server-tls",
true,
set.Description("True if the server should be connected to via TLS."),
)
set.BoolVar(
&c.flagConnection.Server.TlsSkipVerify,
"server-tls-skip-verify",
false,
set.Description("True to skip verification of the TLS certificate advertised by the server."),
)
}
if f != nil {
// Configure our values
f(set)
}
return set
}
// flagSetBit is used with baseCommand.flagSet
type flagSetBit uint
const (
flagSetNone flagSetBit = 1 << iota
flagSetOperation // shared flags for operations (build, deploy, etc)
flagSetConnection // shared flags for server connections
)
const MaxStringMapArgs int = 50
var (
// ErrSentinel is a sentinel value that we can return from Init to force an exit.
ErrSentinel = errors.New("error sentinel")
errMachineModeSingle = strings.TrimSpace(`
This command requires a single targeted machine. You have multiple machines defined
so you can specify the machine to target using the "-machine" flag.
`)
reMachineTarget = regexp.MustCompile(`^(?P<machine>[-0-9A-Za-z_]+)$`)
)

74
internal/cli/base_init.go Normal file
View File

@ -0,0 +1,74 @@
package cli
import (
"errors"
"fmt"
"path/filepath"
clientpkg "github.com/hashicorp/vagrant/internal/client"
configpkg "github.com/hashicorp/vagrant/internal/config"
)
// This file contains the various methods that are used to perform
// the Init call on baseCommand. They are broken down into individual
// smaller methods for readability but more importantly to power the
// "init" subcommand. This allows us to share as much logic as possible
// between Init and "init" to help ensure that "init" succeeding means that
// other commands will succeed as well.
// initConfig initializes the configuration.
func (c *baseCommand) initConfig(optional bool) (*configpkg.Config, error) {
path, err := c.initConfigPath()
if err != nil {
return nil, err
}
if path == "" {
if optional {
return nil, nil
}
return nil, errors.New("A Vagrant configuration file is required but wasn't found.")
}
return c.initConfigLoad(path)
}
// initConfigPath returns the configuration path to load.
func (c *baseCommand) initConfigPath() (string, error) {
// This configuarion is for the Vagrant process, not the same as a Vagrantfile
path, err := configpkg.FindPath("", "vagrant-config.hcl")
if err != nil {
return "", fmt.Errorf("Error looking for a Vagrant configuration: %s", err)
}
return path, nil
}
// initConfigLoad loads the configuration at the given path.
func (c *baseCommand) initConfigLoad(path string) (*configpkg.Config, error) {
cfg, err := configpkg.Load(path, filepath.Dir(path))
if err != nil {
return nil, err
}
// Validate
if err := cfg.Validate(); err != nil {
return nil, err
}
return cfg, nil
}
// initClient initializes the client.
func (c *baseCommand) initClient() (*clientpkg.Basis, error) {
// Start building our client options
opts := []clientpkg.Option{
clientpkg.WithLabels(c.flagLabels),
clientpkg.WithSourceOverrides(c.flagRemoteSource),
clientpkg.WithConfig(c.cfg),
}
// Create our client
return clientpkg.New(c.Ctx, opts...)
}

View File

@ -0,0 +1,250 @@
// Code generated by go-bindata. DO NOT EDIT.
// sources:
package datagen
import (
"bytes"
"compress/gzip"
"crypto/sha256"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
func bindataRead(data, name string) ([]byte, error) {
gz, err := gzip.NewReader(strings.NewReader(data))
if err != nil {
return nil, fmt.Errorf("read %q: %w", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
if err != nil {
return nil, fmt.Errorf("read %q: %w", name, err)
}
clErr := gz.Close()
if clErr != nil {
return nil, clErr
}
return buf.Bytes(), nil
}
type asset struct {
bytes []byte
info os.FileInfo
digest [sha256.Size]byte
}
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
}
func (fi bindataFileInfo) Name() string {
return fi.name
}
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi bindataFileInfo) IsDir() bool {
return false
}
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
canonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[canonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// AssetString returns the asset contents as a string (instead of a []byte).
func AssetString(name string) (string, error) {
data, err := Asset(name)
return string(data), err
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// MustAssetString is like AssetString but panics when Asset would return an
// error. It simplifies safe initialization of global variables.
func MustAssetString(name string) string {
return string(MustAsset(name))
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
canonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[canonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetDigest returns the digest of the file with the given name. It returns an
// error if the asset could not be found or the digest could not be loaded.
func AssetDigest(name string) ([sha256.Size]byte, error) {
canonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[canonicalName]; ok {
a, err := f()
if err != nil {
return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err)
}
return a.digest, nil
}
return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name)
}
// Digests returns a map of all known files and their checksums.
func Digests() (map[string][sha256.Size]byte, error) {
mp := make(map[string][sha256.Size]byte, len(_bindata))
for name := range _bindata {
a, err := _bindata[name]()
if err != nil {
return nil, err
}
mp[name] = a.digest
}
return mp, nil
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){}
// AssetDebug is true if the assets were built with the debug flag enabled.
const AssetDebug = false
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"},
// AssetDir("data/img") would return []string{"a.png", "b.png"},
// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
canonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(canonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{}}
// RestoreAsset restores an asset under the given directory.
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
}
// RestoreAssets restores an asset under the given directory recursively.
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
canonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...)
}

120
internal/cli/dynamic.go Normal file
View File

@ -0,0 +1,120 @@
package cli
import (
"context"
"errors"
"reflect"
"strconv"
"github.com/DavidGamba/go-getoptions"
"github.com/DavidGamba/go-getoptions/option"
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"github.com/hashicorp/vagrant/internal/client"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
)
type DynamicCommand struct {
*baseCommand
name string
synopsis string
help string
flags []*option.Option
flagData map[string]interface{}
}
func (c *DynamicCommand) Run(args []string) int {
if err := c.Init(
WithArgs(args),
WithFlags(c.Flags()),
); err != nil {
return 1
}
err := c.Do(c.Ctx, func(ctx context.Context, tasker Tasker) error {
tasker.UI().Output("Running "+c.name+"... ", terminal.WithHeaderStyle())
taskArgs := &vagrant_plugin_sdk.Command_Arguments{
Args: args,
Flags: []*vagrant_plugin_sdk.Command_Arguments_Flag{},
}
for k, v := range c.flagData {
f := &vagrant_plugin_sdk.Command_Arguments_Flag{Name: k}
switch reflect.Indirect(reflect.ValueOf(v)).Kind() {
case reflect.String:
f.Value = &vagrant_plugin_sdk.Command_Arguments_Flag_String_{
String_: *v.(*string),
}
f.Type = vagrant_plugin_sdk.Command_Arguments_Flag_STRING
case reflect.Bool:
f.Value = &vagrant_plugin_sdk.Command_Arguments_Flag_Bool{
Bool: *v.(*bool),
}
f.Type = vagrant_plugin_sdk.Command_Arguments_Flag_BOOL
}
taskArgs.Flags = append(taskArgs.Flags, f)
}
result, err := tasker.Task(ctx, &vagrant_server.Job_RunOp{
Task: &vagrant_server.Task{
Scope: &vagrant_server.Task_Machine{
Machine: tasker.(*client.Machine).Ref(),
},
Task: c.name,
Component: &vagrant_server.Component{
Type: vagrant_server.Component_COMMAND,
Name: c.name,
},
CliArgs: taskArgs,
},
})
if err != nil {
tasker.UI().Output("Running of task "+c.name+" failed unexpectedly\n", terminal.WithErrorStyle())
tasker.UI().Output("Error: "+err.Error(), terminal.WithErrorStyle())
} else if !result.RunResult {
tasker.UI().Output("Error: "+result.RunError.Message+"\n", terminal.WithErrorStyle())
err = errors.New("execution failed")
}
c.Log.Debug("result from operation", "task", c.name, "result", result)
return err
})
if err != nil {
return 1
}
return 0
}
func (c *DynamicCommand) Synopsis() string {
return c.synopsis
}
func (c *DynamicCommand) Help() string {
return c.help
}
func (c *DynamicCommand) Flags() *getoptions.GetOpt {
return c.flagSet(flagSetOperation, func(opts *getoptions.GetOpt) {
for _, f := range c.flags {
switch f.OptType {
case option.BoolType:
b, _ := strconv.ParseBool(f.DefaultStr)
c.flagData[f.Name] = opts.Bool(
f.Name,
b,
opts.Description(f.Description),
)
case option.StringType:
c.flagData[f.Name] = opts.String(
f.Name,
f.DefaultStr,
opts.Description(f.Description),
)
}
}
})
}

144
internal/cli/help.go Normal file
View File

@ -0,0 +1,144 @@
package cli
import (
"bytes"
"regexp"
"strings"
"github.com/mitchellh/cli"
"github.com/mitchellh/go-glint"
)
// formatHelp takes a raw help string and attempts to colorize it automatically.
func formatHelp(v string) string {
// Trim the empty space
v = strings.TrimSpace(v)
var buf bytes.Buffer
d := glint.New()
d.SetRenderer(&glint.TerminalRenderer{
Output: &buf,
// We set rows/cols here manually. The important bit is the cols
// needs to be wide enough so glint doesn't clamp any text and
// lets the terminal just autowrap it. Rows doesn't make a big
// difference.
Rows: 10,
Cols: 180,
})
for _, line := range strings.Split(v, "\n") {
// Usage: prefix lines
prefix := "Usage: "
if strings.HasPrefix(line, prefix) {
d.Append(glint.Layout(
glint.Style(
glint.Text(prefix),
glint.Color("lightMagenta"),
),
glint.Text(line[len(prefix):]),
).Row())
continue
}
// Alias: prefix lines
prefix = "Alias: "
if strings.HasPrefix(line, prefix) {
d.Append(glint.Layout(
glint.Style(
glint.Text(prefix),
glint.Color("lightMagenta"),
),
glint.Text(line[len(prefix):]),
).Row())
continue
}
// A header line
if reHelpHeader.MatchString(line) {
d.Append(glint.Style(
glint.Text(line),
glint.Bold(),
))
continue
}
// If we have a command in the line, then highlight that.
if matches := reCommand.FindAllStringIndex(line, -1); len(matches) > 0 {
var cs []glint.Component
idx := 0
for _, match := range matches {
start := match[0] + 1
end := match[1] - 1
cs = append(
cs,
glint.Text(line[idx:start]),
glint.Style(
glint.Text(line[start:end]),
glint.Color("lightMagenta"),
),
)
idx = end
}
// Add the rest of the text
cs = append(cs, glint.Text(line[idx:]))
d.Append(glint.Layout(cs...).Row())
continue
}
// Normal line
d.Append(glint.Text(line))
}
d.RenderFrame()
return buf.String()
}
type helpCommand struct {
SynopsisText string
HelpText string
}
func (c *helpCommand) Run(args []string) int {
return cli.RunResultHelp
}
func (c *helpCommand) Synopsis() string {
return strings.TrimSpace(c.SynopsisText)
}
func (c *helpCommand) Help() string {
if c.HelpText == "" {
return c.SynopsisText
}
return formatHelp(c.HelpText)
}
func (c *helpCommand) HelpTemplate() string {
return formatHelp(helpTemplate)
}
var (
reHelpHeader = regexp.MustCompile(`^[a-zA-Z0-9_-].*:$`)
reCommand = regexp.MustCompile(`"vagrant \w+"`)
)
const helpTemplate = `
Usage: {{.Name}} {{.SubcommandName}} SUBCOMMAND
{{indent 2 (trim .Help)}}{{if gt (len .Subcommands) 0}}
Subcommands:
{{- range $value := .Subcommands }}
{{ $value.NameAligned }} {{ $value.Synopsis }}{{ end }}
{{- end }}
`

401
internal/cli/main.go Normal file
View File

@ -0,0 +1,401 @@
package cli
//go:generate go-bindata -nomemcopy -nometadata -pkg datagen -o datagen/datagen.go -prefix data/ data/...
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"text/tabwriter"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/mitchellh/cli"
"github.com/mitchellh/go-glint"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"github.com/hashicorp/vagrant/internal/core"
"github.com/hashicorp/vagrant/internal/pkg/signalcontext"
"github.com/hashicorp/vagrant/internal/version"
)
const (
// EnvLogLevel is the env var to set with the log level.
EnvLogLevel = "VAGRANT_LOG_LEVEL"
// EnvPlain is the env var that can be set to force plain output mode.
EnvPlain = "VAGRANT_PLAIN"
)
var (
// cliName is the name of this CLI.
cliName = "vagrant"
// commonCommands are the commands that are deemed "common" and shown first
// in the CLI help output.
commonCommands = []string{
"up",
"destroy",
"halt",
"status",
"reload",
}
// hiddenCommands are not shown in CLI help output.
hiddenCommands = map[string]struct{}{
"plugin-run": {},
}
ExposeDocs bool
)
// Main runs the CLI with the given arguments and returns the exit code.
// The arguments SHOULD include argv[0] as the program name.
func Main(args []string) int {
// Clean up all our plugins so we don't leave any dangling processes.
// Note that this is a "just in case" catch. We should be properly cleaning
// up plugin processes by calling Close on all the resources we use.
defer plugin.CleanupClients()
// Initialize our logger based on env vars
args, log, logOutput, err := logger(args)
if err != nil {
panic(err)
}
// Log our versions
vsn := version.GetVersion()
log.Info("vagrant version",
"full_string", vsn.FullVersionNumber(true),
"version", vsn.Version,
"prerelease", vsn.VersionPrerelease,
"metadata", vsn.VersionMetadata,
"revision", vsn.Revision,
)
// Build our cancellation context
ctx, closer := signalcontext.WithInterrupt(context.Background(), log)
defer closer()
// Get our base command
base, commands, err := Commands(ctx, args, log, logOutput)
if err != nil {
panic(err)
}
defer base.Close()
// Build the CLI
cli := &cli.CLI{
Name: args[0],
Args: args[1:],
Commands: commands,
Autocomplete: true,
AutocompleteNoDefaultFlags: true,
HelpFunc: GroupedHelpFunc(cli.BasicHelpFunc(cliName)),
}
// Run the CLI
exitCode, err := cli.Run()
if err != nil {
panic(err)
}
return exitCode
}
// commands returns the map of commands that can be used to initialize a CLI.
func Commands(
ctx context.Context,
args []string,
log hclog.Logger,
logOutput io.Writer,
opts ...Option,
) (*baseCommand, map[string]cli.CommandFactory, error) {
commands := make(map[string]cli.CommandFactory)
bc := &baseCommand{
Ctx: ctx,
Log: log,
LogOutput: logOutput,
}
// fetch plugin builtin commands
commands["plugin-run"] = func() (cli.Command, error) {
return &PluginCommand{
baseCommand: bc,
}, nil
}
// If running a builtin don't do all the setup
if len(args) > 1 && args[1] == "plugin-run" {
return bc, commands, nil
}
baseCommand, err := BaseCommand(ctx, log, logOutput,
WithArgs(args),
)
if err != nil {
return nil, nil, err
}
basis := baseCommand.basis
// // Using a custom UI here to prevent weird output behavior
// // TODO(spox): make this better (like respecting noninteractive, etc)
ui := terminal.ConsoleUI(ctx)
s := ui.Status()
s.Update("Loading Vagrant...")
result, err := basis.Commands(ctx, nil)
if err != nil {
s.Step(terminal.StatusError, "Failed to load Vagrant!")
return nil, nil, err
}
s.Step(terminal.StatusOK, "Vagrant loaded!")
s.Close()
if closer, ok := ui.(io.Closer); ok {
closer.Close()
}
// Set plain mode if set
if os.Getenv(EnvPlain) != "" {
baseCommand.globalOptions = append(baseCommand.globalOptions,
WithUI(terminal.NonInteractiveUI(ctx)))
}
// aliases is a list of command aliases we have. The key is the CLI
// command (the alias) and the value is the existing target command.
aliases := map[string]string{}
// fetch remaining builtin commands
commands["version"] = func() (cli.Command, error) {
return &VersionCommand{
baseCommand: baseCommand,
VersionInfo: version.GetVersion(),
}, nil
}
// add dynamic commands
// TODO(spox): reverse the setup here so we load
// dynamic commands first and then define
// any builtin commands on top so the builtin
// commands have proper precedence.
for i := 0; i < len(result.Commands); i++ {
n := result.Commands[i]
flgs, _ := core.ProtoToFlagsMapper(n.Flags)
if _, ok := commands[n.Name]; !ok {
commands[n.Name] = func() (cli.Command, error) {
return &DynamicCommand{
baseCommand: baseCommand,
name: n.Name,
synopsis: n.Synopsis,
help: n.Help,
flags: flgs,
flagData: make(map[string]interface{}),
}, nil
}
}
}
// fetch all known plugin commands
commands["plugin"] = func() (cli.Command, error) {
return &PluginCommand{
baseCommand: baseCommand,
}, nil
}
commands["version"] = func() (cli.Command, error) {
return &VersionCommand{
baseCommand: baseCommand,
VersionInfo: version.GetVersion(),
}, nil
}
// register our aliases
for from, to := range aliases {
commands[from] = commands[to]
}
return baseCommand, commands, nil
}
// logger returns the logger to use for the CLI. Output, level, etc. are
// determined based on environment variables if set.
func logger(args []string) ([]string, hclog.Logger, io.Writer, error) {
app := args[0]
// Determine our log level if we have any. First override we check if env var
level := hclog.NoLevel
if v := os.Getenv(EnvLogLevel); v != "" {
level = hclog.LevelFromString(v)
if level == hclog.NoLevel {
return nil, nil, nil, fmt.Errorf("%s value %q is not a valid log level", EnvLogLevel, v)
}
}
// Process arguments looking for `-v` flags to control the log level.
// This overrides whatever the env var set.
var outArgs []string
for _, arg := range args {
if len(arg) != 0 && arg[0] != '-' {
outArgs = append(outArgs, arg)
continue
}
switch arg {
case "-v":
if level == hclog.NoLevel || level > hclog.Info {
level = hclog.Info
}
case "-vv":
if level == hclog.NoLevel || level > hclog.Debug {
level = hclog.Debug
}
case "-vvv":
if level == hclog.NoLevel || level > hclog.Trace {
level = hclog.Trace
}
default:
outArgs = append(outArgs, arg)
}
}
// Default output is nowhere unless we enable logging.
var output io.Writer = ioutil.Discard
color := hclog.ColorOff
if level != hclog.NoLevel {
output = os.Stderr
color = hclog.AutoColor
}
logger := hclog.New(&hclog.LoggerOptions{
Name: app,
Level: level,
Color: color,
Output: output,
})
return outArgs, logger, output, nil
}
func GroupedHelpFunc(f cli.HelpFunc) cli.HelpFunc {
return func(commands map[string]cli.CommandFactory) string {
var buf bytes.Buffer
d := glint.New()
d.SetRenderer(&glint.TerminalRenderer{
Output: &buf,
// We set rows/cols here manually. The important bit is the cols
// needs to be wide enough so glint doesn't clamp any text and
// lets the terminal just autowrap it. Rows doesn't make a big
// difference.
Rows: 10,
Cols: 180,
})
// Header
d.Append(glint.Style(
glint.Text("Welcome to Vagrant"),
glint.Bold(),
))
d.Append(glint.Layout(
glint.Style(
glint.Text("Docs:"),
glint.Color("lightBlue"),
),
glint.Text(" "),
glint.Text("https://vagrantup.com"),
).Row())
d.Append(glint.Layout(
glint.Style(
glint.Text("Version:"),
glint.Color("green"),
),
glint.Text(" "),
glint.Text(version.GetVersion().VersionNumber()),
).Row())
d.Append(glint.Text(""))
// Usage
d.Append(glint.Layout(
glint.Style(
glint.Text("Usage:"),
glint.Color("lightMagenta"),
),
glint.Text(" "),
glint.Text(cliName),
glint.Text(" "),
glint.Text("[-version] [-help] [-autocomplete-(un)install] <command> [args]"),
).Row())
d.Append(glint.Text(""))
// Add common commands
helpCommandsSection(d, "Common commands", commonCommands, commands)
// Make our other commands
ignoreMap := map[string]struct{}{}
for k := range hiddenCommands {
ignoreMap[k] = struct{}{}
}
for _, k := range commonCommands {
ignoreMap[k] = struct{}{}
}
var otherCommands []string
for k := range commands {
if _, ok := ignoreMap[k]; ok {
continue
}
otherCommands = append(otherCommands, k)
}
sort.Strings(otherCommands)
// Add other commands
helpCommandsSection(d, "Other commands", otherCommands, commands)
d.RenderFrame()
return buf.String()
}
}
func helpCommandsSection(
d *glint.Document,
header string,
commands []string,
factories map[string]cli.CommandFactory,
) {
// Header
d.Append(glint.Style(
glint.Text(header),
glint.Bold(),
))
// Build our commands
var b bytes.Buffer
tw := tabwriter.NewWriter(&b, 0, 2, 6, ' ', 0)
for _, k := range commands {
fn, ok := factories[k]
if !ok {
continue
}
cmd, err := fn()
if err != nil {
panic(fmt.Sprintf("failed to load %q command: %s", k, err))
}
fmt.Fprintf(tw, "%s\t%s\n", k, cmd.Synopsis())
}
tw.Flush()
d.Append(glint.Layout(
glint.Text(b.String()),
).PaddingLeft(2))
}
var helpText = map[string][2]string{}

77
internal/cli/option.go Normal file
View File

@ -0,0 +1,77 @@
package cli
import (
"github.com/DavidGamba/go-getoptions"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
)
// Option is used to configure Init on baseCommand.
type Option func(c *baseConfig)
// WithArgs sets the arguments to the command that are used for parsing.
// Remaining arguments can be accessed using your flag set and asking for Args.
// Example: c.Flags().Args().
func WithArgs(args []string) Option {
return func(c *baseConfig) { c.Args = args }
}
// WithFlags sets the flags that are supported by this command. This MUST
// be set otherwise a panic will happen. This is usually set by just calling
// the Flags function on your command implementation.
func WithFlags(f *getoptions.GetOpt) Option {
return func(c *baseConfig) { c.Flags = f }
}
// TODO(spox): needs to be updated to using arg value for machine name
// WithSingleMachine configures the CLI to expect a configuration with
// one or more machines defined but a single machine targeted with `-app`.
// If only a single machine exists, it is implicitly the target.
// Zero machine is an error.
func WithSingleMachine() Option {
return func(c *baseConfig) {
c.MachineTargetRequired = true
c.Config = false
c.Client = true
}
}
// WithNoConfig configures the CLI to not expect any project configuration.
// This will not read any configuration files.
func WithNoConfig() Option {
return func(c *baseConfig) {
c.Config = false
}
}
// WithConfig configures the CLI to find and load any project configuration.
// If optional is true, no error will be shown if a config can't be found.
func WithConfig(optional bool) Option {
return func(c *baseConfig) {
c.Config = true
c.ConfigOptional = optional
}
}
// WithClient configures the CLI to initialize a client.
func WithClient(v bool) Option {
return func(c *baseConfig) {
c.Client = v
}
}
// WithUI configures the CLI to use a specific UI implementation
func WithUI(ui terminal.UI) Option {
return func(c *baseConfig) {
c.UI = ui
}
}
type baseConfig struct {
Args []string
Flags *getoptions.GetOpt
Config bool
ConfigOptional bool
Client bool
MachineTargetRequired bool
UI terminal.UI
}

29
internal/cli/plugin.go Normal file
View File

@ -0,0 +1,29 @@
package cli
import (
"github.com/hashicorp/vagrant/internal/plugin"
"github.com/hashicorp/vagrant-plugin-sdk"
)
type PluginCommand struct {
*baseCommand
}
func (c *PluginCommand) Run(args []string) int {
plugin, ok := plugin.Builtins[args[0]]
if !ok {
panic("no such plugin: " + args[0])
}
// Run the plugin
sdk.Main(plugin...)
return 0
}
func (c *PluginCommand) Synopsis() string {
return "Execute a built-in plugin."
}
func (c *PluginCommand) Help() string {
return ""
}

120
internal/cli/ui.go Normal file
View File

@ -0,0 +1,120 @@
package cli
import (
"fmt"
"strings"
"time"
"github.com/DavidGamba/go-getoptions"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"github.com/hashicorp/vagrant/internal/clierrors"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
"github.com/skratchdot/open-golang/open"
)
type UICommand struct {
*baseCommand
flagAuthenticate bool
}
func (c *UICommand) Run(args []string) int {
// Initialize. If we fail, we just exit since Init handles the UI.
if err := c.Init(
WithArgs(args),
WithFlags(c.Flags()),
WithNoConfig(),
); err != nil {
return 1
}
if c.basis.Local() {
c.basis.UI().Output("Vagrant must be configured in server mode to access the UI", terminal.WithWarningStyle())
}
// Get our API client
client := c.basis.Client()
var inviteToken string
if c.flagAuthenticate {
c.ui.Output("Creating invite token", terminal.WithStyle(terminal.HeaderStyle))
c.ui.Output("This invite token will be exchanged for an authentication \ntoken that your browser stores.")
resp, err := client.GenerateInviteToken(c.Ctx, &vagrant_server.InviteTokenRequest{
Duration: (2 * time.Minute).String(),
})
if err != nil {
c.basis.UI().Output(clierrors.Humanize(err), terminal.WithErrorStyle())
return 1
}
inviteToken = resp.Token
}
// Get our default context (used context)
name, err := c.contextStorage.Default()
if err != nil {
c.basis.UI().Output(clierrors.Humanize(err), terminal.WithErrorStyle())
return 1
}
ctxConfig, err := c.contextStorage.Load(name)
if err != nil {
c.basis.UI().Output(clierrors.Humanize(err), terminal.WithErrorStyle())
return 1
}
// todo(mitchellh: current default port is hardcoded, cannot configure http address)
addr := strings.Split(ctxConfig.Server.Address, ":")[0]
// Default Docker platform HTTP port, for now
port := 9702
if err != nil {
c.basis.UI().Output(clierrors.Humanize(err), terminal.WithErrorStyle())
return 1
}
c.ui.Output("Opening browser", terminal.WithStyle(terminal.HeaderStyle))
uiAddr := fmt.Sprintf("https://%s:%d", addr, port)
if c.flagAuthenticate {
uiAddr = fmt.Sprintf("%s/auth/invite?token=%s&cli=true", uiAddr, inviteToken)
}
open.Run(uiAddr)
return 0
}
func (c *UICommand) Flags() *getoptions.GetOpt {
return c.flagSet(0, func(set *getoptions.GetOpt) {
set.BoolVar(
&c.flagAuthenticate,
"authenticate",
false,
set.Description("Creates a new invite token and passes it to the UI for authorization"),
)
})
}
// func (c *UICommand) AutocompleteArgs() complete.Predictor {
// return complete.PredictNothing
// }
// func (c *UICommand) AutocompleteFlags() complete.Flags {
// return c.Flags().Completions()
// }
func (c *UICommand) Synopsis() string {
return "Open the web UI"
}
func (c *UICommand) Help() string {
return formatHelp(`
Usage: vagrant ui [options]
Opens the new UI. When provided a flag, will automatically open the
token invite page with an invite token for authentication.
` + c.Flags().Help())
}

58
internal/cli/version.go Normal file
View File

@ -0,0 +1,58 @@
package cli
import (
"github.com/DavidGamba/go-getoptions"
"github.com/hashicorp/vagrant/internal/version"
)
type VersionCommand struct {
*baseCommand
VersionInfo *version.VersionInfo
}
func (c *VersionCommand) Run(args []string) int {
flagSet := c.Flags()
// Initialize. If we fail, we just exit since Init handles the UI.
if err := c.Init(
WithArgs(args),
WithFlags(flagSet),
WithNoConfig(),
WithClient(false),
); err != nil {
return 1
}
out := c.VersionInfo.FullVersionNumber(true)
c.ui.Output(out)
return 0
}
func (c *VersionCommand) Flags() *getoptions.GetOpt {
return c.flagSet(0, nil)
}
// func (c *VersionCommand) AutocompleteArgs() complete.Predictor {
// return complete.PredictNothing
// }
// func (c *VersionCommand) AutocompleteFlags() complete.Flags {
// return c.Flags().Completions()
// }
func (c *VersionCommand) Synopsis() string {
return "Prints the version of this Vagrant CLI"
}
func (c *VersionCommand) Help() string {
return formatHelp(`
Usage: vagrant version
Prints the version of this Vagrant CLI.
There are no arguments or flags to this command. Any additional arguments or
flags are ignored.
`)
}

View File

@ -0,0 +1,33 @@
package clicontext
import (
"io"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/hashicorp/vagrant-plugin-sdk/helper/path"
"github.com/hashicorp/vagrant/internal/serverconfig"
)
// Config is the structure of the context configuration file. This structure
// can be decoded with hclsimple.DecodeFile.
type Config struct {
// Server is the configuration to talk to a Vagrant server.
Server serverconfig.Client `hcl:"server,block"`
}
// LoadPath loads a context configuration from a filepath.
func LoadPath(p path.Path) (*Config, error) {
var cfg Config
err := hclsimple.DecodeFile(p.String(), nil, &cfg)
return &cfg, err
}
// WriteTo implements io.WriterTo and encodes this config as HCL.
func (c *Config) WriteTo(w io.Writer) (int64, error) {
f := hclwrite.NewFile()
gohcl.EncodeIntoBody(c, f.Body())
return f.WriteTo(w)
}

View File

@ -0,0 +1,274 @@
package clicontext
import (
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/hashicorp/vagrant-plugin-sdk/helper/path"
)
// Storage is the primary struct for interacting with stored CLI contexts.
// Contexts are always stored directly on disk with one set as the default.
type Storage struct {
dir path.Path
noSymlink bool
}
// NewStorage initializes context storage.
func NewStorage(opts ...Option) (*Storage, error) {
var m Storage
for _, opt := range opts {
if err := opt(&m); err != nil {
return nil, err
}
}
return &m, nil
}
// List lists the contexts that are available.
func (m *Storage) List() ([]string, error) {
f, err := os.Open(m.dir.String())
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
defer f.Close()
names, err := f.Readdirnames(-1)
if err != nil {
return nil, err
}
// Remove all our _-prefixed names which are system settings.
result := make([]string, 0, len(names))
for _, n := range names {
if n[0] == '_' {
continue
}
result = append(result, m.nameFromPath(path.NewPath(n)))
}
return result, nil
}
// Load loads a context with the given name.
func (m *Storage) Load(n string) (*Config, error) {
return LoadPath(m.configPath(n))
}
// Set will set a new configuration with the given name. This will
// overwrite any existing context of this name.
func (m *Storage) Set(n string, c *Config) error {
path := m.configPath(n)
if err := os.MkdirAll(path.Dir().String(), 0755); err != nil {
return err
}
f, err := os.Create(path.String())
if err != nil {
return err
}
defer f.Close()
_, err = c.WriteTo(f)
if err != nil {
return err
}
// If we have no default, set as the default
def, err := m.Default()
if err != nil {
return err
}
if def == "" {
err = m.SetDefault(n)
}
return err
}
// Rename renames a context. This will error if the "from" context does not
// exist. If "from" is the default context then the default will be switched
// to "to". If "to" already exists, this will overwrite it.
func (m *Storage) Rename(from, to string) error {
fromPath := m.configPath(from)
if _, err := os.Stat(fromPath.String()); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("context %q does not exist", from)
}
return err
}
if err := m.Delete(to); err != nil {
return err
}
toPath := m.configPath(to)
if err := os.Rename(fromPath.String(), toPath.String()); err != nil {
return err
}
def, err := m.Default()
if err != nil {
return err
}
if def == from {
return m.SetDefault(to)
}
return nil
}
// Delete deletes the context with the given name.
func (m *Storage) Delete(n string) error {
// Remove it
err := os.Remove(m.configPath(n).String())
if os.IsNotExist(err) {
err = nil
}
if err != nil {
return err
}
// If our default is this, then unset the default
def, err := m.Default()
if err != nil {
return err
}
if def == n {
err = m.UnsetDefault()
}
return err
}
// SetDefault sets the default context to use. If the given context
// doesn't exist, an os.IsNotExist error will be returned.
func (m *Storage) SetDefault(n string) error {
src := m.configPath(n)
if _, err := os.Stat(src.String()); err != nil {
return err
}
// Attempt to create a symlink
defaultPath := m.defaultPath()
if !m.noSymlink {
err := m.createSymlink(src, defaultPath)
if err == nil {
return nil
}
}
// If the symlink fails, then we use a plain file approach. The downside
// of this approach is that it is not atomic (on Windows it is impossible
// to have atomic writes) so we only do it on error cases.
return ioutil.WriteFile(defaultPath.String(), []byte(n), 0644)
}
// UnsetDefault unsets the default context.
func (m *Storage) UnsetDefault() error {
err := os.Remove(m.defaultPath().String())
if os.IsNotExist(err) {
err = nil
}
return err
}
// Default returns the name of the default context.
func (m *Storage) Default() (string, error) {
p := m.defaultPath()
fi, err := os.Lstat(p.String())
if err != nil {
if os.IsNotExist(err) {
err = nil
}
return "", err
}
// Symlinks are based on the resulting symlink path
if fi.Mode()&os.ModeSymlink != 0 {
pth, err := os.Readlink(p.String())
if err != nil {
return "", err
}
return m.nameFromPath(path.NewPath(pth)), nil
}
// If this is a regular file then we just read it cause it a non-symlink mode.
contents, err := ioutil.ReadFile(p.String())
if err != nil {
return "", err
}
return string(contents), nil
}
func (m *Storage) createSymlink(src, dst path.Path) error {
// delete the old symlink
err := os.Remove(dst.String())
if err != nil && !os.IsNotExist(err) {
return err
}
err = os.Symlink(src.String(), dst.String())
// On Windows when creating a symlink the Windows API can incorrectly
// return an error message when not running as Administrator even when the symlink
// is correctly created.
// Manually validate the symlink was correctly created before returning an error
ln, ferr := os.Readlink(dst.String())
if ferr != nil {
// symlink has not been created return the original error
return err
}
if ln != src.String() {
return err
}
return nil
}
// nameFromPath returns the context name given a path to a context
// HCL file. This is just the name of the file without any extension.
func (m *Storage) nameFromPath(p path.Path) string {
return strings.Replace(p.Base().String(), p.Ext(), "", 1)
}
func (m *Storage) configPath(n string) path.Path {
return m.dir.Join(n + ".hcl")
}
func (m *Storage) defaultPath() path.Path {
return m.dir.Join("_default.hcl")
}
type Option func(*Storage) error
// WithDir specifies the directory where context configuration will be stored.
// This doesn't have to exist already but we must have permission to create it.
func WithDir(d path.Path) Option {
return func(m *Storage) error {
m.dir = d
return nil
}
}
// WithNoSymlink disables all symlink usage in the Storage. If symlinks were
// used previously then they'll still work.
func WithNoSymlink() Option {
return func(m *Storage) error {
m.noSymlink = true
return nil
}
}

View File

@ -0,0 +1,163 @@
package clicontext
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestStorage_workflow(t *testing.T) {
require := require.New(t)
st := TestStorage(t)
// Initially empty
{
list, err := st.List()
require.NoError(err)
require.Empty(list)
def, err := st.Default()
require.NoError(err)
require.Empty(def)
}
// Add a context
cfg := &Config{}
require.NoError(st.Set("hello", cfg))
// Should not be empty anymore
{
list, err := st.List()
require.NoError(err)
require.Len(list, 1)
require.Equal("hello", list[0])
}
{
// Should be the default since we didn't have one before.
def, err := st.Default()
require.NoError(err)
require.Equal("hello", def)
}
// Should be able to load
{
actual, err := st.Load("hello")
require.NoError(err)
require.Equal(cfg, actual)
}
// Should be able to rename
{
err := st.Rename("hello", "goodbye")
require.NoError(err)
// Should be the default since we didn't have one before.
def, err := st.Default()
require.NoError(err)
require.Equal("goodbye", def)
// Should only have this one
list, err := st.List()
require.NoError(err)
require.Len(list, 1)
require.Equal("goodbye", list[0])
}
// Should be able to delete
require.NoError(st.Delete("goodbye"))
// Should be empty again
{
list, err := st.List()
require.NoError(err)
require.Empty(list)
def, err := st.Default()
require.NoError(err)
require.Empty(def)
}
}
func TestStorage_workflowNoSymlink(t *testing.T) {
require := require.New(t)
st := TestStorage(t)
st.noSymlink = true
// Initially empty
{
list, err := st.List()
require.NoError(err)
require.Empty(list)
def, err := st.Default()
require.NoError(err)
require.Empty(def)
}
// Add a context
cfg := &Config{}
require.NoError(st.Set("hello", cfg))
// Should not be empty anymore
{
list, err := st.List()
require.NoError(err)
require.Len(list, 1)
require.Equal("hello", list[0])
}
{
// Should be the default since we didn't have one before.
def, err := st.Default()
require.NoError(err)
require.Equal("hello", def)
}
// Should be able to load
{
actual, err := st.Load("hello")
require.NoError(err)
require.Equal(cfg, actual)
}
// Should be able to rename
{
err := st.Rename("hello", "goodbye")
require.NoError(err)
// Should be the default since we didn't have one before.
def, err := st.Default()
require.NoError(err)
require.Equal("goodbye", def)
// Should only have this one
list, err := st.List()
require.NoError(err)
require.Len(list, 1)
require.Equal("goodbye", list[0])
}
// Should be able to delete
require.NoError(st.Delete("goodbye"))
// Should be empty again
{
list, err := st.List()
require.NoError(err)
require.Empty(list)
def, err := st.Default()
require.NoError(err)
require.Empty(def)
}
}
func TestStorage_deleteNonExist(t *testing.T) {
require := require.New(t)
st := TestStorage(t)
require.NoError(st.Delete("nope"))
}

View File

@ -0,0 +1,23 @@
package clicontext
import (
"io/ioutil"
"os"
"github.com/hashicorp/vagrant-plugin-sdk/helper/path"
"github.com/mitchellh/go-testing-interface"
"github.com/stretchr/testify/require"
)
// TestStorage returns a *Storage pointed at a temporary directory. This
// will cleanup automatically by using t.Cleanup.
func TestStorage(t testing.T) *Storage {
td, err := ioutil.TempDir("", "vagrant-test")
require.NoError(t, err)
t.Cleanup(func() { os.RemoveAll(td) })
st, err := NewStorage(WithDir(path.NewPath(td)))
require.NoError(t, err)
return st
}

296
internal/client/basis.go Normal file
View File

@ -0,0 +1,296 @@
package client
import (
"context"
"io"
// "fmt"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vagrant-plugin-sdk/helper/paths"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
configpkg "github.com/hashicorp/vagrant/internal/config"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
"github.com/hashicorp/vagrant/internal/serverclient"
)
type Basis struct {
ui terminal.UI
basis *vagrant_server.Basis
Project *Project
client *serverclient.VagrantClient
logger hclog.Logger
runner *vagrant_server.Ref_Runner
cleanupFuncs []func()
config *configpkg.Config
labels map[string]string
dataSourceOverrides map[string]string
local bool
localServer bool // True when a local server is created
}
func New(ctx context.Context, opts ...Option) (*Basis, error) {
basis := &Basis{
logger: hclog.L().Named("basis"),
runner: &vagrant_server.Ref_Runner{
Target: &vagrant_server.Ref_Runner_Any{
Any: &vagrant_server.Ref_RunnerAny{},
},
},
}
// Apply any options
var cfg config
for _, opt := range opts {
err := opt(basis, &cfg)
if err != nil {
return nil, err
}
}
// If no internal basis was provided, set it up now
if basis.basis == nil {
vh, err := paths.VagrantHome()
if err != nil {
basis.logger.Error("failed to determine vagrant home", "error", err)
return nil, err
}
basis.basis = &vagrant_server.Basis{
Name: "default",
Path: vh.String(),
}
}
// If no UI was provided, create a default
if basis.ui == nil {
basis.ui = terminal.ConsoleUI(ctx)
}
// If a client was not provided, establish a new connection through
// the serverclient package, or by spinning up an in-process server
if basis.client == nil {
basis.logger.Trace("no API client provided, initializing connection if possible")
conn, err := basis.initServerClient(context.Background(), &cfg)
if err != nil {
return nil, err
}
basis.client = serverclient.WrapVagrantClient(conn)
}
// Negotiate the version
if err := basis.negotiateApiVersion(ctx); err != nil {
return nil, err
}
// Setup our basis within the database
result, err := basis.client.FindBasis(
context.Background(),
&vagrant_server.FindBasisRequest{
Basis: basis.basis,
},
)
if err == nil && result.Found {
basis.basis = result.Basis
return basis, nil
}
basis.logger.Trace("failed to locate existing basis", "basis", basis.basis,
"result", result, "error", err)
uresult, err := basis.client.UpsertBasis(
context.Background(),
&vagrant_server.UpsertBasisRequest{
Basis: basis.basis,
},
)
if err != nil {
return nil, err
}
basis.basis = uresult.Basis
return basis, nil
}
func (b *Basis) LoadProject(p *vagrant_server.Project) (*Project, error) {
result, err := b.client.FindProject(
context.Background(),
&vagrant_server.FindProjectRequest{
Project: p,
},
)
if err == nil && result.Found {
b.Project = &Project{
ui: b.ui,
basis: b,
project: result.Project,
logger: b.logger.Named("project"),
}
return b.Project, nil
}
b.logger.Trace("failed to locate existing project", "project", p,
"result", result, "error", err)
uresult, err := b.client.UpsertProject(
context.Background(),
&vagrant_server.UpsertProjectRequest{
Project: p,
},
)
if err != nil {
return nil, err
}
b.Project = &Project{
ui: b.ui,
project: uresult.Project,
basis: b,
logger: b.logger.Named("project"),
}
return b.Project, nil
}
func (b *Basis) Ref() *vagrant_server.Ref_Basis {
return &vagrant_server.Ref_Basis{
Name: b.basis.Name,
ResourceId: b.basis.ResourceId,
}
}
func (b *Basis) Close() error {
for _, f := range b.cleanupFuncs {
f()
}
if closer, ok := b.ui.(io.Closer); ok {
closer.Close()
}
return nil
}
// Client returns the raw Vagrant server API client.
func (b *Basis) Client() *serverclient.VagrantClient {
return b.client
}
// Local is true if the server is an in-process just-in-time server.
func (b *Basis) Local() bool {
return b.localServer
}
func (b *Basis) UI() terminal.UI {
return b.ui
}
func (b *Basis) cleanup(f func()) {
b.cleanupFuncs = append(b.cleanupFuncs, f)
}
type config struct {
connectOpts []serverclient.ConnectOption
}
type Option func(*Basis, *config) error
func WithBasis(pbb *vagrant_server.Basis) Option {
return func(b *Basis, cfg *config) error {
b.basis = pbb
return nil
}
}
func WithProject(p *Project) Option {
return func(b *Basis, cfg *config) error {
p.basis = b
b.Project = p
return nil
}
}
// WithClient sets the client directly. In this case, the runner won't
// attempt any connection at all regardless of other configuration (env
// vars or vagrant config file). This will be used.
func WithClient(client *serverclient.VagrantClient) Option {
return func(b *Basis, cfg *config) error {
if client != nil {
b.client = client
}
return nil
}
}
// WithClientConnect specifies the options for connecting to a client.
// If WithClient is specified, that client is always used.
//
// If WithLocal is set and no client is specified and no server creds
// can be found, then an in-process server will be created.
func WithClientConnect(opts ...serverclient.ConnectOption) Option {
return func(b *Basis, cfg *config) error {
cfg.connectOpts = opts
return nil
}
}
// WithLocal puts the client in local exec mode. In this mode, the client
// will spin up a per-operation runner locally and reference the local on-disk
// data for all operations.
func WithLocal() Option {
return func(b *Basis, cfg *config) error {
b.local = true
return nil
}
}
// WithLogger sets the logger for the client.
func WithLogger(log hclog.Logger) Option {
return func(b *Basis, cfg *config) error {
b.logger = log
return nil
}
}
// WithUI sets the UI to use for the client.
func WithUI(ui terminal.UI) Option {
return func(b *Basis, cfg *config) error {
b.ui = ui
return nil
}
}
func WithCleanup(f func()) Option {
return func(b *Basis, cfg *config) error {
b.cleanup(f)
return nil
}
}
// WithSourceOverrides sets the data source overrides for queued jobs.
func WithSourceOverrides(m map[string]string) Option {
return func(b *Basis, cfg *config) error {
b.dataSourceOverrides = m
return nil
}
}
// WithLabels sets the labels or any operations.
func WithLabels(m map[string]string) Option {
return func(b *Basis, cfg *config) error {
b.labels = m
return nil
}
}
func WithConfig(c *configpkg.Config) Option {
return func(b *Basis, cfg *config) error {
b.config = c
return nil
}
}

6
internal/client/doc.go Normal file
View File

@ -0,0 +1,6 @@
// Package client contains the Vagrant client implementation.
//
// The Vagrant client exposes a slightly higher level of abstraction
// than direct a API client for performing operations on an application.
// The primary consumer of this package is the CLI.
package client

383
internal/client/job.go Normal file
View File

@ -0,0 +1,383 @@
package client
import (
"context"
"fmt"
"io"
"time"
"github.com/hashicorp/go-hclog"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"github.com/hashicorp/vagrant/internal/pkg/finalcontext"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
)
// job returns the basic job skeleton prepoulated with the correct
// defaults based on how the client is configured. For example, for local
// operations, this will already have the targeting for the local runner.
func (b *Basis) job() *vagrant_server.Job {
job := &vagrant_server.Job{
TargetRunner: b.runner,
DataSource: &vagrant_server.Job_DataSource{
Source: &vagrant_server.Job_DataSource_Local{
Local: &vagrant_server.Job_Local{},
},
},
Operation: &vagrant_server.Job_Noop_{
Noop: &vagrant_server.Job_Noop{},
},
}
job.Basis = b.Ref()
// If we're not local, we set a nil data source so it defaults to
// whatever the project has remotely.
if !b.local {
job.DataSource = nil
}
return job
}
// doJob will queue and execute the job. If the client is configured for
// local mode, this will start and target the proper runner.
func (b *Basis) doJob(ctx context.Context, job *vagrant_server.Job, ui terminal.UI) (*vagrant_server.Job_Result, error) {
log := b.logger
if ui == nil {
ui = b.UI()
}
// cb is used in local mode only to get a callback of the job ID
// so we can tell our runner what ID to expect.
var cb func(string)
// In local mode we have to start a runner.
if b.local {
log.Info("local mode, starting local runner")
r, err := b.startRunner()
if err != nil {
return nil, err
}
log.Info("runner started", "runner_id", r.Id())
// We defer the close so that we clean up resources. Local mode
// always blocks and streams the full output so when doJob exits
// the job is complete.
defer r.Close()
var jobCh chan struct{}
defer func() {
if jobCh != nil {
log.Info("waiting for accept to finish")
<-jobCh
log.Debug("finished waiting for job accept")
}
}()
// Set our callback up so that we will accept a job once it is queued
// so that we can accept exactly this job.
cb = func(id string) {
jobCh = make(chan struct{})
go func() {
defer close(jobCh)
if err := r.AcceptExact(ctx, id); err != nil {
log.Error("runner job accept error", "err", err)
}
}()
}
// Modify the job to target this runner and use the local data source.
job.TargetRunner = &vagrant_server.Ref_Runner{
Target: &vagrant_server.Ref_Runner_Id{
Id: &vagrant_server.Ref_RunnerId{
Id: r.Id(),
},
},
}
}
return b.queueAndStreamJob(ctx, job, ui, cb)
}
// queueAndStreamJob will queue the job. If the client is configured to watch the job,
// it'll also stream the output to the configured UI.
func (b *Basis) queueAndStreamJob(
ctx context.Context,
job *vagrant_server.Job,
ui terminal.UI,
jobIdCallback func(string),
) (*vagrant_server.Job_Result, error) {
log := b.logger
// When local, we set an expiration here in case we can't gracefully
// cancel in the event of an error. This will ensure that the jobs don't
// remain queued forever. This is only for local ops.
expiration := ""
if b.local {
expiration = "30s"
}
// Queue the job
log.Debug("queueing job", "operation", fmt.Sprintf("%T", job.Operation))
queueResp, err := b.client.QueueJob(ctx, &vagrant_server.QueueJobRequest{
Job: job,
ExpiresIn: expiration,
})
if err != nil {
return nil, err
}
log = log.With("job_id", queueResp.JobId)
// Call our callback if it was given
if jobIdCallback != nil {
jobIdCallback(queueResp.JobId)
}
// Get the stream
log.Debug("opening job stream")
stream, err := b.client.GetJobStream(ctx, &vagrant_server.GetJobStreamRequest{
JobId: queueResp.JobId,
})
if err != nil {
return nil, err
}
// Wait for open confirmation
resp, err := stream.Recv()
if err != nil {
return nil, err
}
if _, ok := resp.Event.(*vagrant_server.GetJobStreamResponse_Open_); !ok {
return nil, status.Errorf(codes.Aborted,
"job stream failed to open, got unexpected message %T",
resp.Event)
}
type stepData struct {
terminal.Step
out io.Writer
}
// Process events
var (
completed bool
stateEventTimer *time.Timer
tstatus terminal.Status
stdout, stderr io.Writer
sg terminal.StepGroup
steps = map[int32]*stepData{}
)
if b.local {
defer func() {
// If we completed then do nothing, or if the context is still
// active since this means that we're not cancelled.
if completed || ctx.Err() == nil {
return
}
ctx, cancel := finalcontext.Context(log)
defer cancel()
log.Warn("canceling job")
_, err := b.client.CancelJob(ctx, &vagrant_server.CancelJobRequest{
JobId: queueResp.JobId,
})
if err != nil {
log.Warn("error canceling job", "err", err)
} else {
log.Info("job cancelled successfully")
}
}()
}
for {
resp, err := stream.Recv()
if err != nil {
return nil, err
}
if resp == nil {
// This shouldn't happen, but if it does, just ignore it.
log.Warn("nil response received, ignoring")
continue
}
switch event := resp.Event.(type) {
case *vagrant_server.GetJobStreamResponse_Complete_:
completed = true
if event.Complete.Error == nil {
log.Info("job completed successfully")
return event.Complete.Result, nil
}
st := status.FromProto(event.Complete.Error)
log.Warn("job failed", "code", st.Code(), "message", st.Message())
return nil, st.Err()
case *vagrant_server.GetJobStreamResponse_Error_:
completed = true
st := status.FromProto(event.Error.Error)
log.Warn("job stream failure", "code", st.Code(), "message", st.Message())
return nil, st.Err()
case *vagrant_server.GetJobStreamResponse_Terminal_:
// Ignore this for local jobs since we're using our UI directly.
if b.local {
continue
}
for _, ev := range event.Terminal.Events {
log.Trace("job terminal output", "event", ev)
switch ev := ev.Event.(type) {
case *vagrant_server.GetJobStreamResponse_Terminal_Event_Line_:
ui.Output(ev.Line.Msg, terminal.WithStyle(ev.Line.Style))
case *vagrant_server.GetJobStreamResponse_Terminal_Event_NamedValues_:
var values []terminal.NamedValue
for _, tnv := range ev.NamedValues.Values {
values = append(values, terminal.NamedValue{
Name: tnv.Name,
Value: tnv.Value,
})
}
ui.NamedValues(values)
case *vagrant_server.GetJobStreamResponse_Terminal_Event_Status_:
if tstatus == nil {
tstatus = ui.Status()
defer tstatus.Close()
}
if ev.Status.Msg == "" && !ev.Status.Step {
tstatus.Close()
} else if ev.Status.Step {
tstatus.Step(ev.Status.Status, ev.Status.Msg)
} else {
tstatus.Update(ev.Status.Msg)
}
case *vagrant_server.GetJobStreamResponse_Terminal_Event_Raw_:
if stdout == nil {
stdout, stderr, err = ui.OutputWriters()
if err != nil {
return nil, err
}
}
if ev.Raw.Stderr {
stderr.Write(ev.Raw.Data)
} else {
stdout.Write(ev.Raw.Data)
}
case *vagrant_server.GetJobStreamResponse_Terminal_Event_Table_:
tbl := terminal.NewTable(ev.Table.Headers...)
for _, row := range ev.Table.Rows {
var trow []terminal.TableEntry
for _, ent := range row.Entries {
trow = append(trow, terminal.TableEntry{
Value: ent.Value,
Color: ent.Color,
})
}
}
ui.Table(tbl)
case *vagrant_server.GetJobStreamResponse_Terminal_Event_StepGroup_:
if sg != nil {
sg.Wait()
}
if !ev.StepGroup.Close {
sg = ui.StepGroup()
}
case *vagrant_server.GetJobStreamResponse_Terminal_Event_Step_:
if sg == nil {
continue
}
step, ok := steps[ev.Step.Id]
if !ok {
step = &stepData{
Step: sg.Add(ev.Step.Msg),
}
steps[ev.Step.Id] = step
} else {
if ev.Step.Msg != "" {
step.Update(ev.Step.Msg)
}
}
if ev.Step.Status != "" {
if ev.Step.Status == terminal.StatusAbort {
step.Abort()
} else {
step.Status(ev.Step.Status)
}
}
if len(ev.Step.Output) > 0 {
if step.out == nil {
step.out = step.TermOutput()
}
step.out.Write(ev.Step.Output)
}
if ev.Step.Close {
step.Done()
}
default:
b.logger.Error("Unknown terminal event seen", "type", hclog.Fmt("%T", ev))
}
}
case *vagrant_server.GetJobStreamResponse_State_:
// Stop any state event timers if we have any since the state
// has changed and we don't want to output that information anymore.
if stateEventTimer != nil {
stateEventTimer.Stop()
stateEventTimer = nil
}
// For certain states, we do a quality of life UI message if
// the wait time ends up being long.
switch event.State.Current {
case vagrant_server.Job_QUEUED:
stateEventTimer = time.AfterFunc(stateEventPause, func() {
ui.Output("Operation is queued. Waiting for runner assignment...",
terminal.WithHeaderStyle())
ui.Output("If you interrupt this command, the job will still run in the background.",
terminal.WithInfoStyle())
})
case vagrant_server.Job_WAITING:
stateEventTimer = time.AfterFunc(stateEventPause, func() {
ui.Output("Operation is assigned to a runner. Waiting for start...",
terminal.WithHeaderStyle())
ui.Output("If you interrupt this command, the job will still run in the background.",
terminal.WithInfoStyle())
})
}
default:
log.Warn("unknown stream event", "event", resp.Event)
}
}
}
const stateEventPause = 1500 * time.Millisecond

View File

@ -0,0 +1,44 @@
package client
import (
"context"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
)
type Machine struct {
ui terminal.UI
project *Project
machine *vagrant_server.Machine
logger hclog.Logger
}
func (m *Machine) UI() terminal.UI {
return m.ui
}
func (m *Machine) Ref() *vagrant_server.Ref_Machine {
return &vagrant_server.Ref_Machine{
ResourceId: m.machine.ResourceId,
Name: m.machine.Name,
Project: m.project.Ref(),
}
}
func (m *Machine) job() *vagrant_server.Job {
job := m.project.job()
job.Machine = m.Ref()
return job
}
func (m *Machine) Close() error {
return m.project.Close()
}
func (m *Machine) doJob(ctx context.Context, job *vagrant_server.Job) (*vagrant_server.Job_Result, error) {
return m.project.doJob(ctx, job, m.ui)
}

26
internal/client/noop.go Normal file
View File

@ -0,0 +1,26 @@
package client
import (
"context"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
)
// Noop executes a noop operation. This is primarily for testing but is
// exported since it has its uses in verifying a runner is functioning
// properly.
//
// A noop operation will exercise the full logic of queueing a job,
// assigning it to a runner, dequeueing as a runner, executing, etc. It will
// use real remote runners if the client is configured to do so.
func (b *Basis) Noop(ctx context.Context) error {
// Build our job
job := b.job()
job.Operation = &vagrant_server.Job_Noop_{
Noop: &vagrant_server.Job_Noop{},
}
// Execute it
_, err := b.doJob(ctx, job, nil)
return err
}

View File

@ -0,0 +1,31 @@
package client
import (
"context"
"testing"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require"
"github.com/hashicorp/vagrant/internal/server/singleprocess"
)
func init() {
hclog.L().SetLevel(hclog.Trace)
}
func TestProjectNoop(t *testing.T) {
ctx := context.Background()
require := require.New(t)
client := singleprocess.TestServer(t)
// Build our client
c := TestProject(t, WithClient(client), WithLocal())
app := c.App(TestApp(t, c))
// TODO(mitchellh): once we have an API to list jobs, verify we have
// no jobs, and then verify we execute a job after.
// Noop
require.NoError(app.Noop(ctx))
}

View File

@ -0,0 +1,150 @@
package client
import (
"context"
"github.com/hashicorp/vagrant-plugin-sdk/component"
"github.com/hashicorp/vagrant/internal/server/logviewer"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
)
func (b *Basis) Validate(ctx context.Context, op *vagrant_server.Job_ValidateOp) (*vagrant_server.Job_ValidateResult, error) {
if op == nil {
op = &vagrant_server.Job_ValidateOp{}
}
// Validate our job
job := b.job()
job.Operation = &vagrant_server.Job_Validate{
Validate: op,
}
// Execute it
result, err := b.doJob(ctx, job, nil)
if err != nil {
return nil, err
}
return result.Validate, nil
}
func (b *Basis) Commands(ctx context.Context, op *vagrant_server.Job_InitOp) (*vagrant_server.Job_InitResult, error) {
if op == nil {
op = &vagrant_server.Job_InitOp{}
}
job := b.job()
job.Operation = &vagrant_server.Job_Init{
Init: op,
}
result, err := b.doJob(ctx, job, nil)
if err != nil {
return nil, err
}
return result.Init, nil
}
func (b *Basis) Task(ctx context.Context, op *vagrant_server.Job_RunOp) (*vagrant_server.Job_RunResult, error) {
if op == nil {
op = &vagrant_server.Job_RunOp{}
}
job := b.job()
job.Operation = &vagrant_server.Job_Run{
Run: op,
}
result, err := b.doJob(ctx, job, nil)
return result.Run, err
}
func (p *Project) Task(ctx context.Context, op *vagrant_server.Job_RunOp) (*vagrant_server.Job_RunResult, error) {
if op == nil {
op = &vagrant_server.Job_RunOp{}
}
job := p.job()
job.Operation = &vagrant_server.Job_Run{
Run: op,
}
result, err := p.doJob(ctx, job, nil)
return result.Run, err
}
func (m *Machine) Task(ctx context.Context, op *vagrant_server.Job_RunOp) (*vagrant_server.Job_RunResult, error) {
if op == nil {
op = &vagrant_server.Job_RunOp{}
}
job := m.job()
job.Operation = &vagrant_server.Job_Run{
Run: op,
}
result, err := m.doJob(ctx, job)
return result.Run, err
}
func (b *Basis) Auth(ctx context.Context, op *vagrant_server.Job_AuthOp) (*vagrant_server.Job_AuthResult, error) {
if op == nil {
op = &vagrant_server.Job_AuthOp{}
}
// Auth our job
job := b.job()
job.Operation = &vagrant_server.Job_Auth{
Auth: op,
}
// Execute it
result, err := b.doJob(ctx, job, nil)
if err != nil {
return nil, err
}
return result.Auth, nil
}
func (b *Basis) Docs(ctx context.Context, op *vagrant_server.Job_DocsOp) (*vagrant_server.Job_DocsResult, error) {
if op == nil {
op = &vagrant_server.Job_DocsOp{}
}
job := b.job()
job.Operation = &vagrant_server.Job_Docs{
Docs: op,
}
// Execute it
result, err := b.doJob(ctx, job, nil)
if err != nil {
return nil, err
}
return result.Docs, nil
}
func (b *Basis) Logs(ctx context.Context) (component.LogViewer, error) {
log := b.logger.Named("logs")
// First we attempt to query the server for logs for this deployment.
log.Info("requesting log stream")
client, err := b.client.GetLogStream(ctx, &vagrant_server.GetLogStreamRequest{
Scope: &vagrant_server.GetLogStreamRequest_Basis{
Basis: b.Ref(),
},
})
if err != nil {
return nil, err
}
// Build our log viewer
return &logviewer.Viewer{Stream: client}, nil
}

162
internal/client/project.go Normal file
View File

@ -0,0 +1,162 @@
package client
import (
"context"
"errors"
"os"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vagrant-plugin-sdk/helper/paths"
vagrant_plugin_sdk "github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
)
// Project is the primary structure for interacting with a Vagrant
// server as a client. The client exposes a slightly higher level of
// abstraction over the server API for performing operations locally and
// remotely.
type Project struct {
ui terminal.UI
Machines []*Machine
basis *Basis
project *vagrant_server.Project
logger hclog.Logger
}
func (p *Project) LoadMachine(m *vagrant_server.Machine) (*Machine, error) {
machine, err := p.GetMachine(m.Name)
if err == nil {
return machine, nil
}
// Ensure the machine is set to this project
m.Project = p.Ref()
result, err := p.basis.client.FindMachine(
context.Background(),
&vagrant_server.FindMachineRequest{
Machine: m,
},
)
if err == nil && result.Found {
machine := &Machine{
ui: p.UI(),
project: p,
machine: result.Machine,
logger: p.logger.Named("machine"),
}
p.Machines = append(p.Machines, machine)
return machine, nil
}
p.logger.Trace("failed to locate existing machine", "machine", m,
"result", result, "error", err)
// TODO: set machine box from vagrant file
if m.Datadir == nil {
m.Datadir = p.GetDataDir()
}
if m.Provider == "" {
m.Provider, err = p.GetDefaultProvider([]string{}, false, true)
}
uresult, err := p.basis.client.UpsertMachine(
context.Background(),
&vagrant_server.UpsertMachineRequest{
Machine: m,
},
)
if err != nil {
return nil, err
}
machine = &Machine{
ui: p.UI(),
project: p,
machine: uresult.Machine,
logger: p.logger.Named("machine"),
}
p.Machines = append(p.Machines, machine)
return machine, nil
}
// TODO: Determine default provider by implementing algorithm from
// https://www.vagrantup.com/docs/providers/basic_usage#default-provider
//
// Currently blocked on being able to parse Vagrantfile
func (p *Project) GetDefaultProvider(exclude []string, forceDefault bool, checkUsable bool) (provider string, err error) {
defaultProvider := os.Getenv("VAGRANT_DEFAULT_PROVIDER")
if defaultProvider != "" && forceDefault {
return defaultProvider, nil
}
// HACK: This should throw an error if no default provider is found
return "virtualbox", nil
}
func (p *Project) GetDataDir() *vagrant_plugin_sdk.Args_DataDir_Machine {
// TODO: probably need to get datadir from the projet + basis
root, _ := paths.VagrantHome()
cacheDir := root.Join("cache")
dataDir := root.Join("data")
tmpDir := root.Join("tmp")
return &vagrant_plugin_sdk.Args_DataDir_Machine{
CacheDir: cacheDir.String(),
DataDir: dataDir.String(),
RootDir: root.String(),
TempDir: tmpDir.String(),
}
}
func (p *Project) GetMachine(name string) (m *Machine, err error) {
for _, m = range p.Machines {
if m.Ref().Name == name {
return
}
}
return nil, errors.New("failed to locate requested machine")
}
func (p *Project) UI() terminal.UI {
return p.ui
}
func (p *Project) Close() error {
return p.basis.Close()
}
// Ref returns the raw Vagrant server API client.
func (p *Project) Ref() *vagrant_server.Ref_Project {
return &vagrant_server.Ref_Project{
Name: p.project.Name,
ResourceId: p.project.ResourceId,
Basis: p.basis.Ref(),
}
}
// job is the same as Project.job except this also sets the application
// reference.
func (p *Project) job() *vagrant_server.Job {
job := p.basis.job()
job.Project = p.Ref()
return job
}
func (p *Project) doJob(ctx context.Context, job *vagrant_server.Job, ui terminal.UI) (*vagrant_server.Job_Result, error) {
if ui == nil {
ui = p.ui
}
return p.basis.doJob(ctx, job, ui)
}

27
internal/client/runner.go Normal file
View File

@ -0,0 +1,27 @@
package client
import (
"github.com/hashicorp/vagrant/internal/runner"
)
// startRunner initializes and starts a local runner. If the returned
// runner is non-nil, you must call Close on it to clean up resources properly.
func (b *Basis) startRunner() (*runner.Runner, error) {
// Initialize our runner
r, err := runner.New(
runner.WithClient(b.client),
runner.WithLogger(b.logger.Named("runner")),
runner.ByIdOnly(), // We'll direct target this
runner.WithLocal(b.UI()), // Local mode
)
if err != nil {
return nil, err
}
// Start the runner
if err := r.Start(); err != nil {
return nil, err
}
return r, nil
}

233
internal/client/server.go Normal file
View File

@ -0,0 +1,233 @@
package client
import (
"context"
"io"
"net"
"os/exec"
"path/filepath"
"runtime"
"time"
"github.com/boltdb/bolt"
"github.com/golang/protobuf/ptypes/empty"
"github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
"github.com/hashicorp/vagrant/internal/protocolversion"
"github.com/hashicorp/vagrant/internal/server"
sr "github.com/hashicorp/vagrant/internal/server"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
"github.com/hashicorp/vagrant/internal/server/singleprocess"
"github.com/hashicorp/vagrant/internal/serverclient"
)
// initServerClient will initialize a gRPC connection to the Vagrant server.
// This is called if a client wasn't explicitly given with WithClient.
//
// If a connection is successfully established, this will register connection
// closing and server cleanup with the Project cleanup function.
//
// This function will do one of two things:
//
// 1. If connection options were given, it'll attempt to connect to
// an existing Vagrant server.
//
// 2. If WithLocal was specified and no connection addresses can be
// found, this will spin up an in-memory server.
//
func (b *Basis) initServerClient(ctx context.Context, cfg *config) (*grpc.ClientConn, error) {
log := b.logger.Named("server")
// If we're local, then connection is optional.
opts := cfg.connectOpts
if b.local {
log.Trace("WithLocal set, server credentials optional")
opts = append(opts, serverclient.Optional())
}
// Connect. If we're local, this is set as optional so conn may be nil
log.Info("attempting to source credentials and connect")
conn, err := serverclient.Connect(ctx, opts...)
if err != nil {
return nil, err
}
// If we established a connection
if conn != nil {
log.Debug("connection established with sourced credentials")
b.cleanup(func() { conn.Close() })
return conn, nil
}
// No connection, meaning we have to spin up a local server. This
// can only be reached if we specified "Optional" to serverclient
// which is only possible if we configured this client to support local
// mode.
log.Info("no server credentials found, using in-memory local server")
return b.initLocalServer(ctx)
}
// initLocalServer starts the local server and configures p.client to
// point to it. This also configures p.localClosers so that all the
// resources are properly cleaned up on Close.
//
// If this returns an error, all resources associated with this operation
// will be closed, but the project can retry.
func (b *Basis) initLocalServer(ctx context.Context) (*grpc.ClientConn, error) {
log := b.logger.Named("server")
b.localServer = true
// We use this pointer to accumulate things we need to clean up
// in the case of an error. On success we nil this variable which
// doesn't close anything.
var closers []io.Closer
defer func() {
for _, c := range closers {
c.Close()
}
}()
// TODO(mitchellh): path to this
path := filepath.Join("data.db")
log.Debug("opening local mode DB", "path", path)
// Open our database
db, err := bolt.Open(path, 0600, &bolt.Options{
Timeout: 1 * time.Second,
})
if err != nil {
return nil, err
}
closers = append(closers, db)
vrr, err := b.initVagrantRubyRuntime()
if err != nil {
return nil, err
}
// Create our server
impl, err := singleprocess.New(
singleprocess.WithVagrantRubyRuntime(vrr),
singleprocess.WithDB(db),
singleprocess.WithLogger(log.Named("singleprocess")),
)
if err != nil {
log.Trace("failed singleprocess server setup", "error", err)
return nil, err
}
// We listen on a random locally bound port
// TODO: we should use Unix domain sockets if supported
ln, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
return nil, err
}
closers = append(closers, ln)
// Create a new cancellation context so we can cancel in the case of an error
ctx, cancel := context.WithCancel(ctx)
// Run the server
log.Info("starting built-in server for local operations", "addr", ln.Addr().String())
go server.Run(server.WithContext(ctx),
server.WithLogger(log),
server.WithGRPC(ln),
server.WithImpl(impl),
server.WithMachineImpl(impl.(vagrant_plugin_sdk.MachineServiceServer)),
server.WithEnvironmentImpl(impl.(vagrant_plugin_sdk.ProjectServiceServer)),
)
client, err := serverclient.NewVagrantClient(ctx, log, ln.Addr().String())
if err != nil {
cancel()
return nil, err
}
// Setup our server config. The configuration is specifically set so
// so that there is no advertise address which will disable the CEB
// completely.
_, err = client.SetServerConfig(ctx, &vagrant_server.SetServerConfigRequest{
Config: &vagrant_server.ServerConfig{
AdvertiseAddrs: []*vagrant_server.ServerConfig_AdvertiseAddr{
{
Addr: "",
},
},
},
})
if err != nil {
cancel()
return nil, err
}
// Success, persist the closers
cleanupClosers := closers
closers = nil
b.cleanup(func() {
for _, c := range cleanupClosers {
c.Close()
}
// Force the ruby runtime to shut down
if cl, err := vrr.Client(); err == nil {
cl.Close()
}
})
_ = cancel // pacify vet lostcancel
return client.Conn(), nil
}
func (b *Basis) initVagrantRubyRuntime() (*plugin.Client, error) {
// TODO: Update for actual release usage. This is dev only now.
// TODO: We should also locate a free port on startup and use that port
_, this_dir, _, _ := runtime.Caller(0)
cmd := exec.Command(
"bundle", "exec", "vagrant", "serve",
)
cmd.Env = []string{
"BUNDLE_GEMFILE=" + filepath.Join(this_dir, "../../..", "Gemfile"),
"VAGRANT_I_KNOW_WHAT_IM_DOING_PLEASE_BE_QUIET=true",
"VAGRANT_LOG=debug",
"VAGRANT_LOG_FILE=/tmp/vagrant.log",
}
config := sr.RubyVagrantPluginConfig(b.logger)
config.Cmd = cmd
rubyServerClient := plugin.NewClient(config)
_, err := rubyServerClient.Start()
if err != nil {
return nil, err
}
return rubyServerClient, nil
}
// negotiateApiVersion negotiates the API version to use and validates
// that we are compatible to talk to the server.
func (b *Basis) negotiateApiVersion(ctx context.Context) error {
log := b.logger
log.Trace("requesting version info from server")
resp, err := b.client.GetVersionInfo(ctx, &empty.Empty{})
if err != nil {
return err
}
log.Info("server version info",
"version", resp.Info.Version,
"api_min", resp.Info.Api.Minimum,
"api_current", resp.Info.Api.Current,
"entrypoint_min", resp.Info.Entrypoint.Minimum,
"entrypoint_current", resp.Info.Entrypoint.Current,
)
vsn, err := protocolversion.Negotiate(protocolversion.Current().Api, resp.Info.Api)
if err != nil {
return err
}
log.Info("negotiated api version", "version", vsn)
return nil
}

View File

@ -0,0 +1,69 @@
package client
// import (
// "context"
// "io/ioutil"
// "os"
// "github.com/mitchellh/go-testing-interface"
// "github.com/stretchr/testify/require"
// configpkg "github.com/hashicorp/vagrant/internal/config"
// pb "github.com/hashicorp/vagrant/internal/server/gen"
// "github.com/hashicorp/vagrant/internal/server/singleprocess"
// )
// // TestProject returns an initialized client pointing to an in-memory test
// // server. This will close automatically on test completion.
// //
// // This will also change the working directory to a temporary directory
// // so that any side effect file creation doesn't impact the real working
// // directory. If you need to use your working directory, query it before
// // calling this.
// func TestProject(t testing.T, opts ...Option) *Project {
// require := require.New(t)
// client := singleprocess.TestServer(t)
// ctx := context.Background()
// basis, err := NewBasis(ctx, WithClient(client), WithLocal())
// require.NoError(err)
// // Initialize our client
// result, err := New(ctx, append([]Option{
// WithBasis(basis),
// WithProjectRef(&pb.Ref_Project{Project: "test_p"}),
// }, opts...)...)
// require.NoError(err)
// // Move into a temporary directory
// td := testTempDir(t)
// testChdir(t, td)
// // Create a valid vagrant configuration file
// configpkg.TestConfigFile(t, configpkg.TestSource(t))
// return result
// }
// // TestApp returns an app reference that can be used for testing.
// func TestApp(t testing.T, c *Project) string {
// // Initialize our app
// singleprocess.TestApp(t, c.Client(), c.App("test_a").Ref())
// return "test_a"
// }
// func testChdir(t testing.T, dir string) {
// pwd, err := os.Getwd()
// require.NoError(t, err)
// require.NoError(t, os.Chdir(dir))
// t.Cleanup(func() { require.NoError(t, os.Chdir(pwd)) })
// }
// func testTempDir(t testing.T) string {
// dir, err := ioutil.TempDir("", "vagrant-test")
// require.NoError(t, err)
// t.Cleanup(func() { os.RemoveAll(dir) })
// return dir
// }

View File

@ -0,0 +1,23 @@
package clierrors
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// IsCanceled is true if the error represents a cancellation. This detects
// context cancellation as well as gRPC cancellation codes.
func IsCanceled(err error) bool {
if err == context.Canceled {
return true
}
s, ok := status.FromError(err)
if !ok {
return false
}
return s.Code() == codes.Canceled
}

View File

@ -0,0 +1,31 @@
package clierrors
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func TestIsCanceled(t *testing.T) {
t.Run("nil", func(t *testing.T) {
var err error
require.False(t, IsCanceled(err))
})
t.Run("context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
require.True(t, IsCanceled(ctx.Err()))
})
t.Run("status canceled", func(t *testing.T) {
require.True(t, IsCanceled(status.Errorf(codes.Canceled, "")))
})
t.Run("status other", func(t *testing.T) {
require.False(t, IsCanceled(status.Errorf(codes.FailedPrecondition, "")))
})
}

View File

@ -0,0 +1,23 @@
package clierrors
import (
"github.com/mitchellh/go-wordwrap"
"google.golang.org/grpc/status"
)
func Humanize(err error) string {
if err == nil {
return ""
}
if IsCanceled(err) {
return "operation canceled"
}
v := err.Error()
if s, ok := status.FromError(err); ok {
v = s.Message()
}
return wordwrap.WrapString(v, 80)
}

34
internal/config/basis.go Normal file
View File

@ -0,0 +1,34 @@
package config
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
)
type Basis struct {
// These are new configurations
Location string `hcl:"location,attr"`
Runner *Runner `hcl:"runner,block" default:"{}"`
Labels map[string]string `hcl:"labels,optional"`
// These should _roughly_ map to existing Vagrantfile configurations
Vagrant *Vagrant `hcl:"vagrant,block"`
Machines []*Machine `hcl:"machine,block"`
Communicators []*Communicator `hcl:"communicator,block"`
Body hcl.Body `hcl:",body"`
Remain hcl.Body `hcl:",remain"`
ref *vagrant_server.Basis
path string
config *Config
}
func (b *Basis) Ref() *vagrant_server.Basis {
return b.ref
}
func (b *Basis) Validate() (err error) {
return
}

View File

@ -0,0 +1,12 @@
package config
import (
"github.com/hashicorp/hcl/v2"
)
type Communicator struct {
Name string `hcl:"name,label"`
Body hcl.Body `hcl:",body"`
Remain hcl.Body `hcl:",remain"`
}

134
internal/config/config.go Normal file
View File

@ -0,0 +1,134 @@
package config
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/hashicorp/vagrant/internal/pkg/defaults"
)
// Config is the core configuration
// TODO(spox): We need to do the whole merging thing
// with the config and access things directly
// via the Config, not the Basis or Project
type Config struct {
Runner *Runner `hcl:"runner,block" default:"{}"`
Labels map[string]string `hcl:"labels,optional"`
Basis *Basis
Project *Project
Plugin []*Plugin
pathData map[string]string
ctx *hcl.EvalContext
}
// Runner is the configuration for supporting runners in this project.
type Runner struct {
// Enabled is whether or not runners are enabled. If this is false
// then the "-remote" flag will not work.
Enabled bool
// DataSource is the default data source when a remote job is queued.
DataSource *DataSource
}
// DataSource configures the data source for the runner.
type DataSource struct {
Type string
Body hcl.Body `hcl:",remain"`
}
// Load loads the configuration file from the given path.
func Load(path string, pwd string) (*Config, error) {
// We require an absolute path for the path so we can set the path vars
if path != "" && !filepath.IsAbs(path) {
var err error
path, err = filepath.Abs(path)
if err != nil {
return nil, err
}
}
// If we have no pwd, then create a temporary directory
if pwd == "" {
td, err := ioutil.TempDir("", "vagrant-config")
if err != nil {
return nil, err
}
defer os.RemoveAll(td)
pwd = td
}
// Setup our initial variable set
pathData := map[string]string{
"pwd": pwd,
"basisfile": path,
}
// Decode
var cfg Config
// Build our context
ctx := EvalContext(nil, pwd).NewChild()
addPathValue(ctx, pathData)
// Decode
if err := hclsimple.DecodeFile(path, ctx, &cfg); err != nil {
return nil, err
}
if err := defaults.Set(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
// Load a project from a configuration file (Vagrantfile)
// func (c *Config) LoadProject(vagrantfile *vagrant_server.Vagrantfile, projectRef *vagrant_server.Ref_Project) (*Project, error) {
// // We require an absolute path for the path so we can set the path vars
// // if !filepath.IsAbs(path) {
// // var err error
// // path, err = filepath.Abs(path)
// // if err != nil {
// // return nil, err
// // }
// // }
// // // If we have no pwd, then use pwd from basis config
// // if pwd == "" {
// // pwd = c.pathData["pwd"]
// // }
// // // Setup our initial variable set
// // pathData := map[string]string{
// // "pwd": pwd,
// // "project": filepath.Dir(path),
// // "vagrantfile": path,
// // }
// // Decode
// // var cfg Project
// // cfg.Location = filepath.Dir(path)
// machines := []*Machine{}
// for _, el := range vagrantfile.MachineConfigs {
// machines = append(machines, &Machine{Name: el.Name, Box: el.Box})
// }
// communicators := []*Communicator{}
// for _, el := range vagrantfile.Communicators {
// communicators = append(communicators, &Communicator{Name: el.Name})
// }
// return &Project{
// Location: filepath.Dir(vagrantfile.Path),
// Vagrant: &Vagrant{},
// Machines: machines,
// Communicators: communicators,
// path: filepath.Dir(vagrantfile.Path),
// config: c,
// ref: projectRef,
// }, nil
// }

View File

@ -0,0 +1,66 @@
package config
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func TestLoad_compare(t *testing.T) {
cases := []struct {
File string
Err string
Func func(*testing.T, *Config)
}{
{
"project.hcl",
"",
func(t *testing.T, c *Config) {
require.Equal(t, "hello", c.Project)
},
},
{
"project_pwd.hcl",
"",
func(t *testing.T, c *Config) {
require.NotEmpty(t, c.Project)
},
},
{
"project_path_project.hcl",
"",
func(t *testing.T, c *Config) {
expected, err := filepath.Abs(filepath.Join("testdata", "compare"))
require.NoError(t, err)
require.Equal(t, expected, c.Project)
},
},
{
"project_function.hcl",
"",
func(t *testing.T, c *Config) {
require.Equal(t, "HELLO", c.Project)
},
},
}
for _, tt := range cases {
t.Run(tt.File, func(t *testing.T) {
require := require.New(t)
cfg, err := Load(filepath.Join("testdata", "compare", tt.File), "")
if tt.Err != "" {
require.Error(err)
require.Contains(err.Error(), tt.Err)
return
}
require.NoError(err)
tt.Func(t, cfg)
})
}
}

View File

@ -0,0 +1,66 @@
package config
import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/gocty"
"github.com/hashicorp/vagrant/internal/config/funcs"
)
// EvalContext returns the common eval context to use for parsing all
// configurations. This should always be available for all config types.
//
// The pwd param is the directory to use as a working directory
// for determining things like relative paths. This should be considered
// the pwd over the actual process pwd.
func EvalContext(parent *hcl.EvalContext, pwd string) *hcl.EvalContext {
// NewChild works even with parent == nil so this is valid
result := parent.NewChild()
// Start with our HCL stdlib
result.Functions = funcs.Stdlib()
// add functions to our context
addFuncs := func(fs map[string]function.Function) {
for k, v := range fs {
result.Functions[k] = v
}
}
// Add some of our functions
addFuncs(funcs.VCSGitFuncs(pwd))
addFuncs(funcs.Filesystem(pwd))
addFuncs(funcs.Encoding())
return result
}
// appendContext makes child a child of parent and returns the new context.
// If child is nil this returns parent.
func appendContext(parent, child *hcl.EvalContext) *hcl.EvalContext {
if child == nil {
return parent
}
newChild := parent.NewChild()
newChild.Variables = child.Variables
newChild.Functions = child.Functions
return newChild
}
// addPathValue adds the "path" variable to the context.
func addPathValue(ctx *hcl.EvalContext, v map[string]string) {
value, err := gocty.ToCtyValue(v, cty.Map(cty.String))
if err != nil {
// map[string]string conversion should never fail
panic(err)
}
if ctx.Variables == nil {
ctx.Variables = map[string]cty.Value{}
}
ctx.Variables["path"] = value
}

View File

@ -0,0 +1,149 @@
package funcs
import (
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"log"
"net/url"
"unicode/utf8"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
func Encoding() map[string]function.Function {
return map[string]function.Function{
"base64decode": Base64DecodeFunc,
"base64encode": Base64EncodeFunc,
"base64gzip": Base64GzipFunc,
"urlencode": URLEncodeFunc,
}
}
// Base64DecodeFunc constructs a function that decodes a string containing a base64 sequence.
var Base64DecodeFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
s := args[0].AsString()
sDec, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode base64 data '%s'", s)
}
if !utf8.Valid([]byte(sDec)) {
log.Printf("[DEBUG] the result of decoding the provided string is not valid UTF-8: %s", sDec)
return cty.UnknownVal(cty.String), fmt.Errorf("the result of decoding the provided string is not valid UTF-8")
}
return cty.StringVal(string(sDec)), nil
},
})
// Base64EncodeFunc constructs a function that encodes a string to a base64 sequence.
var Base64EncodeFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return cty.StringVal(base64.StdEncoding.EncodeToString([]byte(args[0].AsString()))), nil
},
})
// Base64GzipFunc constructs a function that compresses a string with gzip and then encodes the result in
// Base64 encoding.
var Base64GzipFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
s := args[0].AsString()
var b bytes.Buffer
gz := gzip.NewWriter(&b)
if _, err := gz.Write([]byte(s)); err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to write gzip raw data: '%s'", s)
}
if err := gz.Flush(); err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to flush gzip writer: '%s'", s)
}
if err := gz.Close(); err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to close gzip writer: '%s'", s)
}
return cty.StringVal(base64.StdEncoding.EncodeToString(b.Bytes())), nil
},
})
// URLEncodeFunc constructs a function that applies URL encoding to a given string.
var URLEncodeFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return cty.StringVal(url.QueryEscape(args[0].AsString())), nil
},
})
// Base64Decode decodes a string containing a base64 sequence.
//
// Vagrant uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
//
// Strings in the Vagrant language are sequences of unicode characters rather
// than bytes, so this function will also interpret the resulting bytes as
// UTF-8. If the bytes after Base64 decoding are _not_ valid UTF-8, this function
// produces an error.
func Base64Decode(str cty.Value) (cty.Value, error) {
return Base64DecodeFunc.Call([]cty.Value{str})
}
// Base64Encode applies Base64 encoding to a string.
//
// Vagrant uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
//
// Strings in the Vagrant language are sequences of unicode characters rather
// than bytes, so this function will first encode the characters from the string
// as UTF-8, and then apply Base64 encoding to the result.
func Base64Encode(str cty.Value) (cty.Value, error) {
return Base64EncodeFunc.Call([]cty.Value{str})
}
// Base64Gzip compresses a string with gzip and then encodes the result in
// Base64 encoding.
//
// Vagrant uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
//
// Strings in the Vagrant language are sequences of unicode characters rather
// than bytes, so this function will first encode the characters from the string
// as UTF-8, then apply gzip compression, and then finally apply Base64 encoding.
func Base64Gzip(str cty.Value) (cty.Value, error) {
return Base64GzipFunc.Call([]cty.Value{str})
}
// URLEncode applies URL encoding to a given string.
//
// This function identifies characters in the given string that would have a
// special meaning when included as a query string argument in a URL and
// escapes them using RFC 3986 "percent encoding".
//
// If the given string contains non-ASCII characters, these are first encoded as
// UTF-8 and then percent encoding is applied separately to each UTF-8 byte.
func URLEncode(str cty.Value) (cty.Value, error) {
return URLEncodeFunc.Call([]cty.Value{str})
}

View File

@ -0,0 +1,165 @@
package funcs
import (
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestBase64Decode(t *testing.T) {
tests := []struct {
String cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("YWJjMTIzIT8kKiYoKSctPUB+"),
cty.StringVal("abc123!?$*&()'-=@~"),
false,
},
{ // Invalid base64 data decoding
cty.StringVal("this-is-an-invalid-base64-data"),
cty.UnknownVal(cty.String),
true,
},
{ // Invalid utf-8
cty.StringVal("\xc3\x28"),
cty.UnknownVal(cty.String),
true,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("base64decode(%#v)", test.String), func(t *testing.T) {
got, err := Base64Decode(test.String)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestBase64Encode(t *testing.T) {
tests := []struct {
String cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("abc123!?$*&()'-=@~"),
cty.StringVal("YWJjMTIzIT8kKiYoKSctPUB+"),
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("base64encode(%#v)", test.String), func(t *testing.T) {
got, err := Base64Encode(test.String)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestBase64Gzip(t *testing.T) {
tests := []struct {
String cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("test"),
cty.StringVal("H4sIAAAAAAAA/ypJLS4BAAAA//8BAAD//wx+f9gEAAAA"),
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("base64gzip(%#v)", test.String), func(t *testing.T) {
got, err := Base64Gzip(test.String)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestURLEncode(t *testing.T) {
tests := []struct {
String cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("abc123-_"),
cty.StringVal("abc123-_"),
false,
},
{
cty.StringVal("foo:bar@localhost?foo=bar&bar=baz"),
cty.StringVal("foo%3Abar%40localhost%3Ffoo%3Dbar%26bar%3Dbaz"),
false,
},
{
cty.StringVal("mailto:email?subject=this+is+my+subject"),
cty.StringVal("mailto%3Aemail%3Fsubject%3Dthis%2Bis%2Bmy%2Bsubject"),
false,
},
{
cty.StringVal("foo/bar"),
cty.StringVal("foo%2Fbar"),
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("urlencode(%#v)", test.String), func(t *testing.T) {
got, err := URLEncode(test.String)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}

View File

@ -0,0 +1,475 @@
package funcs
import (
"encoding/base64"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"unicode/utf8"
"github.com/bmatcuk/doublestar"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
homedir "github.com/mitchellh/go-homedir"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
func Filesystem(pwd string) map[string]function.Function {
funcs := map[string]function.Function{
"file": MakeFileFunc(pwd, false),
"filebase64": MakeFileFunc(pwd, true),
"fileexists": MakeFileExistsFunc(pwd),
"fileset": MakeFileSetFunc(pwd),
"basename": BasenameFunc,
"dirname": DirnameFunc,
"abspath": AbsPathFunc,
"pathexpand": PathExpandFunc,
}
funcs["templatefile"] = MakeTemplateFileFunc(pwd, func() map[string]function.Function {
// The templatefile function prevents recursive calls to itself
// by copying this map and overwriting the "templatefile" entry.
return funcs
})
return funcs
}
// MakeFileFunc constructs a function that takes a file path and returns the
// contents of that file, either directly as a string (where valid UTF-8 is
// required) or as a string containing base64 bytes.
func MakeFileFunc(baseDir string, encBase64 bool) function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "path",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
path := args[0].AsString()
src, err := readFileBytes(baseDir, path)
if err != nil {
err = function.NewArgError(0, err)
return cty.UnknownVal(cty.String), err
}
switch {
case encBase64:
enc := base64.StdEncoding.EncodeToString(src)
return cty.StringVal(enc), nil
default:
if !utf8.Valid(src) {
return cty.UnknownVal(cty.String), fmt.Errorf("contents of %s are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead", path)
}
return cty.StringVal(string(src)), nil
}
},
})
}
// MakeTemplateFileFunc constructs a function that takes a file path and
// an arbitrary object of named values and attempts to render the referenced
// file as a template using HCL template syntax.
//
// The template itself may recursively call other functions so a callback
// must be provided to get access to those functions. The template cannot,
// however, access any variables defined in the scope: it is restricted only to
// those variables provided in the second function argument, to ensure that all
// dependencies on other graph nodes can be seen before executing this function.
//
// As a special exception, a referenced template file may not recursively call
// the templatefile function, since that would risk the same file being
// included into itself indefinitely.
func MakeTemplateFileFunc(baseDir string, funcsCb func() map[string]function.Function) function.Function {
params := []function.Parameter{
{
Name: "path",
Type: cty.String,
},
{
Name: "vars",
Type: cty.DynamicPseudoType,
},
}
loadTmpl := func(fn string) (hcl.Expression, error) {
// We re-use File here to ensure the same filename interpretation
// as it does, along with its other safety checks.
tmplVal, err := File(baseDir, cty.StringVal(fn))
if err != nil {
return nil, err
}
expr, diags := hclsyntax.ParseTemplate([]byte(tmplVal.AsString()), fn, hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
return nil, diags
}
return expr, nil
}
renderTmpl := func(expr hcl.Expression, varsVal cty.Value) (cty.Value, error) {
if varsTy := varsVal.Type(); !(varsTy.IsMapType() || varsTy.IsObjectType()) {
return cty.DynamicVal, function.NewArgErrorf(1, "invalid vars value: must be a map") // or an object, but we don't strongly distinguish these most of the time
}
ctx := &hcl.EvalContext{
Variables: varsVal.AsValueMap(),
}
// We require all of the variables to be valid HCL identifiers, because
// otherwise there would be no way to refer to them in the template
// anyway. Rejecting this here gives better feedback to the user
// than a syntax error somewhere in the template itself.
for n := range ctx.Variables {
if !hclsyntax.ValidIdentifier(n) {
// This error message intentionally doesn't describe _all_ of
// the different permutations that are technically valid as an
// HCL identifier, but rather focuses on what we might
// consider to be an "idiomatic" variable name.
return cty.DynamicVal, function.NewArgErrorf(1, "invalid template variable name %q: must start with a letter, followed by zero or more letters, digits, and underscores", n)
}
}
// We'll pre-check references in the template here so we can give a
// more specialized error message than HCL would by default, so it's
// clearer that this problem is coming from a templatefile call.
for _, traversal := range expr.Variables() {
root := traversal.RootName()
if _, ok := ctx.Variables[root]; !ok {
return cty.DynamicVal, function.NewArgErrorf(1, "vars map does not contain key %q, referenced at %s", root, traversal[0].SourceRange())
}
}
givenFuncs := funcsCb() // this callback indirection is to avoid chicken/egg problems
funcs := make(map[string]function.Function, len(givenFuncs))
for name, fn := range givenFuncs {
if name == "templatefile" {
// We stub this one out to prevent recursive calls.
funcs[name] = function.New(&function.Spec{
Params: params,
Type: func(args []cty.Value) (cty.Type, error) {
return cty.NilType, fmt.Errorf("cannot recursively call templatefile from inside templatefile call")
},
})
continue
}
funcs[name] = fn
}
ctx.Functions = funcs
val, diags := expr.Value(ctx)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
return val, nil
}
return function.New(&function.Spec{
Params: params,
Type: func(args []cty.Value) (cty.Type, error) {
if !(args[0].IsKnown() && args[1].IsKnown()) {
return cty.DynamicPseudoType, nil
}
// We'll render our template now to see what result type it produces.
// A template consisting only of a single interpolation an potentially
// return any type.
expr, err := loadTmpl(args[0].AsString())
if err != nil {
return cty.DynamicPseudoType, err
}
// This is safe even if args[1] contains unknowns because the HCL
// template renderer itself knows how to short-circuit those.
val, err := renderTmpl(expr, args[1])
return val.Type(), err
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
expr, err := loadTmpl(args[0].AsString())
if err != nil {
return cty.DynamicVal, err
}
return renderTmpl(expr, args[1])
},
})
}
// MakeFileExistsFunc constructs a function that takes a path
// and determines whether a file exists at that path
func MakeFileExistsFunc(baseDir string) function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "path",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
path := args[0].AsString()
path, err := homedir.Expand(path)
if err != nil {
return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to expand ~: %s", err)
}
if !filepath.IsAbs(path) {
path = filepath.Join(baseDir, path)
}
// Ensure that the path is canonical for the host OS
path = filepath.Clean(path)
fi, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return cty.False, nil
}
return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to stat %s", path)
}
if fi.Mode().IsRegular() {
return cty.True, nil
}
return cty.False, fmt.Errorf("%s is not a regular file, but %q",
path, fi.Mode().String())
},
})
}
// MakeFileSetFunc constructs a function that takes a glob pattern
// and enumerates a file set from that pattern
func MakeFileSetFunc(baseDir string) function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "path",
Type: cty.String,
},
{
Name: "pattern",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.Set(cty.String)),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
path := args[0].AsString()
pattern := args[1].AsString()
if !filepath.IsAbs(path) {
path = filepath.Join(baseDir, path)
}
// Join the path to the glob pattern, while ensuring the full
// pattern is canonical for the host OS. The joined path is
// automatically cleaned during this operation.
pattern = filepath.Join(path, pattern)
matches, err := doublestar.Glob(pattern)
if err != nil {
return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to glob pattern (%s): %s", pattern, err)
}
var matchVals []cty.Value
for _, match := range matches {
fi, err := os.Stat(match)
if err != nil {
return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to stat (%s): %s", match, err)
}
if !fi.Mode().IsRegular() {
continue
}
// Remove the path and file separator from matches.
match, err = filepath.Rel(path, match)
if err != nil {
return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to trim path of match (%s): %s", match, err)
}
// Replace any remaining file separators with forward slash (/)
// separators for cross-system compatibility.
match = filepath.ToSlash(match)
matchVals = append(matchVals, cty.StringVal(match))
}
if len(matchVals) == 0 {
return cty.SetValEmpty(cty.String), nil
}
return cty.SetVal(matchVals), nil
},
})
}
// BasenameFunc constructs a function that takes a string containing a filesystem path
// and removes all except the last portion from it.
var BasenameFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "path",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return cty.StringVal(filepath.Base(args[0].AsString())), nil
},
})
// DirnameFunc constructs a function that takes a string containing a filesystem path
// and removes the last portion from it.
var DirnameFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "path",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return cty.StringVal(filepath.Dir(args[0].AsString())), nil
},
})
// AbsPathFunc constructs a function that converts a filesystem path to an absolute path
var AbsPathFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "path",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
absPath, err := filepath.Abs(args[0].AsString())
return cty.StringVal(filepath.ToSlash(absPath)), err
},
})
// PathExpandFunc constructs a function that expands a leading ~ character to the current user's home directory.
var PathExpandFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "path",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
homePath, err := homedir.Expand(args[0].AsString())
return cty.StringVal(homePath), err
},
})
func readFileBytes(baseDir, path string) ([]byte, error) {
path, err := homedir.Expand(path)
if err != nil {
return nil, fmt.Errorf("failed to expand ~: %s", err)
}
if !filepath.IsAbs(path) {
path = filepath.Join(baseDir, path)
}
// Ensure that the path is canonical for the host OS
path = filepath.Clean(path)
src, err := ioutil.ReadFile(path)
if err != nil {
// ReadFile does not return Vagrant-user-friendly error
// messages, so we'll provide our own.
if os.IsNotExist(err) {
return nil, fmt.Errorf("no file exists at %s; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource", path)
}
return nil, fmt.Errorf("failed to read %s", path)
}
return src, nil
}
// File reads the contents of the file at the given path.
//
// The file must contain valid UTF-8 bytes, or this function will return an error.
//
// The underlying function implementation works relative to a particular base
// directory, so this wrapper takes a base directory string and uses it to
// construct the underlying function before calling it.
func File(baseDir string, path cty.Value) (cty.Value, error) {
fn := MakeFileFunc(baseDir, false)
return fn.Call([]cty.Value{path})
}
// FileExists determines whether a file exists at the given path.
//
// The underlying function implementation works relative to a particular base
// directory, so this wrapper takes a base directory string and uses it to
// construct the underlying function before calling it.
func FileExists(baseDir string, path cty.Value) (cty.Value, error) {
fn := MakeFileExistsFunc(baseDir)
return fn.Call([]cty.Value{path})
}
// FileSet enumerates a set of files given a glob pattern
//
// The underlying function implementation works relative to a particular base
// directory, so this wrapper takes a base directory string and uses it to
// construct the underlying function before calling it.
func FileSet(baseDir string, path, pattern cty.Value) (cty.Value, error) {
fn := MakeFileSetFunc(baseDir)
return fn.Call([]cty.Value{path, pattern})
}
// FileBase64 reads the contents of the file at the given path.
//
// The bytes from the file are encoded as base64 before returning.
//
// The underlying function implementation works relative to a particular base
// directory, so this wrapper takes a base directory string and uses it to
// construct the underlying function before calling it.
func FileBase64(baseDir string, path cty.Value) (cty.Value, error) {
fn := MakeFileFunc(baseDir, true)
return fn.Call([]cty.Value{path})
}
// Basename takes a string containing a filesystem path and removes all except the last portion from it.
//
// The underlying function implementation works only with the path string and does not access the filesystem itself.
// It is therefore unable to take into account filesystem features such as symlinks.
//
// If the path is empty then the result is ".", representing the current working directory.
func Basename(path cty.Value) (cty.Value, error) {
return BasenameFunc.Call([]cty.Value{path})
}
// Dirname takes a string containing a filesystem path and removes the last portion from it.
//
// The underlying function implementation works only with the path string and does not access the filesystem itself.
// It is therefore unable to take into account filesystem features such as symlinks.
//
// If the path is empty then the result is ".", representing the current working directory.
func Dirname(path cty.Value) (cty.Value, error) {
return DirnameFunc.Call([]cty.Value{path})
}
// Pathexpand takes a string that might begin with a `~` segment, and if so it replaces that segment with
// the current user's home directory path.
//
// The underlying function implementation works only with the path string and does not access the filesystem itself.
// It is therefore unable to take into account filesystem features such as symlinks.
//
// If the leading segment in the path is not `~` then the given path is returned unmodified.
func Pathexpand(path cty.Value) (cty.Value, error) {
return PathExpandFunc.Call([]cty.Value{path})
}

View File

@ -0,0 +1,636 @@
package funcs
import (
"fmt"
"path/filepath"
"testing"
homedir "github.com/mitchellh/go-homedir"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/function/stdlib"
)
func TestFile(t *testing.T) {
tests := []struct {
Path cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("testdata/filesystem/hello.txt"),
cty.StringVal("Hello World"),
false,
},
{
cty.StringVal("testdata/filesystem/icon.png"),
cty.NilVal,
true, // Not valid UTF-8
},
{
cty.StringVal("testdata/filesystem/missing"),
cty.NilVal,
true, // no file exists
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("File(\".\", %#v)", test.Path), func(t *testing.T) {
got, err := File(".", test.Path)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestTemplateFile(t *testing.T) {
tests := []struct {
Path cty.Value
Vars cty.Value
Want cty.Value
Err string
}{
{
cty.StringVal("testdata/filesystem/hello.txt"),
cty.EmptyObjectVal,
cty.StringVal("Hello World"),
``,
},
{
cty.StringVal("testdata/filesystem/icon.png"),
cty.EmptyObjectVal,
cty.NilVal,
`contents of testdata/filesystem/icon.png are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead`,
},
{
cty.StringVal("testdata/filesystem/missing"),
cty.EmptyObjectVal,
cty.NilVal,
`no file exists at testdata/filesystem/missing; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`,
},
{
cty.StringVal("testdata/filesystem/hello.tmpl"),
cty.MapVal(map[string]cty.Value{
"name": cty.StringVal("Jodie"),
}),
cty.StringVal("Hello, Jodie!"),
``,
},
{
cty.StringVal("testdata/filesystem/hello.tmpl"),
cty.MapVal(map[string]cty.Value{
"name!": cty.StringVal("Jodie"),
}),
cty.NilVal,
`invalid template variable name "name!": must start with a letter, followed by zero or more letters, digits, and underscores`,
},
{
cty.StringVal("testdata/filesystem/hello.tmpl"),
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Jimbo"),
}),
cty.StringVal("Hello, Jimbo!"),
``,
},
{
cty.StringVal("testdata/filesystem/hello.tmpl"),
cty.EmptyObjectVal,
cty.NilVal,
`vars map does not contain key "name", referenced at testdata/filesystem/hello.tmpl:1,10-14`,
},
{
cty.StringVal("testdata/filesystem/func.tmpl"),
cty.ObjectVal(map[string]cty.Value{
"list": cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
cty.StringVal("c"),
}),
}),
cty.StringVal("The items are a, b, c"),
``,
},
{
cty.StringVal("testdata/filesystem/recursive.tmpl"),
cty.MapValEmpty(cty.String),
cty.NilVal,
`testdata/filesystem/recursive.tmpl:1,3-16: Error in function call; Call to function "templatefile" failed: cannot recursively call templatefile from inside templatefile call.`,
},
{
cty.StringVal("testdata/filesystem/list.tmpl"),
cty.ObjectVal(map[string]cty.Value{
"list": cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
cty.StringVal("c"),
}),
}),
cty.StringVal("- a\n- b\n- c\n"),
``,
},
{
cty.StringVal("testdata/filesystem/list.tmpl"),
cty.ObjectVal(map[string]cty.Value{
"list": cty.True,
}),
cty.NilVal,
`testdata/filesystem/list.tmpl:1,13-17: Iteration over non-iterable value; A value of type bool cannot be used as the collection in a 'for' expression.`,
},
{
cty.StringVal("testdata/filesystem/bare.tmpl"),
cty.ObjectVal(map[string]cty.Value{
"val": cty.True,
}),
cty.True, // since this template contains only an interpolation, its true value shines through
``,
},
}
templateFileFn := MakeTemplateFileFunc(".", func() map[string]function.Function {
return map[string]function.Function{
"join": stdlib.JoinFunc,
"templatefile": MakeFileFunc(".", false), // just a placeholder, since templatefile itself overrides this
}
})
for _, test := range tests {
t.Run(fmt.Sprintf("TemplateFile(%#v, %#v)", test.Path, test.Vars), func(t *testing.T) {
got, err := templateFileFn.Call([]cty.Value{test.Path, test.Vars})
if argErr, ok := err.(function.ArgError); ok {
if argErr.Index < 0 || argErr.Index > 1 {
t.Errorf("ArgError index %d is out of range for templatefile (must be 0 or 1)", argErr.Index)
}
}
if test.Err != "" {
if err == nil {
t.Fatal("succeeded; want error")
}
if got, want := err.Error(), test.Err; got != want {
t.Errorf("wrong error\ngot: %s\nwant: %s", got, want)
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestFileExists(t *testing.T) {
tests := []struct {
Path cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("testdata/filesystem/hello.txt"),
cty.BoolVal(true),
false,
},
{
cty.StringVal(""), // empty path
cty.BoolVal(false),
true,
},
{
cty.StringVal("testdata/filesystem/missing"),
cty.BoolVal(false),
false, // no file exists
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("FileExists(\".\", %#v)", test.Path), func(t *testing.T) {
got, err := FileExists(".", test.Path)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestFileSet(t *testing.T) {
tests := []struct {
Path cty.Value
Pattern cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("."),
cty.StringVal("testdata*"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("{testdata,missing}"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/missing"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/missing*"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("*/missing"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("**/missing"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/filesystem/*.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/filesystem/hello.txt"),
}),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/filesystem/hello.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/filesystem/hello.txt"),
}),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/filesystem/hello.???"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/filesystem/hello.txt"),
}),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/filesystem/hello*"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/filesystem/hello.tmpl"),
cty.StringVal("testdata/filesystem/hello.txt"),
}),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/filesystem/hello.{tmpl,txt}"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/filesystem/hello.tmpl"),
cty.StringVal("testdata/filesystem/hello.txt"),
}),
false,
},
{
cty.StringVal("."),
cty.StringVal("*/*/hello.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/filesystem/hello.txt"),
}),
false,
},
{
cty.StringVal("."),
cty.StringVal("*/*/*.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/filesystem/hello.txt"),
}),
false,
},
{
cty.StringVal("."),
cty.StringVal("*/*/hello*"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/filesystem/hello.tmpl"),
cty.StringVal("testdata/filesystem/hello.txt"),
}),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/**/list*"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/filesystem/list.tmpl"),
}),
false,
},
{
cty.StringVal("."),
cty.StringVal("**/hello.{tmpl,txt}"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/filesystem/hello.tmpl"),
cty.StringVal("testdata/filesystem/hello.txt"),
}),
false,
},
{
cty.StringVal("."),
cty.StringVal("["),
cty.SetValEmpty(cty.String),
true,
},
{
cty.StringVal("."),
cty.StringVal("\\"),
cty.SetValEmpty(cty.String),
true,
},
{
cty.StringVal("testdata/filesystem"),
cty.StringVal("missing"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("testdata/filesystem"),
cty.StringVal("missing*"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("testdata/filesystem"),
cty.StringVal("*.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("hello.txt"),
}),
false,
},
{
cty.StringVal("testdata/filesystem"),
cty.StringVal("hello.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("hello.txt"),
}),
false,
},
{
cty.StringVal("testdata/filesystem"),
cty.StringVal("hello.???"),
cty.SetVal([]cty.Value{
cty.StringVal("hello.txt"),
}),
false,
},
{
cty.StringVal("testdata/filesystem"),
cty.StringVal("hello*"),
cty.SetVal([]cty.Value{
cty.StringVal("hello.tmpl"),
cty.StringVal("hello.txt"),
}),
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("FileSet(\".\", %#v, %#v)", test.Path, test.Pattern), func(t *testing.T) {
got, err := FileSet(".", test.Path, test.Pattern)
if test.Err {
if err == nil {
t.Fatalf("succeeded; want error\ngot: %#v", got)
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestFileBase64(t *testing.T) {
tests := []struct {
Path cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("testdata/filesystem/hello.txt"),
cty.StringVal("SGVsbG8gV29ybGQ="),
false,
},
{
cty.StringVal("testdata/filesystem/icon.png"),
cty.StringVal("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAq1BMVEX///9cTuVeUeRcTuZcTuZcT+VbSe1cTuVdT+MAAP9JSbZcT+VcTuZAQLFAQLJcTuVcTuZcUuBBQbA/P7JAQLJaTuRcT+RcTuVGQ7xAQLJVVf9cTuVcTuVGRMFeUeRbTeJcTuU/P7JeTeZbTOVcTeZAQLJBQbNAQLNaUORcTeZbT+VcTuRAQLNAQLRdTuRHR8xgUOdgUN9cTuVdTeRdT+VZTulcTuVAQLL///8+GmETAAAANnRSTlMApibw+osO6DcBB3fIX87+oRk3yehB0/Nj/gNs7nsTRv3dHmu//JYUMLVr3bssjxkgEK5CaxeK03nIAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAADoQAAA6EBvJf9gwAAAAd0SU1FB+EEBRIQDxZNTKsAAACCSURBVBjTfc7JFsFQEATQQpCYxyBEzJ55rvf/f0ZHcyQLvelTd1GngEwWycs5+UISyKLraSi9geWKK9Gr1j7AeqOJVtt2XtD1Bchef2BjQDAcCTC0CsA4mihMtXw2XwgsV2sFw812F+4P3y2GdI6nn3FGSs//4HJNAXDzU4Dg/oj/E+bsEbhf5cMsAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE3LTA0LTA1VDE4OjE2OjE1KzAyOjAws5bLVQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNy0wNC0wNVQxODoxNjoxNSswMjowMMLLc+kAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAAC3RFWHRUaXRsZQBHcm91cJYfIowAAABXelRYdFJhdyBwcm9maWxlIHR5cGUgaXB0YwAAeJzj8gwIcVYoKMpPy8xJ5VIAAyMLLmMLEyMTS5MUAxMgRIA0w2QDI7NUIMvY1MjEzMQcxAfLgEigSi4A6hcRdPJCNZUAAAAASUVORK5CYII="),
false,
},
{
cty.StringVal("testdata/filesystem/missing"),
cty.NilVal,
true, // no file exists
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("FileBase64(\".\", %#v)", test.Path), func(t *testing.T) {
got, err := FileBase64(".", test.Path)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestBasename(t *testing.T) {
tests := []struct {
Path cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("testdata/filesystem/hello.txt"),
cty.StringVal("hello.txt"),
false,
},
{
cty.StringVal("hello.txt"),
cty.StringVal("hello.txt"),
false,
},
{
cty.StringVal(""),
cty.StringVal("."),
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("Basename(%#v)", test.Path), func(t *testing.T) {
got, err := Basename(test.Path)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestDirname(t *testing.T) {
tests := []struct {
Path cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("testdata/filesystem/hello.txt"),
cty.StringVal("testdata/filesystem"),
false,
},
{
cty.StringVal("testdata/filesystem/foo/hello.txt"),
cty.StringVal("testdata/filesystem/foo"),
false,
},
{
cty.StringVal("hello.txt"),
cty.StringVal("."),
false,
},
{
cty.StringVal(""),
cty.StringVal("."),
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("Dirname(%#v)", test.Path), func(t *testing.T) {
got, err := Dirname(test.Path)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestPathExpand(t *testing.T) {
homePath, err := homedir.Dir()
if err != nil {
t.Fatalf("Error getting home directory: %v", err)
}
tests := []struct {
Path cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("~/test-file"),
cty.StringVal(filepath.Join(homePath, "test-file")),
false,
},
{
cty.StringVal("~/another/test/file"),
cty.StringVal(filepath.Join(homePath, "another/test/file")),
false,
},
{
cty.StringVal("/root/file"),
cty.StringVal("/root/file"),
false,
},
{
cty.StringVal("/"),
cty.StringVal("/"),
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("Dirname(%#v)", test.Path), func(t *testing.T) {
got, err := Pathexpand(test.Path)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}

View File

@ -0,0 +1,66 @@
package funcs
import (
ctyyaml "github.com/zclconf/go-cty-yaml"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/function/stdlib"
)
// Stdlib are the functions provided by the HCL stdlib.
func Stdlib() map[string]function.Function {
return map[string]function.Function{
"abs": stdlib.AbsoluteFunc,
"ceil": stdlib.CeilFunc,
"chomp": stdlib.ChompFunc,
"coalescelist": stdlib.CoalesceListFunc,
"compact": stdlib.CompactFunc,
"concat": stdlib.ConcatFunc,
"contains": stdlib.ContainsFunc,
"csvdecode": stdlib.CSVDecodeFunc,
"distinct": stdlib.DistinctFunc,
"element": stdlib.ElementFunc,
"chunklist": stdlib.ChunklistFunc,
"flatten": stdlib.FlattenFunc,
"floor": stdlib.FloorFunc,
"format": stdlib.FormatFunc,
"formatdate": stdlib.FormatDateFunc,
"formatlist": stdlib.FormatListFunc,
"indent": stdlib.IndentFunc,
"join": stdlib.JoinFunc,
"jsondecode": stdlib.JSONDecodeFunc,
"jsonencode": stdlib.JSONEncodeFunc,
"keys": stdlib.KeysFunc,
"log": stdlib.LogFunc,
"lower": stdlib.LowerFunc,
"max": stdlib.MaxFunc,
"merge": stdlib.MergeFunc,
"min": stdlib.MinFunc,
"parseint": stdlib.ParseIntFunc,
"pow": stdlib.PowFunc,
"range": stdlib.RangeFunc,
"regex": stdlib.RegexFunc,
"regexall": stdlib.RegexAllFunc,
"reverse": stdlib.ReverseListFunc,
"setintersection": stdlib.SetIntersectionFunc,
"setproduct": stdlib.SetProductFunc,
"setsubtract": stdlib.SetSubtractFunc,
"setunion": stdlib.SetUnionFunc,
"signum": stdlib.SignumFunc,
"slice": stdlib.SliceFunc,
"sort": stdlib.SortFunc,
"split": stdlib.SplitFunc,
"strrev": stdlib.ReverseFunc,
"substr": stdlib.SubstrFunc,
"timeadd": stdlib.TimeAddFunc,
"title": stdlib.TitleFunc,
"trim": stdlib.TrimFunc,
"trimprefix": stdlib.TrimPrefixFunc,
"trimspace": stdlib.TrimSpaceFunc,
"trimsuffix": stdlib.TrimSuffixFunc,
"upper": stdlib.UpperFunc,
"values": stdlib.ValuesFunc,
"yamldecode": ctyyaml.YAMLDecodeFunc,
"yamlencode": ctyyaml.YAMLEncodeFunc,
"zipmap": stdlib.ZipmapFunc,
}
}

View File

@ -0,0 +1 @@
${val}

View File

@ -0,0 +1 @@
The items are ${join(", ", list)}

View File

@ -0,0 +1 @@
Hello, ${name}!

View File

@ -0,0 +1 @@
Hello World

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

View File

@ -0,0 +1,3 @@
%{ for x in list ~}
- ${x}
%{ endfor ~}

View File

@ -0,0 +1 @@
${templatefile("recursive.tmpl", {})}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,8 @@
Commit two
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Changes to be committed:
# modified: A
#

View File

@ -0,0 +1 @@
ref: refs/heads/master

View File

@ -0,0 +1,7 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true

View File

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@ -0,0 +1,15 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to check the commit log message taken by
# applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit. The hook is
# allowed to edit the commit message file.
#
# To enable this hook, rename this file to "applypatch-msg".
. git-sh-setup
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
:

View File

@ -0,0 +1,24 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message. The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit. The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".
# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
# This example catches duplicate Signed-off-by lines.
test "" = "$(grep '^Signed-off-by: ' "$1" |
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
echo >&2 Duplicate Signed-off-by lines.
exit 1
}

View File

@ -0,0 +1,173 @@
#!/nix/store/5mkw2nn6ghpadr95hic5l84vfp5xyana-perl-5.30.2/bin/perl
use strict;
use warnings;
use IPC::Open2;
# An example hook script to integrate Watchman
# (https://facebook.github.io/watchman/) with git to speed up detecting
# new and modified files.
#
# The hook is passed a version (currently 2) and last update token
# formatted as a string and outputs to stdout a new update token and
# all files that have been modified since the update token. Paths must
# be relative to the root of the working tree and separated by a single NUL.
#
# To enable this hook, rename this file to "query-watchman" and set
# 'git config core.fsmonitor .git/hooks/query-watchman'
#
my ($version, $last_update_token) = @ARGV;
# Uncomment for debugging
# print STDERR "$0 $version $last_update_token\n";
# Check the hook interface version
if ($version ne 2) {
die "Unsupported query-fsmonitor hook version '$version'.\n" .
"Falling back to scanning...\n";
}
my $git_work_tree = get_working_dir();
my $retry = 1;
my $json_pkg;
eval {
require JSON::XS;
$json_pkg = "JSON::XS";
1;
} or do {
require JSON::PP;
$json_pkg = "JSON::PP";
};
launch_watchman();
sub launch_watchman {
my $o = watchman_query();
if (is_work_tree_watched($o)) {
output_result($o->{clock}, @{$o->{files}});
}
}
sub output_result {
my ($clockid, @files) = @_;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# binmode $fh, ":utf8";
# print $fh "$clockid\n@files\n";
# close $fh;
binmode STDOUT, ":utf8";
print $clockid;
print "\0";
local $, = "\0";
print @files;
}
sub watchman_clock {
my $response = qx/watchman clock "$git_work_tree"/;
die "Failed to get clock id on '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
return $json_pkg->new->utf8->decode($response);
}
sub watchman_query {
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
or die "open2() failed: $!\n" .
"Falling back to scanning...\n";
# In the query expression below we're asking for names of files that
# changed since $last_update_token but not from the .git folder.
#
# To accomplish this, we're using the "since" generator to use the
# recency index to select candidate nodes and "fields" to limit the
# output to file names only. Then we're using the "expression" term to
# further constrain the results.
if (substr($last_update_token, 0, 1) eq "c") {
$last_update_token = "\"$last_update_token\"";
}
my $query = <<" END";
["query", "$git_work_tree", {
"since": $last_update_token,
"fields": ["name"],
"expression": ["not", ["dirname", ".git"]]
}]
END
# Uncomment for debugging the watchman query
# open (my $fh, ">", ".git/watchman-query.json");
# print $fh $query;
# close $fh;
print CHLD_IN $query;
close CHLD_IN;
my $response = do {local $/; <CHLD_OUT>};
# Uncomment for debugging the watch response
# open ($fh, ">", ".git/watchman-response.json");
# print $fh $response;
# close $fh;
die "Watchman: command returned no output.\n" .
"Falling back to scanning...\n" if $response eq "";
die "Watchman: command returned invalid output: $response\n" .
"Falling back to scanning...\n" unless $response =~ /^\{/;
return $json_pkg->new->utf8->decode($response);
}
sub is_work_tree_watched {
my ($output) = @_;
my $error = $output->{error};
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
$retry--;
my $response = qx/watchman watch "$git_work_tree"/;
die "Failed to make watchman watch '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
$output = $json_pkg->new->utf8->decode($response);
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# close $fh;
# Watchman will always return all files on the first query so
# return the fast "everything is dirty" flag to git and do the
# Watchman query just to get it over with now so we won't pay
# the cost in git to look up each individual file.
my $o = watchman_clock();
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
output_result($o->{clock}, ("/"));
$last_update_token = $o->{clock};
eval { launch_watchman() };
return 0;
}
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
return 1;
}
sub get_working_dir {
my $working_dir;
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
$working_dir = Win32::GetCwd();
$working_dir =~ tr/\\/\//;
} else {
require Cwd;
$working_dir = Cwd::cwd();
}
return $working_dir;
}

View File

@ -0,0 +1,8 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to prepare a packed repository for use over
# dumb transports.
#
# To enable this hook, rename this file to "post-update".
exec git update-server-info

View File

@ -0,0 +1,14 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to verify what is about to be committed
# by applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-applypatch".
. git-sh-setup
precommit="$(git rev-parse --git-path hooks/pre-commit)"
test -x "$precommit" && exec "$precommit" ${1+"$@"}
:

View File

@ -0,0 +1,49 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=$(git hash-object -t tree /dev/null)
fi
# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --type=bool hooks.allownonascii)
# Redirect output to stderr.
exec 1>&2
# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
# Note that the use of brackets around a tr range is ok here, (it's
# even required, for portability to Solaris 10's /usr/bin/tr), since
# the square bracket bytes happen to fall in the designated range.
test $(git diff --cached --name-only --diff-filter=A -z $against |
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
cat <<\EOF
Error: Attempt to add a non-ASCII file name.
This can cause problems if you want to work with people on other platforms.
To be portable it is advisable to rename the file.
If you know what you are doing you can disable this check using:
git config hooks.allownonascii true
EOF
exit 1
fi
# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --

View File

@ -0,0 +1,13 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to verify what is about to be committed.
# Called by "git merge" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message to
# stderr if it wants to stop the merge commit.
#
# To enable this hook, rename this file to "pre-merge-commit".
. git-sh-setup
test -x "$GIT_DIR/hooks/pre-commit" &&
exec "$GIT_DIR/hooks/pre-commit"
:

View File

@ -0,0 +1,53 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
# An example hook script to verify what is about to be pushed. Called by "git
# push" after it has checked the remote status, but before anything has been
# pushed. If this script exits with a non-zero status nothing will be pushed.
#
# This hook is called with the following parameters:
#
# $1 -- Name of the remote to which the push is being done
# $2 -- URL to which the push is being done
#
# If pushing without using a named remote those arguments will be equal.
#
# Information about the commits which are being pushed is supplied as lines to
# the standard input in the form:
#
# <local ref> <local sha1> <remote ref> <remote sha1>
#
# This sample shows how to prevent push of commits where the log message starts
# with "WIP" (work in progress).
remote="$1"
url="$2"
z40=0000000000000000000000000000000000000000
while read local_ref local_sha remote_ref remote_sha
do
if [ "$local_sha" = $z40 ]
then
# Handle delete
:
else
if [ "$remote_sha" = $z40 ]
then
# New branch, examine all commits
range="$local_sha"
else
# Update to existing branch, examine new commits
range="$remote_sha..$local_sha"
fi
# Check for WIP commit
commit=`git rev-list -n 1 --grep '^WIP' "$range"`
if [ -n "$commit" ]
then
echo >&2 "Found WIP commit in $local_ref, not pushing"
exit 1
fi
fi
done
exit 0

View File

@ -0,0 +1,169 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# Copyright (c) 2006, 2008 Junio C Hamano
#
# The "pre-rebase" hook is run just before "git rebase" starts doing
# its job, and can prevent the command from running by exiting with
# non-zero status.
#
# The hook is called with the following parameters:
#
# $1 -- the upstream the series was forked from.
# $2 -- the branch being rebased (or empty when rebasing the current branch).
#
# This sample shows how to prevent topic branches that are already
# merged to 'next' branch from getting rebased, because allowing it
# would result in rebasing already published history.
publish=next
basebranch="$1"
if test "$#" = 2
then
topic="refs/heads/$2"
else
topic=`git symbolic-ref HEAD` ||
exit 0 ;# we do not interrupt rebasing detached HEAD
fi
case "$topic" in
refs/heads/??/*)
;;
*)
exit 0 ;# we do not interrupt others.
;;
esac
# Now we are dealing with a topic branch being rebased
# on top of master. Is it OK to rebase it?
# Does the topic really exist?
git show-ref -q "$topic" || {
echo >&2 "No such branch $topic"
exit 1
}
# Is topic fully merged to master?
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
if test -z "$not_in_master"
then
echo >&2 "$topic is fully merged to master; better remove it."
exit 1 ;# we could allow it, but there is no point.
fi
# Is topic ever merged to next? If so you should not be rebasing it.
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
only_next_2=`git rev-list ^master ${publish} | sort`
if test "$only_next_1" = "$only_next_2"
then
not_in_topic=`git rev-list "^$topic" master`
if test -z "$not_in_topic"
then
echo >&2 "$topic is already up to date with master"
exit 1 ;# we could allow it, but there is no point.
else
exit 0
fi
else
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
/nix/store/5mkw2nn6ghpadr95hic5l84vfp5xyana-perl-5.30.2/bin/perl -e '
my $topic = $ARGV[0];
my $msg = "* $topic has commits already merged to public branch:\n";
my (%not_in_next) = map {
/^([0-9a-f]+) /;
($1 => 1);
} split(/\n/, $ARGV[1]);
for my $elem (map {
/^([0-9a-f]+) (.*)$/;
[$1 => $2];
} split(/\n/, $ARGV[2])) {
if (!exists $not_in_next{$elem->[0]}) {
if ($msg) {
print STDERR $msg;
undef $msg;
}
print STDERR " $elem->[1]\n";
}
}
' "$topic" "$not_in_next" "$not_in_master"
exit 1
fi
<<\DOC_END
This sample hook safeguards topic branches that have been
published from being rewound.
The workflow assumed here is:
* Once a topic branch forks from "master", "master" is never
merged into it again (either directly or indirectly).
* Once a topic branch is fully cooked and merged into "master",
it is deleted. If you need to build on top of it to correct
earlier mistakes, a new topic branch is created by forking at
the tip of the "master". This is not strictly necessary, but
it makes it easier to keep your history simple.
* Whenever you need to test or publish your changes to topic
branches, merge them into "next" branch.
The script, being an example, hardcodes the publish branch name
to be "next", but it is trivial to make it configurable via
$GIT_DIR/config mechanism.
With this workflow, you would want to know:
(1) ... if a topic branch has ever been merged to "next". Young
topic branches can have stupid mistakes you would rather
clean up before publishing, and things that have not been
merged into other branches can be easily rebased without
affecting other people. But once it is published, you would
not want to rewind it.
(2) ... if a topic branch has been fully merged to "master".
Then you can delete it. More importantly, you should not
build on top of it -- other people may already want to
change things related to the topic as patches against your
"master", so if you need further changes, it is better to
fork the topic (perhaps with the same name) afresh from the
tip of "master".
Let's look at this example:
o---o---o---o---o---o---o---o---o---o "next"
/ / / /
/ a---a---b A / /
/ / / /
/ / c---c---c---c B /
/ / / \ /
/ / / b---b C \ /
/ / / / \ /
---o---o---o---o---o---o---o---o---o---o---o "master"
A, B and C are topic branches.
* A has one fix since it was merged up to "next".
* B has finished. It has been fully merged up to "master" and "next",
and is ready to be deleted.
* C has not merged to "next" at all.
We would want to allow C to be rebased, refuse A, and encourage
B to be deleted.
To compute (1):
git rev-list ^master ^topic next
git rev-list ^master next
if these match, topic has not merged in next at all.
To compute (2):
git rev-list master..topic
if this is empty, it is fully merged to "master".
DOC_END

View File

@ -0,0 +1,24 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to make use of push options.
# The example simply echoes all push options that start with 'echoback='
# and rejects all pushes when the "reject" push option is used.
#
# To enable this hook, rename this file to "pre-receive".
if test -n "$GIT_PUSH_OPTION_COUNT"
then
i=0
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
do
eval "value=\$GIT_PUSH_OPTION_$i"
case "$value" in
echoback=*)
echo "echo from the pre-receive-hook: ${value#*=}" >&2
;;
reject)
exit 1
esac
i=$((i + 1))
done
fi

View File

@ -0,0 +1,42 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to prepare the commit log message.
# Called by "git commit" with the name of the file that has the
# commit message, followed by the description of the commit
# message's source. The hook's purpose is to edit the commit
# message file. If the hook fails with a non-zero status,
# the commit is aborted.
#
# To enable this hook, rename this file to "prepare-commit-msg".
# This hook includes three examples. The first one removes the
# "# Please enter the commit message..." help message.
#
# The second includes the output of "git diff --name-status -r"
# into the message, just before the "git status" output. It is
# commented because it doesn't cope with --amend or with squashed
# commits.
#
# The third example adds a Signed-off-by line to the message, that can
# still be edited. This is rarely a good idea.
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
SHA1=$3
/nix/store/5mkw2nn6ghpadr95hic5l84vfp5xyana-perl-5.30.2/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
# case "$COMMIT_SOURCE,$SHA1" in
# ,|template,)
# /nix/store/5mkw2nn6ghpadr95hic5l84vfp5xyana-perl-5.30.2/bin/perl -i.bak -pe '
# print "\n" . `git diff --cached --name-status -r`
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
# *) ;;
# esac
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
# if test -z "$COMMIT_SOURCE"
# then
# /nix/store/5mkw2nn6ghpadr95hic5l84vfp5xyana-perl-5.30.2/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
# fi

View File

@ -0,0 +1,128 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to block unannotated tags from entering.
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
#
# To enable this hook, rename this file to "update".
#
# Config
# ------
# hooks.allowunannotated
# This boolean sets whether unannotated tags will be allowed into the
# repository. By default they won't be.
# hooks.allowdeletetag
# This boolean sets whether deleting tags will be allowed in the
# repository. By default they won't be.
# hooks.allowmodifytag
# This boolean sets whether a tag may be modified after creation. By default
# it won't be.
# hooks.allowdeletebranch
# This boolean sets whether deleting branches will be allowed in the
# repository. By default they won't be.
# hooks.denycreatebranch
# This boolean sets whether remotely creating branches will be denied
# in the repository. By default this is allowed.
#
# --- Command line
refname="$1"
oldrev="$2"
newrev="$3"
# --- Safety check
if [ -z "$GIT_DIR" ]; then
echo "Don't run this script from the command line." >&2
echo " (if you want, you could supply GIT_DIR then run" >&2
echo " $0 <ref> <oldrev> <newrev>)" >&2
exit 1
fi
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
exit 1
fi
# --- Config
allowunannotated=$(git config --type=bool hooks.allowunannotated)
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
# check for no description
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
case "$projectdesc" in
"Unnamed repository"* | "")
echo "*** Project description file hasn't been set" >&2
exit 1
;;
esac
# --- Check types
# if $newrev is 0000...0000, it's a commit to delete a ref.
zero="0000000000000000000000000000000000000000"
if [ "$newrev" = "$zero" ]; then
newrev_type=delete
else
newrev_type=$(git cat-file -t $newrev)
fi
case "$refname","$newrev_type" in
refs/tags/*,commit)
# un-annotated tag
short_refname=${refname##refs/tags/}
if [ "$allowunannotated" != "true" ]; then
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
exit 1
fi
;;
refs/tags/*,delete)
# delete tag
if [ "$allowdeletetag" != "true" ]; then
echo "*** Deleting a tag is not allowed in this repository" >&2
exit 1
fi
;;
refs/tags/*,tag)
# annotated tag
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
then
echo "*** Tag '$refname' already exists." >&2
echo "*** Modifying a tag is not allowed in this repository." >&2
exit 1
fi
;;
refs/heads/*,commit)
# branch
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
echo "*** Creating a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/heads/*,delete)
# delete branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/remotes/*,commit)
# tracking branch
;;
refs/remotes/*,delete)
# delete tracking branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
exit 1
fi
;;
*)
# Anything else (is there anything else?)
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
exit 1
;;
esac
# --- Finished
exit 0

Binary file not shown.

View File

@ -0,0 +1,6 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

View File

@ -0,0 +1,2 @@
0000000000000000000000000000000000000000 b1a2dcd337f590a185a20f013721e7410764bab4 Mitchell Hashimoto <mitchell.hashimoto@gmail.com> 1597446600 -0700 commit (initial): Commit one
b1a2dcd337f590a185a20f013721e7410764bab4 380afd697abe993b89bfa08d8dd8724d6a513ba1 Mitchell Hashimoto <mitchell.hashimoto@gmail.com> 1597446608 -0700 commit: Commit two

View File

@ -0,0 +1,2 @@
0000000000000000000000000000000000000000 b1a2dcd337f590a185a20f013721e7410764bab4 Mitchell Hashimoto <mitchell.hashimoto@gmail.com> 1597446600 -0700 commit (initial): Commit one
b1a2dcd337f590a185a20f013721e7410764bab4 380afd697abe993b89bfa08d8dd8724d6a513ba1 Mitchell Hashimoto <mitchell.hashimoto@gmail.com> 1597446608 -0700 commit: Commit two

View File

@ -0,0 +1 @@
380afd697abe993b89bfa08d8dd8724d6a513ba1

View File

@ -0,0 +1 @@
ref: refs/heads/master

View File

@ -0,0 +1,10 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/hashicorp/example.git
fetch = +refs/heads/*:refs/remotes/origin/*

View File

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@ -0,0 +1,15 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to check the commit log message taken by
# applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit. The hook is
# allowed to edit the commit message file.
#
# To enable this hook, rename this file to "applypatch-msg".
. git-sh-setup
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
:

View File

@ -0,0 +1,24 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message. The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit. The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".
# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
# This example catches duplicate Signed-off-by lines.
test "" = "$(grep '^Signed-off-by: ' "$1" |
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
echo >&2 Duplicate Signed-off-by lines.
exit 1
}

View File

@ -0,0 +1,173 @@
#!/nix/store/5mkw2nn6ghpadr95hic5l84vfp5xyana-perl-5.30.2/bin/perl
use strict;
use warnings;
use IPC::Open2;
# An example hook script to integrate Watchman
# (https://facebook.github.io/watchman/) with git to speed up detecting
# new and modified files.
#
# The hook is passed a version (currently 2) and last update token
# formatted as a string and outputs to stdout a new update token and
# all files that have been modified since the update token. Paths must
# be relative to the root of the working tree and separated by a single NUL.
#
# To enable this hook, rename this file to "query-watchman" and set
# 'git config core.fsmonitor .git/hooks/query-watchman'
#
my ($version, $last_update_token) = @ARGV;
# Uncomment for debugging
# print STDERR "$0 $version $last_update_token\n";
# Check the hook interface version
if ($version ne 2) {
die "Unsupported query-fsmonitor hook version '$version'.\n" .
"Falling back to scanning...\n";
}
my $git_work_tree = get_working_dir();
my $retry = 1;
my $json_pkg;
eval {
require JSON::XS;
$json_pkg = "JSON::XS";
1;
} or do {
require JSON::PP;
$json_pkg = "JSON::PP";
};
launch_watchman();
sub launch_watchman {
my $o = watchman_query();
if (is_work_tree_watched($o)) {
output_result($o->{clock}, @{$o->{files}});
}
}
sub output_result {
my ($clockid, @files) = @_;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# binmode $fh, ":utf8";
# print $fh "$clockid\n@files\n";
# close $fh;
binmode STDOUT, ":utf8";
print $clockid;
print "\0";
local $, = "\0";
print @files;
}
sub watchman_clock {
my $response = qx/watchman clock "$git_work_tree"/;
die "Failed to get clock id on '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
return $json_pkg->new->utf8->decode($response);
}
sub watchman_query {
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
or die "open2() failed: $!\n" .
"Falling back to scanning...\n";
# In the query expression below we're asking for names of files that
# changed since $last_update_token but not from the .git folder.
#
# To accomplish this, we're using the "since" generator to use the
# recency index to select candidate nodes and "fields" to limit the
# output to file names only. Then we're using the "expression" term to
# further constrain the results.
if (substr($last_update_token, 0, 1) eq "c") {
$last_update_token = "\"$last_update_token\"";
}
my $query = <<" END";
["query", "$git_work_tree", {
"since": $last_update_token,
"fields": ["name"],
"expression": ["not", ["dirname", ".git"]]
}]
END
# Uncomment for debugging the watchman query
# open (my $fh, ">", ".git/watchman-query.json");
# print $fh $query;
# close $fh;
print CHLD_IN $query;
close CHLD_IN;
my $response = do {local $/; <CHLD_OUT>};
# Uncomment for debugging the watch response
# open ($fh, ">", ".git/watchman-response.json");
# print $fh $response;
# close $fh;
die "Watchman: command returned no output.\n" .
"Falling back to scanning...\n" if $response eq "";
die "Watchman: command returned invalid output: $response\n" .
"Falling back to scanning...\n" unless $response =~ /^\{/;
return $json_pkg->new->utf8->decode($response);
}
sub is_work_tree_watched {
my ($output) = @_;
my $error = $output->{error};
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
$retry--;
my $response = qx/watchman watch "$git_work_tree"/;
die "Failed to make watchman watch '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
$output = $json_pkg->new->utf8->decode($response);
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# close $fh;
# Watchman will always return all files on the first query so
# return the fast "everything is dirty" flag to git and do the
# Watchman query just to get it over with now so we won't pay
# the cost in git to look up each individual file.
my $o = watchman_clock();
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
output_result($o->{clock}, ("/"));
$last_update_token = $o->{clock};
eval { launch_watchman() };
return 0;
}
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
return 1;
}
sub get_working_dir {
my $working_dir;
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
$working_dir = Win32::GetCwd();
$working_dir =~ tr/\\/\//;
} else {
require Cwd;
$working_dir = Cwd::cwd();
}
return $working_dir;
}

View File

@ -0,0 +1,8 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to prepare a packed repository for use over
# dumb transports.
#
# To enable this hook, rename this file to "post-update".
exec git update-server-info

View File

@ -0,0 +1,14 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to verify what is about to be committed
# by applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-applypatch".
. git-sh-setup
precommit="$(git rev-parse --git-path hooks/pre-commit)"
test -x "$precommit" && exec "$precommit" ${1+"$@"}
:

View File

@ -0,0 +1,49 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=$(git hash-object -t tree /dev/null)
fi
# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --type=bool hooks.allownonascii)
# Redirect output to stderr.
exec 1>&2
# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
# Note that the use of brackets around a tr range is ok here, (it's
# even required, for portability to Solaris 10's /usr/bin/tr), since
# the square bracket bytes happen to fall in the designated range.
test $(git diff --cached --name-only --diff-filter=A -z $against |
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
cat <<\EOF
Error: Attempt to add a non-ASCII file name.
This can cause problems if you want to work with people on other platforms.
To be portable it is advisable to rename the file.
If you know what you are doing you can disable this check using:
git config hooks.allownonascii true
EOF
exit 1
fi
# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --

View File

@ -0,0 +1,13 @@
#!/nix/store/ilgf30winx4zw3acm5pk79cvhzkjch0f-bash-4.4-p23/bin/bash
#
# An example hook script to verify what is about to be committed.
# Called by "git merge" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message to
# stderr if it wants to stop the merge commit.
#
# To enable this hook, rename this file to "pre-merge-commit".
. git-sh-setup
test -x "$GIT_DIR/hooks/pre-commit" &&
exec "$GIT_DIR/hooks/pre-commit"
:

Some files were not shown because too many files have changed in this diff Show More