146 lines
3.4 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package datasource
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/mitchellh/mapstructure"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
)
type GitSource struct{}
func newGitSource() Sourcer { return &GitSource{} }
func (s *GitSource) ProjectSource(body hcl.Body, ctx *hcl.EvalContext) (*vagrant_server.Job_DataSource, error) {
// Decode
var cfg gitConfig
if diag := gohcl.DecodeBody(body, ctx, &cfg); len(diag) > 0 {
return nil, diag
}
// Return the data source
return &vagrant_server.Job_DataSource{
Source: &vagrant_server.Job_DataSource_Git{
Git: &vagrant_server.Job_Git{
Url: cfg.Url,
Path: cfg.Path,
},
},
}, nil
}
func (s *GitSource) Override(raw *vagrant_server.Job_DataSource, m map[string]string) error {
src := raw.Source.(*vagrant_server.Job_DataSource_Git).Git
var md mapstructure.Metadata
if err := mapstructure.DecodeMetadata(m, src, &md); err != nil {
return err
}
if len(md.Unused) > 0 {
return fmt.Errorf("invalid override keys: %v", md.Unused)
}
return nil
}
func (s *GitSource) Get(
ctx context.Context,
log hclog.Logger,
ui terminal.UI,
raw *vagrant_server.Job_DataSource,
baseDir string,
) (string, func() error, error) {
source := raw.Source.(*vagrant_server.Job_DataSource_Git)
// Some quick validation
if p := source.Git.Path; p != "" {
if filepath.IsAbs(p) {
return "", nil, status.Errorf(codes.FailedPrecondition,
"git path must be relative")
}
for _, part := range filepath.SplitList(p) {
if part == ".." {
return "", nil, status.Errorf(codes.FailedPrecondition,
"git path may not contain '..'")
}
}
}
// Create a temporary directory where we will store the cloned data.
td, err := ioutil.TempDir(baseDir, "vagrant")
if err != nil {
return "", nil, err
}
closer := func() error {
return os.RemoveAll(td)
}
// Output
ui.Output("Cloning data from Git", terminal.WithHeaderStyle())
ui.Output("URL: %s", source.Git.Url, terminal.WithInfoStyle())
if source.Git.Ref != "" {
ui.Output("Ref: %s", source.Git.Ref, terminal.WithInfoStyle())
}
// Clone
var output bytes.Buffer
cmd := exec.CommandContext(ctx, "git", "clone", source.Git.Url, td)
cmd.Stdout = &output
cmd.Stderr = &output
cmd.Stdin = nil
if err := cmd.Run(); err != nil {
closer()
return "", nil, status.Errorf(codes.Aborted,
"Git clone failed: %s", output.String())
}
// Checkout if we have a ref. If we don't have a ref we use the
// default of whatever we got.
if ref := source.Git.Ref; ref != "" {
output.Reset()
cmd := exec.CommandContext(ctx, "git", "checkout", ref)
cmd.Dir = td
cmd.Stdout = &output
cmd.Stderr = &output
cmd.Stdin = nil
if err := cmd.Run(); err != nil {
closer()
return "", nil, status.Errorf(codes.Aborted,
"Git checkout failed: %s", output.String())
}
}
// If we have a path, set it.
result := td
if p := source.Git.Path; p != "" {
result = filepath.Join(result, p)
}
return result, closer, nil
}
type gitConfig struct {
Url string `hcl:"url,attr"`
Path string `hcl:"path,optional"`
}
var _ Sourcer = (*GitSource)(nil)