Add gogo
This commit is contained in:
parent
20fbe05c23
commit
c3ee750db1
6
.gitignore
vendored
6
.gitignore
vendored
@ -13,6 +13,11 @@ boxes/*
|
||||
/website/build
|
||||
/vagrant-spec.config.rb
|
||||
test/vagrant-spec/.vagrant/
|
||||
.vagrant/
|
||||
/vagrant
|
||||
/pkg
|
||||
data.db
|
||||
vagrant-restore.db.lock
|
||||
|
||||
# Bundler/Rubygems
|
||||
*.gem
|
||||
@ -40,6 +45,7 @@ doc/
|
||||
.idea/*
|
||||
*.iml
|
||||
.project
|
||||
.vscode
|
||||
|
||||
# Ruby Managers
|
||||
.rbenv
|
||||
|
||||
3
.gitmodules
vendored
Normal file
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