Add gogo
This commit is contained in:
parent
20fbe05c23
commit
c3ee750db1
6
.gitignore
vendored
6
.gitignore
vendored
@ -13,6 +13,11 @@ boxes/*
|
|||||||
/website/build
|
/website/build
|
||||||
/vagrant-spec.config.rb
|
/vagrant-spec.config.rb
|
||||||
test/vagrant-spec/.vagrant/
|
test/vagrant-spec/.vagrant/
|
||||||
|
.vagrant/
|
||||||
|
/vagrant
|
||||||
|
/pkg
|
||||||
|
data.db
|
||||||
|
vagrant-restore.db.lock
|
||||||
|
|
||||||
# Bundler/Rubygems
|
# Bundler/Rubygems
|
||||||
*.gem
|
*.gem
|
||||||
@ -40,6 +45,7 @@ doc/
|
|||||||
.idea/*
|
.idea/*
|
||||||
*.iml
|
*.iml
|
||||||
.project
|
.project
|
||||||
|
.vscode
|
||||||
|
|
||||||
# Ruby Managers
|
# Ruby Managers
|
||||||
.rbenv
|
.rbenv
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal 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
41
Dockerfile
Normal 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
58
Makefile
Normal 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
10
builtin/README.md
Normal 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
112
builtin/myplugin/command.go
Normal 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
12
builtin/myplugin/main.go
Normal 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{}),
|
||||||
|
}
|
||||||
139
builtin/myplugin/plugin.pb.go
Normal file
139
builtin/myplugin/plugin.pb.go
Normal 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
|
||||||
|
}
|
||||||
8
builtin/myplugin/plugin.proto
Normal file
8
builtin/myplugin/plugin.proto
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package myplugin;
|
||||||
|
|
||||||
|
option go_package = "vagrant-agogo/builtin/myplugin";
|
||||||
|
|
||||||
|
message UpResult {}
|
||||||
|
|
||||||
74
builtin/myplugin/provider.go
Normal file
74
builtin/myplugin/provider.go
Normal 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)
|
||||||
|
)
|
||||||
74
cmd/vagrant-entrypoint/main.go
Normal file
74
cmd/vagrant-entrypoint/main.go
Normal 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
15
cmd/vagrant/main.go
Normal 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
114
go.mod
Normal 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
|
||||||
|
)
|
||||||
2
internal/assets/.gitignore
vendored
Normal file
2
internal/assets/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ceb/ceb
|
||||||
|
prod.go
|
||||||
35
internal/assets/dev.go
Normal file
35
internal/assets/dev.go
Normal 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"
|
||||||
|
}
|
||||||
235
internal/assets/dev_assets.go
Normal file
235
internal/assets/dev_assets.go
Normal 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
580
internal/cli/base.go
Normal 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
74
internal/cli/base_init.go
Normal 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...)
|
||||||
|
}
|
||||||
250
internal/cli/datagen/datagen.go
Normal file
250
internal/cli/datagen/datagen.go
Normal 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
120
internal/cli/dynamic.go
Normal 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
144
internal/cli/help.go
Normal 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
401
internal/cli/main.go
Normal 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
77
internal/cli/option.go
Normal 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
29
internal/cli/plugin.go
Normal 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
120
internal/cli/ui.go
Normal 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
58
internal/cli/version.go
Normal 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.
|
||||||
|
`)
|
||||||
|
}
|
||||||
33
internal/clicontext/config.go
Normal file
33
internal/clicontext/config.go
Normal 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)
|
||||||
|
}
|
||||||
274
internal/clicontext/storage.go
Normal file
274
internal/clicontext/storage.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
163
internal/clicontext/storage_test.go
Normal file
163
internal/clicontext/storage_test.go
Normal 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"))
|
||||||
|
}
|
||||||
23
internal/clicontext/testing.go
Normal file
23
internal/clicontext/testing.go
Normal 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
296
internal/client/basis.go
Normal 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
6
internal/client/doc.go
Normal 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
383
internal/client/job.go
Normal 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
|
||||||
44
internal/client/machine.go
Normal file
44
internal/client/machine.go
Normal 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
26
internal/client/noop.go
Normal 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
|
||||||
|
}
|
||||||
31
internal/client/noop_test.go
Normal file
31
internal/client/noop_test.go
Normal 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))
|
||||||
|
}
|
||||||
150
internal/client/operation.go
Normal file
150
internal/client/operation.go
Normal 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
162
internal/client/project.go
Normal 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
27
internal/client/runner.go
Normal 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
233
internal/client/server.go
Normal 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
|
||||||
|
}
|
||||||
69
internal/client/testing.go
Normal file
69
internal/client/testing.go
Normal 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
|
||||||
|
// }
|
||||||
23
internal/clierrors/detect.go
Normal file
23
internal/clierrors/detect.go
Normal 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
|
||||||
|
}
|
||||||
31
internal/clierrors/detect_test.go
Normal file
31
internal/clierrors/detect_test.go
Normal 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, "")))
|
||||||
|
})
|
||||||
|
}
|
||||||
23
internal/clierrors/humanize.go
Normal file
23
internal/clierrors/humanize.go
Normal 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
34
internal/config/basis.go
Normal 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
|
||||||
|
}
|
||||||
12
internal/config/communicator.go
Normal file
12
internal/config/communicator.go
Normal 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
134
internal/config/config.go
Normal 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
|
||||||
|
// }
|
||||||
66
internal/config/config_test.go
Normal file
66
internal/config/config_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
66
internal/config/eval_context.go
Normal file
66
internal/config/eval_context.go
Normal 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
|
||||||
|
}
|
||||||
149
internal/config/funcs/encoding.go
Normal file
149
internal/config/funcs/encoding.go
Normal 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})
|
||||||
|
}
|
||||||
165
internal/config/funcs/encoding_test.go
Normal file
165
internal/config/funcs/encoding_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
475
internal/config/funcs/filesystem.go
Normal file
475
internal/config/funcs/filesystem.go
Normal 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})
|
||||||
|
}
|
||||||
636
internal/config/funcs/filesystem_test.go
Normal file
636
internal/config/funcs/filesystem_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
66
internal/config/funcs/stdlib.go
Normal file
66
internal/config/funcs/stdlib.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
1
internal/config/funcs/testdata/filesystem/bare.tmpl
vendored
Normal file
1
internal/config/funcs/testdata/filesystem/bare.tmpl
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
${val}
|
||||||
1
internal/config/funcs/testdata/filesystem/func.tmpl
vendored
Normal file
1
internal/config/funcs/testdata/filesystem/func.tmpl
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
The items are ${join(", ", list)}
|
||||||
1
internal/config/funcs/testdata/filesystem/hello.tmpl
vendored
Normal file
1
internal/config/funcs/testdata/filesystem/hello.tmpl
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
Hello, ${name}!
|
||||||
1
internal/config/funcs/testdata/filesystem/hello.txt
vendored
Normal file
1
internal/config/funcs/testdata/filesystem/hello.txt
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
Hello World
|
||||||
BIN
internal/config/funcs/testdata/filesystem/icon.png
vendored
Normal file
BIN
internal/config/funcs/testdata/filesystem/icon.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 806 B |
3
internal/config/funcs/testdata/filesystem/list.tmpl
vendored
Normal file
3
internal/config/funcs/testdata/filesystem/list.tmpl
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
%{ for x in list ~}
|
||||||
|
- ${x}
|
||||||
|
%{ endfor ~}
|
||||||
1
internal/config/funcs/testdata/filesystem/recursive.tmpl
vendored
Normal file
1
internal/config/funcs/testdata/filesystem/recursive.tmpl
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
${templatefile("recursive.tmpl", {})}
|
||||||
1
internal/config/funcs/testdata/git-commits/A
vendored
Normal file
1
internal/config/funcs/testdata/git-commits/A
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
8
internal/config/funcs/testdata/git-commits/DOTgit/COMMIT_EDITMSG
vendored
Normal file
8
internal/config/funcs/testdata/git-commits/DOTgit/COMMIT_EDITMSG
vendored
Normal 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
|
||||||
|
#
|
||||||
1
internal/config/funcs/testdata/git-commits/DOTgit/HEAD
vendored
Normal file
1
internal/config/funcs/testdata/git-commits/DOTgit/HEAD
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
ref: refs/heads/master
|
||||||
7
internal/config/funcs/testdata/git-commits/DOTgit/config
vendored
Normal file
7
internal/config/funcs/testdata/git-commits/DOTgit/config
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = false
|
||||||
|
logallrefupdates = true
|
||||||
|
ignorecase = true
|
||||||
|
precomposeunicode = true
|
||||||
1
internal/config/funcs/testdata/git-commits/DOTgit/description
vendored
Normal file
1
internal/config/funcs/testdata/git-commits/DOTgit/description
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
Unnamed repository; edit this file 'description' to name the repository.
|
||||||
15
internal/config/funcs/testdata/git-commits/DOTgit/hooks/applypatch-msg.sample
vendored
Executable file
15
internal/config/funcs/testdata/git-commits/DOTgit/hooks/applypatch-msg.sample
vendored
Executable 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+"$@"}
|
||||||
|
:
|
||||||
24
internal/config/funcs/testdata/git-commits/DOTgit/hooks/commit-msg.sample
vendored
Executable file
24
internal/config/funcs/testdata/git-commits/DOTgit/hooks/commit-msg.sample
vendored
Executable 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
|
||||||
|
}
|
||||||
173
internal/config/funcs/testdata/git-commits/DOTgit/hooks/fsmonitor-watchman.sample
vendored
Executable file
173
internal/config/funcs/testdata/git-commits/DOTgit/hooks/fsmonitor-watchman.sample
vendored
Executable 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;
|
||||||
|
}
|
||||||
8
internal/config/funcs/testdata/git-commits/DOTgit/hooks/post-update.sample
vendored
Executable file
8
internal/config/funcs/testdata/git-commits/DOTgit/hooks/post-update.sample
vendored
Executable 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
|
||||||
14
internal/config/funcs/testdata/git-commits/DOTgit/hooks/pre-applypatch.sample
vendored
Executable file
14
internal/config/funcs/testdata/git-commits/DOTgit/hooks/pre-applypatch.sample
vendored
Executable 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+"$@"}
|
||||||
|
:
|
||||||
49
internal/config/funcs/testdata/git-commits/DOTgit/hooks/pre-commit.sample
vendored
Executable file
49
internal/config/funcs/testdata/git-commits/DOTgit/hooks/pre-commit.sample
vendored
Executable 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 --
|
||||||
13
internal/config/funcs/testdata/git-commits/DOTgit/hooks/pre-merge-commit.sample
vendored
Executable file
13
internal/config/funcs/testdata/git-commits/DOTgit/hooks/pre-merge-commit.sample
vendored
Executable 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"
|
||||||
|
:
|
||||||
53
internal/config/funcs/testdata/git-commits/DOTgit/hooks/pre-push.sample
vendored
Executable file
53
internal/config/funcs/testdata/git-commits/DOTgit/hooks/pre-push.sample
vendored
Executable 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
|
||||||
169
internal/config/funcs/testdata/git-commits/DOTgit/hooks/pre-rebase.sample
vendored
Executable file
169
internal/config/funcs/testdata/git-commits/DOTgit/hooks/pre-rebase.sample
vendored
Executable 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
|
||||||
24
internal/config/funcs/testdata/git-commits/DOTgit/hooks/pre-receive.sample
vendored
Executable file
24
internal/config/funcs/testdata/git-commits/DOTgit/hooks/pre-receive.sample
vendored
Executable 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
|
||||||
42
internal/config/funcs/testdata/git-commits/DOTgit/hooks/prepare-commit-msg.sample
vendored
Executable file
42
internal/config/funcs/testdata/git-commits/DOTgit/hooks/prepare-commit-msg.sample
vendored
Executable 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
|
||||||
128
internal/config/funcs/testdata/git-commits/DOTgit/hooks/update.sample
vendored
Executable file
128
internal/config/funcs/testdata/git-commits/DOTgit/hooks/update.sample
vendored
Executable 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
|
||||||
BIN
internal/config/funcs/testdata/git-commits/DOTgit/index
vendored
Normal file
BIN
internal/config/funcs/testdata/git-commits/DOTgit/index
vendored
Normal file
Binary file not shown.
6
internal/config/funcs/testdata/git-commits/DOTgit/info/exclude
vendored
Normal file
6
internal/config/funcs/testdata/git-commits/DOTgit/info/exclude
vendored
Normal 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]
|
||||||
|
# *~
|
||||||
2
internal/config/funcs/testdata/git-commits/DOTgit/logs/HEAD
vendored
Normal file
2
internal/config/funcs/testdata/git-commits/DOTgit/logs/HEAD
vendored
Normal 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
|
||||||
2
internal/config/funcs/testdata/git-commits/DOTgit/logs/refs/heads/master
vendored
Normal file
2
internal/config/funcs/testdata/git-commits/DOTgit/logs/refs/heads/master
vendored
Normal 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
|
||||||
BIN
internal/config/funcs/testdata/git-commits/DOTgit/objects/38/0afd697abe993b89bfa08d8dd8724d6a513ba1
vendored
Normal file
BIN
internal/config/funcs/testdata/git-commits/DOTgit/objects/38/0afd697abe993b89bfa08d8dd8724d6a513ba1
vendored
Normal file
Binary file not shown.
BIN
internal/config/funcs/testdata/git-commits/DOTgit/objects/45/9023a450b8e8aa344d230839d41e2f115d3d28
vendored
Normal file
BIN
internal/config/funcs/testdata/git-commits/DOTgit/objects/45/9023a450b8e8aa344d230839d41e2f115d3d28
vendored
Normal file
Binary file not shown.
BIN
internal/config/funcs/testdata/git-commits/DOTgit/objects/7c/178d1296d8b87e83382c324aeb32e2def2a5af
vendored
Normal file
BIN
internal/config/funcs/testdata/git-commits/DOTgit/objects/7c/178d1296d8b87e83382c324aeb32e2def2a5af
vendored
Normal file
Binary file not shown.
BIN
internal/config/funcs/testdata/git-commits/DOTgit/objects/8b/137891791fe96927ad78e64b0aad7bded08bdc
vendored
Normal file
BIN
internal/config/funcs/testdata/git-commits/DOTgit/objects/8b/137891791fe96927ad78e64b0aad7bded08bdc
vendored
Normal file
Binary file not shown.
BIN
internal/config/funcs/testdata/git-commits/DOTgit/objects/b1/a2dcd337f590a185a20f013721e7410764bab4
vendored
Normal file
BIN
internal/config/funcs/testdata/git-commits/DOTgit/objects/b1/a2dcd337f590a185a20f013721e7410764bab4
vendored
Normal file
Binary file not shown.
BIN
internal/config/funcs/testdata/git-commits/DOTgit/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
vendored
Normal file
BIN
internal/config/funcs/testdata/git-commits/DOTgit/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
vendored
Normal file
Binary file not shown.
1
internal/config/funcs/testdata/git-commits/DOTgit/refs/heads/master
vendored
Normal file
1
internal/config/funcs/testdata/git-commits/DOTgit/refs/heads/master
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
380afd697abe993b89bfa08d8dd8724d6a513ba1
|
||||||
1
internal/config/funcs/testdata/git-remote/DOTgit/HEAD
vendored
Normal file
1
internal/config/funcs/testdata/git-remote/DOTgit/HEAD
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
ref: refs/heads/master
|
||||||
10
internal/config/funcs/testdata/git-remote/DOTgit/config
vendored
Normal file
10
internal/config/funcs/testdata/git-remote/DOTgit/config
vendored
Normal 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/*
|
||||||
1
internal/config/funcs/testdata/git-remote/DOTgit/description
vendored
Normal file
1
internal/config/funcs/testdata/git-remote/DOTgit/description
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
Unnamed repository; edit this file 'description' to name the repository.
|
||||||
15
internal/config/funcs/testdata/git-remote/DOTgit/hooks/applypatch-msg.sample
vendored
Executable file
15
internal/config/funcs/testdata/git-remote/DOTgit/hooks/applypatch-msg.sample
vendored
Executable 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+"$@"}
|
||||||
|
:
|
||||||
24
internal/config/funcs/testdata/git-remote/DOTgit/hooks/commit-msg.sample
vendored
Executable file
24
internal/config/funcs/testdata/git-remote/DOTgit/hooks/commit-msg.sample
vendored
Executable 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
|
||||||
|
}
|
||||||
173
internal/config/funcs/testdata/git-remote/DOTgit/hooks/fsmonitor-watchman.sample
vendored
Executable file
173
internal/config/funcs/testdata/git-remote/DOTgit/hooks/fsmonitor-watchman.sample
vendored
Executable 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;
|
||||||
|
}
|
||||||
8
internal/config/funcs/testdata/git-remote/DOTgit/hooks/post-update.sample
vendored
Executable file
8
internal/config/funcs/testdata/git-remote/DOTgit/hooks/post-update.sample
vendored
Executable 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
|
||||||
14
internal/config/funcs/testdata/git-remote/DOTgit/hooks/pre-applypatch.sample
vendored
Executable file
14
internal/config/funcs/testdata/git-remote/DOTgit/hooks/pre-applypatch.sample
vendored
Executable 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+"$@"}
|
||||||
|
:
|
||||||
49
internal/config/funcs/testdata/git-remote/DOTgit/hooks/pre-commit.sample
vendored
Executable file
49
internal/config/funcs/testdata/git-remote/DOTgit/hooks/pre-commit.sample
vendored
Executable 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 --
|
||||||
13
internal/config/funcs/testdata/git-remote/DOTgit/hooks/pre-merge-commit.sample
vendored
Executable file
13
internal/config/funcs/testdata/git-remote/DOTgit/hooks/pre-merge-commit.sample
vendored
Executable 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
Loading…
x
Reference in New Issue
Block a user