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.
395 lines
8.0 KiB
Go
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
|
|
}
|