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.
458 lines
9.7 KiB
Go
458 lines
9.7 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, &Project{})
|
|
}
|
|
|
|
type Project struct {
|
|
Model
|
|
|
|
Basis *Basis `gorm:"constraint:OnDelete:SET NULL"`
|
|
BasisID uint `gorm:"uniqueIndex:idx_bname" mapstructure:"-"`
|
|
Vagrantfile *Vagrantfile `gorm:"constraint:OnDelete:SET NULL" mapstructure:"Configuration"`
|
|
VagrantfileID *uint `mapstructure:"-" gorm:"constraint:OnDelete:SET NULL"`
|
|
DataSource *ProtoValue
|
|
Jobs []*InternalJob `gorm:"polymorphic:Scope"`
|
|
Metadata MetadataSet
|
|
Name string `gorm:"uniqueIndex:idx_bname;not null"`
|
|
Path string `gorm:"uniqueIndex:idx_bname;not null"`
|
|
RemoteEnabled bool
|
|
ResourceId string `gorm:"<-:create;uniqueIndex;not null"`
|
|
Targets []*Target
|
|
}
|
|
|
|
func (p *Project) scope() interface{} {
|
|
return p
|
|
}
|
|
|
|
func (p *Project) find(db *gorm.DB) (*Project, error) {
|
|
var project Project
|
|
result := db.Preload(clause.Associations).
|
|
Where(&Project{ResourceId: p.ResourceId}).
|
|
Or(&Project{BasisID: p.BasisID, Name: p.Name}).
|
|
Or(&Project{BasisID: p.BasisID, Path: p.Path}).
|
|
Or(&Project{Model: Model{ID: p.ID}}).
|
|
First(&project)
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
return &project, nil
|
|
}
|
|
|
|
// Use before delete hook to remove all assocations
|
|
func (p *Project) BeforeDelete(tx *gorm.DB) error {
|
|
project, err := p.find(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if project.VagrantfileID != nil {
|
|
result := tx.Where(&Vagrantfile{Model: Model{ID: *project.VagrantfileID}}).
|
|
Delete(&Vagrantfile{})
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
}
|
|
|
|
if len(project.Targets) > 0 {
|
|
if result := tx.Delete(project.Targets); result.Error != nil {
|
|
return result.Error
|
|
}
|
|
}
|
|
|
|
if len(project.Jobs) > 0 {
|
|
if result := tx.Delete(project.Jobs); result.Error != nil {
|
|
return result.Error
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Set a public ID on the project before creating
|
|
func (p *Project) BeforeSave(tx *gorm.DB) error {
|
|
if p.ResourceId == "" {
|
|
if err := p.setId(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := p.Validate(tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Project) BeforeUpdate(tx *gorm.DB) error {
|
|
// If a Vagrantfile was already set for the project, just update it
|
|
if p.Vagrantfile != nil && p.Vagrantfile.ID == 0 && p.VagrantfileID != nil {
|
|
var v Vagrantfile
|
|
result := tx.First(&v, &Vagrantfile{Model: Model{ID: *p.VagrantfileID}})
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
id := v.ID
|
|
if err := decode(p.Vagrantfile, &v); err != nil {
|
|
return err
|
|
}
|
|
v.ID = id
|
|
p.Vagrantfile = &v
|
|
|
|
// NOTE: Just updating the value doesn't save the changes so
|
|
// save the changes in this transaction
|
|
if result := tx.Save(&v); result.Error != nil {
|
|
return result.Error
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Project) Validate(tx *gorm.DB) error {
|
|
basisID := p.BasisID
|
|
if p.Basis != nil {
|
|
basisID = p.Basis.ID
|
|
}
|
|
|
|
err := validation.ValidateStruct(p,
|
|
validation.Field(&p.BasisID,
|
|
validation.Required.When(p.Basis == nil),
|
|
),
|
|
validation.Field(&p.Basis,
|
|
validation.Required.When(p.BasisID == 0),
|
|
),
|
|
validation.Field(&p.Name,
|
|
validation.Required,
|
|
validation.When(
|
|
p.ID != 0,
|
|
validation.By(
|
|
checkUnique(
|
|
tx.Model(&Project{}).
|
|
Where(&Project{Name: p.Name, BasisID: basisID}).
|
|
Not(&Project{Model: Model{ID: p.ID}}),
|
|
),
|
|
),
|
|
),
|
|
validation.When(
|
|
p.ID == 0,
|
|
validation.By(
|
|
checkUnique(
|
|
tx.Model(&Project{}).
|
|
Where(&Project{Name: p.Name, BasisID: basisID}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
validation.Field(&p.Path,
|
|
validation.Required,
|
|
validation.When(
|
|
p.ID != 0,
|
|
validation.By(
|
|
checkUnique(
|
|
tx.Model(&Project{}).
|
|
Where(&Project{Path: p.Path, BasisID: basisID}).
|
|
Not(&Project{Model: Model{ID: p.ID}}),
|
|
),
|
|
),
|
|
),
|
|
validation.When(
|
|
p.ID == 0,
|
|
validation.By(
|
|
checkUnique(
|
|
tx.Model(&Project{}).
|
|
Where(&Project{Path: p.Path, BasisID: basisID}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
validation.Field(&p.ResourceId,
|
|
validation.Required,
|
|
validation.When(
|
|
p.ID == 0,
|
|
validation.By(
|
|
checkUnique(
|
|
tx.Model(&Project{}).
|
|
Where(&Project{ResourceId: p.ResourceId}),
|
|
),
|
|
),
|
|
),
|
|
validation.When(
|
|
p.ID != 0,
|
|
validation.By(
|
|
checkNotModified(
|
|
tx.Statement.Changed("ResourceId"),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Project) setId() error {
|
|
id, err := server.Id()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.ResourceId = id
|
|
|
|
return nil
|
|
}
|
|
|
|
// Convert project to reference protobuf message
|
|
func (p *Project) ToProtoRef() *vagrant_plugin_sdk.Ref_Project {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
|
|
ref := vagrant_plugin_sdk.Ref_Project{}
|
|
err := decode(p, &ref)
|
|
if err != nil {
|
|
panic("failed to decode project to ref: " + err.Error())
|
|
}
|
|
|
|
return &ref
|
|
}
|
|
|
|
// Convert project to protobuf message
|
|
func (p *Project) ToProto() *vagrant_server.Project {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
var project vagrant_server.Project
|
|
|
|
err := decode(p, &project)
|
|
if err != nil {
|
|
panic("failed to decode project: " + err.Error())
|
|
}
|
|
|
|
// Manually include the vagrantfile since we force it to be ignored
|
|
if p.Vagrantfile != nil {
|
|
project.Configuration = p.Vagrantfile.ToProto()
|
|
}
|
|
|
|
return &project
|
|
}
|
|
|
|
// Load a Project from reference protobuf message.
|
|
func (s *State) ProjectFromProtoRef(
|
|
ref *vagrant_plugin_sdk.Ref_Project,
|
|
) (*Project, error) {
|
|
if ref == nil {
|
|
return nil, ErrEmptyProtoArgument
|
|
}
|
|
|
|
if ref.ResourceId == "" {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
|
|
var project Project
|
|
result := s.search().First(&project,
|
|
&Project{ResourceId: ref.ResourceId})
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
return &project, nil
|
|
}
|
|
|
|
func (s *State) ProjectFromProtoRefFuzzy(
|
|
ref *vagrant_plugin_sdk.Ref_Project,
|
|
) (*Project, error) {
|
|
project, err := s.ProjectFromProtoRef(ref)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
if ref.Basis == nil {
|
|
return nil, ErrMissingProtoParent
|
|
}
|
|
|
|
if ref.Name == "" && ref.Path == "" {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
|
|
project = &Project{}
|
|
query := &Project{}
|
|
|
|
if ref.Name != "" {
|
|
query.Name = ref.Name
|
|
}
|
|
if ref.Path != "" {
|
|
query.Path = ref.Path
|
|
}
|
|
|
|
result := s.search().
|
|
Joins("Basis", &Basis{ResourceId: ref.Basis.ResourceId}).
|
|
Where(query).
|
|
First(project)
|
|
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
return project, nil
|
|
}
|
|
|
|
// Load a Project from protobuf message.
|
|
func (s *State) ProjectFromProto(
|
|
p *vagrant_server.Project,
|
|
) (*Project, error) {
|
|
if p == nil {
|
|
return nil, ErrEmptyProtoArgument
|
|
}
|
|
|
|
project, err := s.ProjectFromProtoRef(
|
|
&vagrant_plugin_sdk.Ref_Project{
|
|
ResourceId: p.ResourceId,
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return project, nil
|
|
}
|
|
|
|
func (s *State) ProjectFromProtoFuzzy(
|
|
p *vagrant_server.Project,
|
|
) (*Project, error) {
|
|
if p == nil {
|
|
return nil, ErrEmptyProtoArgument
|
|
}
|
|
|
|
project, err := s.ProjectFromProtoRefFuzzy(
|
|
&vagrant_plugin_sdk.Ref_Project{
|
|
ResourceId: p.ResourceId,
|
|
Basis: p.Basis,
|
|
Name: p.Name,
|
|
Path: p.Path,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return project, nil
|
|
}
|
|
|
|
// Get a project record using a reference protobuf message
|
|
func (s *State) ProjectGet(
|
|
p *vagrant_plugin_sdk.Ref_Project,
|
|
) (*vagrant_server.Project, error) {
|
|
project, err := s.ProjectFromProtoRef(p)
|
|
if err != nil {
|
|
return nil, lookupErrorToStatus("project", err)
|
|
}
|
|
|
|
return project.ToProto(), nil
|
|
}
|
|
|
|
// Store a Project
|
|
func (s *State) ProjectPut(
|
|
p *vagrant_server.Project,
|
|
) (*vagrant_server.Project, error) {
|
|
project, err := s.ProjectFromProto(p)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, lookupErrorToStatus("project", err)
|
|
}
|
|
|
|
// If a project is found, remove the basis
|
|
// ref to prevent update attempts
|
|
if project != nil {
|
|
p.Basis = nil
|
|
} else {
|
|
project = &Project{}
|
|
}
|
|
|
|
err = s.softDecode(p, project)
|
|
if err != nil {
|
|
return nil, saveErrorToStatus("project", err)
|
|
}
|
|
|
|
if p.Configuration != nil && p.Configuration.Finalized == nil {
|
|
project.Vagrantfile.Finalized = nil
|
|
}
|
|
|
|
if err := s.upsertFull(project); err != nil {
|
|
return nil, saveErrorToStatus("project", err)
|
|
}
|
|
|
|
return project.ToProto(), nil
|
|
}
|
|
|
|
// List all project records
|
|
func (s *State) ProjectList() ([]*vagrant_plugin_sdk.Ref_Project, error) {
|
|
var projects []Project
|
|
result := s.search().Find(&projects)
|
|
if result.Error != nil {
|
|
return nil, lookupErrorToStatus("projects", result.Error)
|
|
}
|
|
|
|
prefs := make([]*vagrant_plugin_sdk.Ref_Project, len(projects))
|
|
for i, prj := range projects {
|
|
prefs[i] = prj.ToProtoRef()
|
|
}
|
|
|
|
return prefs, nil
|
|
}
|
|
|
|
// Find a Project using a protobuf message
|
|
func (s *State) ProjectFind(p *vagrant_server.Project) (*vagrant_server.Project, error) {
|
|
project, err := s.ProjectFromProtoFuzzy(p)
|
|
if err != nil {
|
|
return nil, lookupErrorToStatus("project", fmt.Errorf("%w (%#v)", err, p))
|
|
}
|
|
|
|
return project.ToProto(), nil
|
|
}
|
|
|
|
// Delete a project
|
|
func (s *State) ProjectDelete(
|
|
p *vagrant_plugin_sdk.Ref_Project,
|
|
) error {
|
|
project, err := s.ProjectFromProtoRef(p)
|
|
// If the record was not found, we return with no error
|
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil
|
|
}
|
|
|
|
// If an unexpected error was encountered, return it
|
|
if err != nil {
|
|
return deleteErrorToStatus("project", err)
|
|
}
|
|
|
|
result := s.db.Delete(project)
|
|
if result.Error != nil {
|
|
return deleteErrorToStatus("project", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
_ scope = (*Project)(nil)
|
|
)
|