Chris Roberts e958c6183a Adds initial HCP config support
Adds initial basic support for HCP based configuration in vagrant-go.
The initalization process has been updated to remove Vagrantfile parsing
from the client, moving it to the runner using init jobs for the basis
and the project (if there is one). Detection is done on the file based
on extension for Ruby based parsing or HCP based parsing.

Current HCP parsing is extremely simple and currently just a base to
build off. Config components will be able to implement an `Init`
function to handle receiving configuration data from a non-native source
file. This will be extended to include a default approach for injecting
defined data in the future.

Some cleanup was done in the state around validations. Some logging
adjustments were applied on the Ruby side for better behavior
consistency.

VirtualBox provider now caches locale detection to prevent multiple
checks every time the driver is initialized.
2023-09-07 17:26:10 -07:00

395 lines
8.0 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package state
import (
"errors"
"fmt"
"time"
"github.com/go-ozzo/ozzo-validation/v4"
"github.com/hashicorp/go-version"
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
"github.com/hashicorp/vagrant/internal/server"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
"github.com/mitchellh/mapstructure"
"gorm.io/gorm"
)
func init() {
models = append(models, &Box{})
}
const (
DEFAULT_BOX_VERSION = "0.0.0"
DEFAULT_BOX_CONSTRAINT = "> 0"
)
type Box struct {
Model
Directory string `gorm:"uniqueIndex"`
LastUpdate time.Time `gorm:"autoUpdateTime"`
Metadata *ProtoValue
MetadataUrl *string
Name string `gorm:"uniqueIndex:idx_nameverprov"`
Provider string `gorm:"uniqueIndex:idx_nameverprov"`
ResourceId string `gorm:"uniqueIndex"`
Version string `gorm:"uniqueIndex:idx_nameverprov"`
}
func (b *Box) find(db *gorm.DB) (*Box, error) {
var box Box
result := db.Where(&Box{ResourceId: b.ResourceId}).
Or(&Box{Directory: b.Directory}).
Or(&Box{Name: b.Name, Provider: b.Provider, Version: b.Version}).
First(&box)
if result.Error != nil {
return nil, result.Error
}
return &box, nil
}
func (b *Box) BeforeSave(tx *gorm.DB) error {
if b.ResourceId == "" {
if err := b.setId(); err != nil {
return err
}
}
// If version is not set, default it to 0
if b.Version == "" || b.Version == "0" {
b.Version = DEFAULT_BOX_VERSION
}
if err := b.Validate(tx); err != nil {
return err
}
return nil
}
func (b *Box) setId() error {
id, err := server.Id()
if err != nil {
return err
}
b.ResourceId = id
return nil
}
func (b *Box) Validate(tx *gorm.DB) error {
err := validation.ValidateStruct(b,
validation.Field(&b.Directory,
validation.Required,
validation.By(
checkUnique(
tx.Model((*Box)(nil)).
Where(&Box{Directory: b.Directory}).
Not(&Box{Model: Model{ID: b.ID}}),
),
),
),
validation.Field(&b.Name, validation.Required),
validation.Field(&b.Provider, validation.Required),
validation.Field(&b.ResourceId,
validation.Required,
validation.By(
checkUnique(
tx.Model(&Box{}).
Where(&Box{ResourceId: b.ResourceId}).
Not(&Box{Model: Model{ID: b.ID}}),
),
),
validation.When(
b.ID != 0,
validation.By(
checkNotModified(
tx.Statement.Changed("ResourceId"),
),
),
),
),
validation.Field(&b.Version,
validation.Required,
validation.By(checkValidVersion),
),
)
if err != nil {
return err
}
err = validation.Validate(b,
validation.By(
checkUnique(
tx.Model(&Box{}).
Where(&Box{Name: b.Name, Provider: b.Provider, Version: b.Version}).
Not(&Box{Model: Model{ID: b.ID}}),
),
),
)
if err != nil {
return fmt.Errorf("name, provider and version %s", err)
}
return nil
}
func (b *Box) ToProto() *vagrant_server.Box {
var p vagrant_server.Box
err := decode(b, &p)
if err != nil {
panic(fmt.Sprintf("failed to decode box: " + err.Error()))
}
return &p
}
func (b *Box) ToProtoRef() *vagrant_plugin_sdk.Ref_Box {
var p vagrant_plugin_sdk.Ref_Box
err := decode(b, &p)
if err != nil {
panic(fmt.Sprintf("failed to decode box ref: " + err.Error()))
}
return &p
}
func (s *State) BoxFromProtoRef(
b *vagrant_plugin_sdk.Ref_Box,
) (*Box, error) {
if b == nil {
return nil, ErrEmptyProtoArgument
}
if b.ResourceId == "" {
return nil, gorm.ErrRecordNotFound
}
var box Box
result := s.search().First(&box, &Box{ResourceId: b.ResourceId})
if result.Error != nil {
return nil, result.Error
}
return &box, nil
}
func (s *State) BoxFromProtoRefFuzzy(
b *vagrant_plugin_sdk.Ref_Box,
) (*Box, error) {
box, err := s.BoxFromProtoRef(b)
if err == nil {
return box, nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
if b.Name == "" || b.Provider == "" || b.Version == "" {
return nil, gorm.ErrRecordNotFound
}
box = &Box{}
result := s.search().First(box,
&Box{
Name: b.Name,
Provider: b.Provider,
Version: b.Version,
},
)
if result.Error != nil {
return nil, result.Error
}
return box, nil
}
func (s *State) BoxFromProto(
b *vagrant_server.Box,
) (*Box, error) {
return s.BoxFromProtoRef(
&vagrant_plugin_sdk.Ref_Box{
ResourceId: b.ResourceId,
},
)
}
func (s *State) BoxFromProtoFuzzy(
b *vagrant_server.Box,
) (*Box, error) {
return s.BoxFromProtoRefFuzzy(
&vagrant_plugin_sdk.Ref_Box{
Name: b.Name,
Provider: b.Provider,
ResourceId: b.ResourceId,
Version: b.Version,
},
)
}
func (s *State) BoxList() ([]*vagrant_plugin_sdk.Ref_Box, error) {
var boxes []Box
result := s.db.Find(&boxes)
if result.Error != nil {
return nil, lookupErrorToStatus("boxes", result.Error)
}
refs := make([]*vagrant_plugin_sdk.Ref_Box, len(boxes))
for i, b := range boxes {
refs[i] = b.ToProtoRef()
}
return refs, nil
}
func (s *State) BoxDelete(
b *vagrant_plugin_sdk.Ref_Box,
) error {
box, err := s.BoxFromProtoRef(b)
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return deleteErrorToStatus("box", err)
}
result := s.db.Delete(box)
if result.Error != nil {
return deleteErrorToStatus("box", result.Error)
}
return nil
}
func (s *State) BoxGet(
b *vagrant_plugin_sdk.Ref_Box,
) (*vagrant_server.Box, error) {
box, err := s.BoxFromProtoRef(b)
if err != nil {
return nil, lookupErrorToStatus("box", err)
}
return box.ToProto(), nil
}
func (s *State) BoxPut(b *vagrant_server.Box) error {
box, err := s.BoxFromProto(b)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return lookupErrorToStatus("box", err)
}
if err != nil {
box = &Box{}
}
err = s.softDecode(b, box)
if err != nil {
return saveErrorToStatus("box", err)
}
if err := s.upsertFull(box); err != nil {
return saveErrorToStatus("box", err)
}
return nil
}
func (s *State) BoxFind(
ref *vagrant_plugin_sdk.Ref_Box,
) (*vagrant_server.Box, error) {
b := &vagrant_plugin_sdk.Ref_Box{}
if err := mapstructure.Decode(ref, b); err != nil {
return nil, lookupErrorToStatus("box", err)
}
if b.ResourceId != "" {
box, err := s.BoxFromProtoRef(b)
if err != nil {
return nil, lookupErrorToStatus("box", err)
}
return box.ToProto(), nil
}
// If no name is given, we error immediately
if b.Name == "" {
return nil, lookupErrorToStatus("box", fmt.Errorf("no name given for box lookup"))
}
// If the version is set to 0, mark it as default
if b.Version == "0" {
b.Version = DEFAULT_BOX_VERSION
}
// If we are provided an explicit version, just do a direct lookup
if _, err := version.NewVersion(b.Version); err == nil && b.Provider != "" {
box, err := s.BoxFromProtoRefFuzzy(b)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, lookupErrorToStatus("box", err)
}
return box.ToProto(), nil
}
var boxes []Box
q := &Box{Name: b.Name}
if b.Provider != "" {
q.Provider = b.Provider
}
result := s.search().Find(&boxes, q)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, lookupErrorToStatus("box", result.Error)
}
// If we found no boxes, return nil result with no error
if len(boxes) < 1 {
return nil, nil
}
// If we have no version value set, apply the default
// version constraint
if b.Version == "" {
b.Version = DEFAULT_BOX_CONSTRAINT
}
var match *Box
highestVersion, _ := version.NewVersion("0.0.0")
versionConstraint, err := version.NewConstraint(b.Version)
if err != nil {
return nil, lookupErrorToStatus("box", err)
}
for _, box := range boxes {
boxVersion, err := version.NewVersion(box.Version)
if err != nil {
return nil, lookupErrorToStatus("box", err)
}
if !versionConstraint.Check(boxVersion) {
continue
}
if boxVersion.GreaterThan(highestVersion) {
match = &box
highestVersion = boxVersion
}
}
if match != nil {
return match.ToProto(), nil
}
// If nothing was found, return a nil
// result and no error
return nil, nil
}