350 lines
8.8 KiB
Go
350 lines
8.8 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package core
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/h2non/filetype"
|
|
"github.com/hashicorp/go-getter"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/core"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/helper/path"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/localizer"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
|
|
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
)
|
|
|
|
const (
|
|
TempPrefix = "vagrant-box-add-temp-"
|
|
VagrantSlash = "-VAGRANTSLASH-"
|
|
VagrantColon = "-VAGRANTCOLON-"
|
|
)
|
|
|
|
type BoxCollection struct {
|
|
basis *Basis
|
|
directory string
|
|
logger hclog.Logger
|
|
}
|
|
|
|
func NewBoxCollection(basis *Basis, dir string, logger hclog.Logger) (bc *BoxCollection, err error) {
|
|
bc = &BoxCollection{
|
|
basis: basis,
|
|
directory: dir,
|
|
logger: logger,
|
|
}
|
|
err = bc.RecoverBoxes()
|
|
return
|
|
}
|
|
|
|
// This adds a new box to the system.
|
|
// There are some exceptional cases:
|
|
// - BoxAlreadyExists - The box you're attempting to add already exists.
|
|
// - BoxProviderDoesntMatch - If the given box provider doesn't match the
|
|
// actual box provider in the untarred box.
|
|
// - BoxUnpackageFailure - An invalid tar file.
|
|
func (b *BoxCollection) Add(p path.Path, name, version, metadataURL string, force bool, providers ...string) (box core.Box, err error) {
|
|
if _, err := os.Stat(p.String()); err != nil {
|
|
return nil, fmt.Errorf("Could not add box, unable to find path %s", p.String())
|
|
}
|
|
exists, err := b.Find(name, version, providers...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if exists != nil && !force {
|
|
return nil, fmt.Errorf("Box already exits, can't add %s v%s", name, version)
|
|
} else {
|
|
if exists != nil {
|
|
// If the box already exists but force is enabled, then delete the box
|
|
exists.Destroy()
|
|
}
|
|
}
|
|
|
|
tempDir := filepath.Join(b.basis.dir.TempDir().String(), "box-extractor")
|
|
err = os.MkdirAll(tempDir, 0755)
|
|
if err != nil {
|
|
return nil, err
|
|
} // delete tempdir when finished
|
|
defer os.RemoveAll(tempDir)
|
|
b.logger.Debug("Unpacking box")
|
|
boxFile, err := os.Open(p.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buffer := make([]byte, 512)
|
|
n, err := boxFile.Read(buffer)
|
|
if err != nil && err != io.EOF {
|
|
return nil, err
|
|
}
|
|
io.MultiReader(bytes.NewBuffer(buffer[:n]), boxFile)
|
|
typ, err := filetype.Match(buffer)
|
|
ext := typ.Extension
|
|
if typ.Extension == "gz" {
|
|
ext = "tar.gz"
|
|
}
|
|
decompressor := getter.Decompressors[ext]
|
|
err = decompressor.Decompress(tempDir, p.String(), true, os.ModeDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check if the box is a V1 Vagrant box
|
|
if b.isV1Box(tempDir) {
|
|
b.basis.ui.Output(
|
|
localizer.LocalizeMsg("adding_v1_box", map[string]string{"BoxName": name}),
|
|
)
|
|
tempDir, err = b.upgradeV1Box(tempDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
newBox, err := NewBox(
|
|
BoxWithBasis(b.basis),
|
|
BoxWithBox(&vagrant_server.Box{
|
|
Name: name,
|
|
Version: version,
|
|
Directory: tempDir,
|
|
}),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
provider := newBox.box.Provider
|
|
|
|
if providers != nil {
|
|
foundProvider := false
|
|
for _, p := range providers {
|
|
if p == provider {
|
|
foundProvider = true
|
|
break
|
|
}
|
|
}
|
|
if !foundProvider {
|
|
return nil, fmt.Errorf("could not add box %s, provider '%s' does not match the expected providers %s", p.String(), provider, providers)
|
|
}
|
|
}
|
|
|
|
destDir := filepath.Join(b.directory, b.generateDirectoryName(name), version, provider)
|
|
b.logger.Debug("Box directory: %s", destDir)
|
|
os.MkdirAll(destDir, 0755)
|
|
// Copy the contents of the tempdir to the final dir
|
|
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, erro error) (err error) {
|
|
destPath, err := validateNewPath(filepath.Join(destDir, info.Name()), destDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
err = os.MkdirAll(destPath, info.Mode())
|
|
return err
|
|
} else {
|
|
data, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer data.Close()
|
|
dest, err := os.Create(destPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dest.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(dest, data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return
|
|
})
|
|
|
|
newBox, err = NewBox(
|
|
BoxWithBasis(b.basis),
|
|
BoxWithBox(&vagrant_server.Box{
|
|
Name: name,
|
|
Version: version,
|
|
Directory: destDir,
|
|
Provider: provider,
|
|
MetadataUrl: metadataURL,
|
|
}),
|
|
)
|
|
newBox.Save()
|
|
return newBox, nil
|
|
}
|
|
|
|
// This returns an array of all the boxes on the system
|
|
func (b *BoxCollection) All() (boxes []core.Box, err error) {
|
|
resp, err := b.basis.client.ListBoxes(
|
|
b.basis.ctx,
|
|
&emptypb.Empty{},
|
|
)
|
|
boxes = []core.Box{}
|
|
for _, boxRef := range resp.Boxes {
|
|
box, err := NewBox(
|
|
BoxWithBasis(b.basis),
|
|
BoxWithRef(boxRef, b.basis.ctx),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
boxes = append(boxes, box)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Find a box in the collection with the given name, version and provider.
|
|
func (b *BoxCollection) Find(name, version string, providers ...string) (box core.Box, err error) {
|
|
// If no providers are spcified then search for any provider
|
|
if len(providers) == 0 {
|
|
providers = append(providers, "")
|
|
}
|
|
for _, provider := range providers {
|
|
resp, err := b.basis.client.FindBox(
|
|
b.basis.ctx,
|
|
&vagrant_server.FindBoxRequest{
|
|
Box: &vagrant_plugin_sdk.Ref_Box{
|
|
Name: name, Version: version, Provider: provider,
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Box != nil {
|
|
// Return the first box that is found
|
|
return NewBox(
|
|
BoxWithBasis(b.basis),
|
|
BoxWithBox(resp.Box),
|
|
)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Cleans the directory for a box by removing the folders that are
|
|
// empty.
|
|
func (b *BoxCollection) Clean(name string) (err error) {
|
|
path := filepath.Join(b.directory, name)
|
|
return os.RemoveAll(path)
|
|
}
|
|
|
|
func (b *BoxCollection) RecoverBoxes() (err error) {
|
|
resp, err := b.basis.client.ListBoxes(
|
|
b.basis.ctx,
|
|
&emptypb.Empty{},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Ensure that each box exists
|
|
for _, boxRef := range resp.Boxes {
|
|
box, erro := b.basis.client.GetBox(b.basis.ctx, &vagrant_server.GetBoxRequest{Box: boxRef})
|
|
// If the box directory does not exist, then the box doesn't exist.
|
|
if _, err := os.Stat(box.Box.Directory); err != nil {
|
|
// Remove the box
|
|
_, erro := b.basis.client.DeleteBox(b.basis.ctx, &vagrant_server.DeleteBoxRequest{Box: boxRef})
|
|
if erro != nil {
|
|
return erro
|
|
}
|
|
}
|
|
if erro != nil {
|
|
return erro
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (b *BoxCollection) generateDirectoryName(path string) (out string) {
|
|
out = strings.ReplaceAll(path, ":", VagrantColon)
|
|
return strings.ReplaceAll(out, "/", VagrantSlash)
|
|
}
|
|
|
|
func validateNewPath(path string, parentPath string) (newPath string, err error) {
|
|
newPath, err = filepath.Abs(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// Ensure that the newPath is within the parentPath
|
|
if !strings.HasPrefix(newPath, parentPath) {
|
|
return "", fmt.Errorf("could not add box outside of box directory %s", parentPath)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Checks is the given directory represents a V1 box
|
|
func (b *BoxCollection) isV1Box(dir string) bool {
|
|
// If there is a box.ovf file then there is a good chance that this is a V1 box
|
|
boxOvfPath := filepath.Join(dir, "box.ovf")
|
|
if _, err := os.Stat(boxOvfPath); errors.Is(err, os.ErrNotExist) {
|
|
return false
|
|
}
|
|
// If a metadata.json file exists then this is not a V1 box
|
|
metadataPath := filepath.Join(dir, "metadata.json")
|
|
if _, err := os.Stat(metadataPath); err == nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Upgrade the V1 box. This will destroy the contents of the old box
|
|
// in order to build the new box. The provider for the new box will
|
|
// be defaulted to be virtualbox.
|
|
func (b *BoxCollection) upgradeV1Box(dir string) (newDir string, err error) {
|
|
newDir, err = ioutil.TempDir(b.basis.dir.TempDir().String(), "box-update")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Move contents of dir into tempDir
|
|
files, err := filepath.Glob(filepath.Join(dir, "*"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, f := range files {
|
|
rel, err := filepath.Rel(dir, f)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if s, _ := os.Stat(f); s.IsDir() {
|
|
err = os.MkdirAll(filepath.Join(newDir, rel), os.ModePerm)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
} else {
|
|
err = os.Rename(f, filepath.Join(newDir, rel))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write the metadata.json file if it does not exist
|
|
metadataFile := filepath.Join(newDir, "metadata.json")
|
|
if _, err := os.Stat(metadataFile); errors.Is(err, os.ErrNotExist) {
|
|
file, _ := json.MarshalIndent(
|
|
map[string]string{"provider": "virtualbox"}, "", " ",
|
|
)
|
|
err = ioutil.WriteFile(metadataFile, file, 0644)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
var _ core.BoxCollection = (*BoxCollection)(nil)
|