vagrant/internal/server/singleprocess/state/project.go

458 lines
9.7 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
2021-04-06 18:03:30 -04:00
package state
import (
2022-09-19 11:40:11 -04:00
"errors"
2022-10-05 20:21:00 -04:00
"fmt"
2021-04-06 18:03:30 -04:00
2022-09-19 11:40:11 -04:00
"github.com/go-ozzo/ozzo-validation/v4"
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
2022-09-19 11:40:11 -04:00
"github.com/hashicorp/vagrant/internal/server"
2021-04-06 18:03:30 -04:00
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
2022-09-19 11:40:11 -04:00
"gorm.io/gorm"
"gorm.io/gorm/clause"
2021-04-06 18:03:30 -04:00
)
func init() {
2022-09-19 11:40:11 -04:00
models = append(models, &Project{})
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
type Project struct {
2022-09-28 11:38:34 -04:00
Model
2022-09-19 11:40:11 -04:00
Basis *Basis `gorm:"constraint:OnDelete:SET NULL"`
2022-09-28 11:38:34 -04:00
BasisID uint `gorm:"uniqueIndex:idx_bname" mapstructure:"-"`
Vagrantfile *Vagrantfile `gorm:"constraint:OnDelete:SET NULL" mapstructure:"Configuration"`
VagrantfileID *uint `mapstructure:"-" gorm:"constraint:OnDelete:SET NULL"`
2022-09-19 11:40:11 -04:00
DataSource *ProtoValue
2022-09-28 11:38:34 -04:00
Jobs []*InternalJob `gorm:"polymorphic:Scope"`
2022-09-19 11:40:11 -04:00
Metadata MetadataSet
Name string `gorm:"uniqueIndex:idx_bname;not null"`
Path string `gorm:"uniqueIndex:idx_bname;not null"`
2022-09-19 11:40:11 -04:00
RemoteEnabled bool
ResourceId string `gorm:"<-:create;uniqueIndex;not null"`
Targets []*Target
2022-09-19 11:40:11 -04:00
}
2021-04-06 18:03:30 -04:00
2022-09-19 11:40:11 -04:00
func (p *Project) scope() interface{} {
return p
}
2021-04-06 18:03:30 -04:00
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
}
2022-09-19 11:40:11 -04:00
// Set a public ID on the project before creating
func (p *Project) BeforeSave(tx *gorm.DB) error {
if p.ResourceId == "" {
2022-09-19 11:40:11 -04:00
if err := p.setId(); err != nil {
return err
}
}
2022-09-19 11:40:11 -04:00
if err := p.Validate(tx); err != nil {
return err
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
return nil
2021-04-06 18:03:30 -04:00
}
2022-09-28 11:38:34 -04:00
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 {
2022-09-28 11:38:34 -04:00
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
}
2022-09-28 11:38:34 -04:00
}
return nil
}
2022-09-19 11:40:11 -04:00
func (p *Project) Validate(tx *gorm.DB) error {
basisID := p.BasisID
if p.Basis != nil {
basisID = p.Basis.ID
}
err := validation.ValidateStruct(p,
2022-09-28 11:38:34 -04:00
validation.Field(&p.BasisID,
validation.Required.When(p.Basis == nil),
),
validation.Field(&p.Basis,
validation.Required.When(p.BasisID == 0),
),
2022-09-19 11:40:11 -04:00
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}),
),
2022-09-19 11:40:11 -04:00
),
),
),
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}),
),
2022-09-19 11:40:11 -04:00
),
),
),
validation.Field(&p.ResourceId,
validation.Required,
validation.When(
p.ID == 0,
validation.By(
checkUnique(
tx.Model(&Project{}).
Where(&Project{ResourceId: p.ResourceId}),
),
2022-09-19 11:40:11 -04:00
),
),
validation.When(
p.ID != 0,
validation.By(
checkNotModified(
tx.Statement.Changed("ResourceId"),
),
),
),
2022-09-19 11:40:11 -04:00
),
)
2021-04-06 18:03:30 -04:00
2022-09-19 11:40:11 -04:00
if err != nil {
2021-04-06 18:03:30 -04:00
return err
2022-09-19 11:40:11 -04:00
}
2021-04-06 18:03:30 -04:00
2022-09-19 11:40:11 -04:00
return nil
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
func (p *Project) setId() error {
id, err := server.Id()
if err != nil {
2021-04-06 18:03:30 -04:00
return err
2022-09-19 11:40:11 -04:00
}
p.ResourceId = id
2021-04-06 18:03:30 -04:00
2022-09-19 11:40:11 -04:00
return nil
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
// Convert project to reference protobuf message
func (p *Project) ToProtoRef() *vagrant_plugin_sdk.Ref_Project {
if p == nil {
return nil
}
2021-04-06 18:03:30 -04:00
2022-09-19 11:40:11 -04:00
ref := vagrant_plugin_sdk.Ref_Project{}
err := decode(p, &ref)
if err != nil {
panic("failed to decode project to ref: " + err.Error())
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
return &ref
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
// Convert project to protobuf message
func (p *Project) ToProto() *vagrant_server.Project {
if p == nil {
return nil
}
var project vagrant_server.Project
2021-04-06 18:03:30 -04:00
2022-09-19 11:40:11 -04:00
err := decode(p, &project)
if err != nil {
panic("failed to decode project: " + err.Error())
}
2021-04-06 18:03:30 -04:00
2022-09-19 11:40:11 -04:00
// Manually include the vagrantfile since we force it to be ignored
if p.Vagrantfile != nil {
project.Configuration = p.Vagrantfile.ToProto()
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
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
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
if ref.ResourceId == "" {
return nil, gorm.ErrRecordNotFound
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
var project Project
result := s.search().First(&project,
&Project{ResourceId: ref.ResourceId})
2022-09-19 11:40:11 -04:00
if result.Error != nil {
return nil, result.Error
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
return &project, nil
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
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
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
if ref.Basis == nil {
return nil, ErrMissingProtoParent
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
if ref.Name == "" && ref.Path == "" {
return nil, gorm.ErrRecordNotFound
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
project = &Project{}
query := &Project{}
if ref.Name != "" {
query.Name = ref.Name
2022-09-19 11:40:11 -04:00
}
if ref.Path != "" {
query.Path = ref.Path
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
result := s.search().
Joins("Basis", &Basis{ResourceId: ref.Basis.ResourceId}).
2022-09-19 11:40:11 -04:00
Where(query).
First(project)
if result.Error != nil {
return nil, result.Error
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
return project, nil
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
// 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,
},
)
2021-04-06 18:03:30 -04:00
if err != nil {
return nil, err
}
2022-09-19 11:40:11 -04:00
return project, nil
}
2021-04-06 18:03:30 -04:00
2022-09-19 11:40:11 -04:00
func (s *State) ProjectFromProtoFuzzy(
p *vagrant_server.Project,
) (*Project, error) {
if p == nil {
return nil, ErrEmptyProtoArgument
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
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
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
// 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)
2021-04-06 18:03:30 -04:00
if err != nil {
2022-09-19 11:40:11 -04:00
return nil, lookupErrorToStatus("project", err)
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
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)
2021-04-06 18:03:30 -04:00
}
// If a project is found, remove the basis
// ref to prevent update attempts
if project != nil {
p.Basis = nil
} else {
2022-09-19 11:40:11 -04:00
project = &Project{}
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
err = s.softDecode(p, project)
if err != nil {
return nil, saveErrorToStatus("project", err)
2021-04-06 18:03:30 -04:00
}
if p.Configuration != nil && p.Configuration.Finalized == nil {
project.Vagrantfile.Finalized = nil
}
2022-09-28 11:38:34 -04:00
if err := s.upsertFull(project); err != nil {
return nil, saveErrorToStatus("project", err)
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
return project.ToProto(), nil
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
// 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)
}
2021-04-06 18:03:30 -04:00
2022-09-19 11:40:11 -04:00
prefs := make([]*vagrant_plugin_sdk.Ref_Project, len(projects))
for i, prj := range projects {
prefs[i] = prj.ToProtoRef()
}
2021-04-06 18:03:30 -04:00
2022-09-19 11:40:11 -04:00
return prefs, nil
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
// 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 {
2022-10-05 20:21:00 -04:00
return nil, lookupErrorToStatus("project", fmt.Errorf("%w (%#v)", err, p))
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
return project.ToProto(), nil
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
// 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
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
// If an unexpected error was encountered, return it
if err != nil {
return deleteErrorToStatus("project", err)
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
result := s.db.Delete(project)
if result.Error != nil {
return deleteErrorToStatus("project", err)
}
2022-09-19 11:40:11 -04:00
return nil
2021-04-06 18:03:30 -04:00
}
2022-09-19 11:40:11 -04:00
var (
_ scope = (*Project)(nil)
)