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.
474 lines
9.9 KiB
Go
474 lines
9.9 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package state
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/go-ozzo/ozzo-validation/v4"
|
|
"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"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
func init() {
|
|
models = append(models, &Target{})
|
|
}
|
|
|
|
type Target struct {
|
|
Model
|
|
|
|
Configuration *ProtoValue
|
|
Jobs []*InternalJob `gorm:"polymorphic:Scope;" mapstructure:"-"`
|
|
Metadata MetadataSet
|
|
Name string `gorm:"uniqueIndex:idx_pname;not null"`
|
|
Parent *Target `gorm:"foreignkey:ID"`
|
|
ParentID *uint `mapstructure:"-"`
|
|
Project *Project
|
|
ProjectID uint `gorm:"uniqueIndex:idx_pname" mapstructure:"-"`
|
|
Provider *string
|
|
Record *ProtoValue
|
|
ResourceId string `gorm:"<-:create;uniqueIndex;not null"`
|
|
State vagrant_server.Operation_PhysicalState
|
|
Subtargets []*Target `gorm:"foreignkey:ParentID;constraint:OnDelete:SET NULL"`
|
|
Uuid *string `gorm:"uniqueIndex"`
|
|
}
|
|
|
|
func (t *Target) find(db *gorm.DB) (*Target, error) {
|
|
var target Target
|
|
result := db.Preload(clause.Associations).
|
|
Where(&Target{ResourceId: t.ResourceId}).
|
|
Or(&Target{Uuid: t.Uuid}).
|
|
Or(&Target{ProjectID: t.ProjectID, Name: t.Name}).
|
|
Or(&Target{Model: Model{ID: t.ID}}).
|
|
First(&target)
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
return &target, nil
|
|
}
|
|
|
|
func (t *Target) scope() interface{} {
|
|
return t
|
|
}
|
|
|
|
// Use before delete hook to remove all associations
|
|
func (t *Target) BeforeDelete(tx *gorm.DB) error {
|
|
target, err := t.find(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(target.Subtargets) > 0 {
|
|
if result := tx.Delete(target.Subtargets); result.Error != nil {
|
|
return result.Error
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Set a public ID on the target before creating
|
|
func (t *Target) BeforeSave(tx *gorm.DB) error {
|
|
if t.ResourceId == "" {
|
|
if err := t.setId(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := t.validate(tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NOTE: Need better validation on parent <-> subtarget
|
|
// project matching. It currently does basic check but
|
|
// will miss edge cases easily.
|
|
func (t *Target) validate(tx *gorm.DB) error {
|
|
projectID := t.ProjectID
|
|
if t.Project != nil {
|
|
projectID = t.Project.ID
|
|
}
|
|
|
|
parent := &Target{}
|
|
parentProjectID := uint(0)
|
|
if t.Parent != nil {
|
|
parent = t.Parent
|
|
} else if t.ParentID != nil {
|
|
result := tx.First(parent, &Target{Model: Model{ID: *t.ParentID}})
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
}
|
|
if parent != nil {
|
|
parentProjectID = parent.ProjectID
|
|
if parent.Project != nil {
|
|
parentProjectID = parent.Project.ID
|
|
}
|
|
}
|
|
|
|
err := validation.ValidateStruct(t,
|
|
validation.Field(&t.Name,
|
|
validation.Required,
|
|
validation.When(
|
|
t.ID != 0,
|
|
validation.By(
|
|
checkUnique(
|
|
tx.Model(&Target{}).
|
|
Where(&Target{Name: t.Name, ProjectID: projectID}).
|
|
Not(&Target{Model: Model{ID: t.ID}}),
|
|
),
|
|
),
|
|
),
|
|
validation.When(
|
|
t.ID == 0,
|
|
validation.By(
|
|
checkUnique(
|
|
tx.Model(&Target{}).
|
|
Where(&Target{Name: t.Name, ProjectID: projectID}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
validation.Field(&t.ResourceId,
|
|
validation.Required,
|
|
validation.When(
|
|
t.ID == 0,
|
|
validation.By(
|
|
checkUnique(
|
|
tx.Model(&Target{}).
|
|
Where(&Target{ResourceId: t.ResourceId}),
|
|
),
|
|
),
|
|
),
|
|
validation.When(
|
|
t.ID != 0,
|
|
validation.By(
|
|
checkNotModified(
|
|
tx.Statement.Changed("ResourceId"),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
validation.Field(&t.Uuid,
|
|
validation.When(t.Uuid != nil && t.ID != 0,
|
|
validation.By(
|
|
checkUnique(
|
|
tx.Model(&Target{}).
|
|
Where(&Target{Uuid: t.Uuid}).
|
|
Not(&Target{Model: Model{ID: t.ID}}),
|
|
),
|
|
),
|
|
),
|
|
validation.When(t.Uuid != nil && t.ID == 0,
|
|
validation.By(
|
|
checkUnique(
|
|
tx.Model(&Target{}).
|
|
Where(&Target{Uuid: t.Uuid}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
validation.Field(&t.ProjectID,
|
|
validation.Required.When(t.Project == nil),
|
|
validation.When(
|
|
t.ProjectID != 0 && parentProjectID != 0,
|
|
validation.By(
|
|
checkSameProject(parentProjectID),
|
|
),
|
|
),
|
|
),
|
|
validation.Field(&t.Project,
|
|
validation.Required.When(t.ProjectID == 0),
|
|
validation.When(
|
|
t.Project != nil && parentProjectID != 0,
|
|
validation.By(
|
|
checkSameProject(parentProjectID),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *Target) setId() error {
|
|
id, err := server.Id()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.ResourceId = id
|
|
|
|
return nil
|
|
}
|
|
|
|
// Convert target to reference protobuf message
|
|
func (t *Target) ToProtoRef() *vagrant_plugin_sdk.Ref_Target {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
|
|
var ref vagrant_plugin_sdk.Ref_Target
|
|
|
|
err := decode(t, &ref)
|
|
if err != nil {
|
|
panic("failed to decode target to ref: " + err.Error())
|
|
}
|
|
|
|
return &ref
|
|
}
|
|
|
|
// Convert target to protobuf message
|
|
func (t *Target) ToProto() *vagrant_server.Target {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
|
|
var target vagrant_server.Target
|
|
|
|
err := decode(t, &target)
|
|
if err != nil {
|
|
panic("failed to decode target: " + err.Error())
|
|
}
|
|
|
|
return &target
|
|
}
|
|
|
|
// Load a Target from reference protobuf message
|
|
func (s *State) TargetFromProtoRef(
|
|
ref *vagrant_plugin_sdk.Ref_Target,
|
|
) (*Target, error) {
|
|
if ref == nil {
|
|
return nil, ErrEmptyProtoArgument
|
|
}
|
|
|
|
if ref.ResourceId == "" {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
|
|
var target Target
|
|
result := s.search().Preload("Project.Basis").First(&target,
|
|
&Target{ResourceId: ref.ResourceId},
|
|
)
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
return &target, nil
|
|
}
|
|
|
|
func (s *State) TargetFromProtoRefFuzzy(
|
|
ref *vagrant_plugin_sdk.Ref_Target,
|
|
) (*Target, error) {
|
|
target, err := s.TargetFromProtoRef(ref)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
if ref.Project == nil {
|
|
return nil, ErrMissingProtoParent
|
|
}
|
|
|
|
if ref.Name == "" {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
|
|
target = &Target{}
|
|
result := s.search().
|
|
Joins("Project", &Project{ResourceId: ref.Project.ResourceId}).
|
|
Preload("Project.Basis").
|
|
First(target, &Target{Name: ref.Name})
|
|
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
return target, nil
|
|
}
|
|
|
|
// Load a Target from protobuf message
|
|
func (s *State) TargetFromProto(
|
|
t *vagrant_server.Target,
|
|
) (*Target, error) {
|
|
target, err := s.TargetFromProtoRef(
|
|
&vagrant_plugin_sdk.Ref_Target{
|
|
ResourceId: t.ResourceId,
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return target, nil
|
|
}
|
|
|
|
func (s *State) TargetFromProtoFuzzy(
|
|
t *vagrant_server.Target,
|
|
) (*Target, error) {
|
|
target, err := s.TargetFromProto(t)
|
|
if err == nil {
|
|
return target, nil
|
|
}
|
|
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
if t.Uuid == "" && t.Name == "" {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
|
|
if t.Project == nil && t.Uuid == "" {
|
|
return nil, ErrMissingProtoParent
|
|
}
|
|
|
|
target = &Target{}
|
|
if t.Project != nil {
|
|
tx := s.search().
|
|
Joins("Project").
|
|
Preload("Project.Basis").
|
|
Where("Project.resource_id = ?", t.Project.ResourceId)
|
|
|
|
result := tx.First(target, &Target{Name: t.Name})
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
return target, nil
|
|
}
|
|
|
|
tx := s.search().Preload("Project.Basis").
|
|
Where("uuid LIKE ?", fmt.Sprintf("%%%s%%", t.Uuid))
|
|
|
|
result := tx.First(target)
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
return target, nil
|
|
}
|
|
|
|
// Get a target record using a reference protobuf message
|
|
func (s *State) TargetGet(
|
|
ref *vagrant_plugin_sdk.Ref_Target,
|
|
) (*vagrant_server.Target, error) {
|
|
t, err := s.TargetFromProtoRef(ref)
|
|
if err != nil {
|
|
return nil, lookupErrorToStatus("target", err)
|
|
}
|
|
|
|
return t.ToProto(), nil
|
|
}
|
|
|
|
// List all target records
|
|
func (s *State) TargetList() ([]*vagrant_plugin_sdk.Ref_Target, error) {
|
|
var targets []Target
|
|
result := s.search().Find(&targets)
|
|
if result.Error != nil {
|
|
return nil, lookupErrorToStatus("targets", result.Error)
|
|
}
|
|
|
|
trefs := make([]*vagrant_plugin_sdk.Ref_Target, len(targets))
|
|
for i, t := range targets {
|
|
trefs[i] = t.ToProtoRef()
|
|
}
|
|
|
|
return trefs, nil
|
|
}
|
|
|
|
// Delete a target by reference protobuf message
|
|
func (s *State) TargetDelete(
|
|
t *vagrant_plugin_sdk.Ref_Target,
|
|
) error {
|
|
target, err := s.TargetFromProtoRef(t)
|
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil
|
|
}
|
|
|
|
if err != nil {
|
|
return lookupErrorToStatus("target", err)
|
|
}
|
|
|
|
result := s.db.Delete(target)
|
|
if result.Error != nil {
|
|
return deleteErrorToStatus("target", result.Error)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Store a Target
|
|
func (s *State) TargetPut(
|
|
t *vagrant_server.Target,
|
|
) (*vagrant_server.Target, error) {
|
|
target, err := s.TargetFromProtoFuzzy(t)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, lookupErrorToStatus("target", err)
|
|
}
|
|
|
|
// If a target is found, remove the project
|
|
// ref to prevent update attempts
|
|
if target != nil {
|
|
t.Project = nil
|
|
} else {
|
|
// Otherwise, init target for the decode
|
|
target = &Target{}
|
|
}
|
|
|
|
// TODO(spox): forcing the record to be updated here
|
|
// but the soft decoding should be handling it properly
|
|
if t.Record != nil {
|
|
target.Record = nil
|
|
}
|
|
|
|
err = s.softDecode(t, target)
|
|
if err != nil {
|
|
return nil, saveErrorToStatus("target", err)
|
|
}
|
|
|
|
if target.Project == nil {
|
|
return nil, saveErrorToStatus("target", ErrMissingProtoParent)
|
|
}
|
|
|
|
// NOTE: this does not get set when updating to
|
|
// the UNKNOWN value since it's a zero
|
|
// value and thus ignored with soft decode
|
|
target.State = t.State
|
|
|
|
if err := s.upsertFull(target); err != nil {
|
|
return nil, saveErrorToStatus("target", err)
|
|
}
|
|
|
|
s.log.Info("target has been upserted", "record", target.Record, "original-record", t.Record)
|
|
|
|
return target.ToProto(), nil
|
|
}
|
|
|
|
// Find a Target
|
|
func (s *State) TargetFind(
|
|
t *vagrant_server.Target,
|
|
) (*vagrant_server.Target, error) {
|
|
target, err := s.TargetFromProtoFuzzy(t)
|
|
if err != nil {
|
|
return nil, lookupErrorToStatus("target", err)
|
|
}
|
|
|
|
return target.ToProto(), nil
|
|
}
|
|
|
|
var (
|
|
_ scope = (*Target)(nil)
|
|
)
|