430 lines
9.1 KiB
Go
430 lines
9.1 KiB
Go
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,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 {
|
|
existing, err := p.find(tx)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
|
|
if existing == nil {
|
|
existing = &Project{}
|
|
}
|
|
|
|
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.By(
|
|
checkUnique(
|
|
tx.Model(&Project{}).
|
|
Where(&Project{Name: p.Name, BasisID: basisID}).
|
|
Not(&Project{Model: Model{ID: p.ID}}),
|
|
),
|
|
),
|
|
),
|
|
validation.Field(&p.Path,
|
|
validation.Required,
|
|
validation.By(
|
|
checkUnique(
|
|
tx.Model(&Project{}).
|
|
Where(&Project{Path: p.Path, BasisID: basisID}).
|
|
Not(&Project{Model: Model{ID: p.ID}}),
|
|
),
|
|
),
|
|
),
|
|
validation.Field(&p.ResourceId,
|
|
validation.Required,
|
|
validation.By(
|
|
checkUnique(
|
|
tx.Model(&Project{}).
|
|
Where(&Project{ResourceId: p.ResourceId}).
|
|
Not(&Project{Model: Model{ID: p.ID}}),
|
|
),
|
|
),
|
|
validation.When(
|
|
p.ID != 0,
|
|
validation.By(
|
|
checkNotModified(
|
|
existing.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)
|
|
}
|
|
|
|
// Make sure we don't have a nil value
|
|
if err != nil {
|
|
project = &Project{}
|
|
}
|
|
|
|
err = s.softDecode(p, project)
|
|
if err != nil {
|
|
return nil, saveErrorToStatus("project", err)
|
|
}
|
|
|
|
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)
|
|
)
|