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.
555 lines
13 KiB
Go
555 lines
13 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package state
|
|
|
|
// TODO(spox): When dealing with the scopes on the configvar protos,
|
|
// we need to do lookups + fillins to populate parents so we index
|
|
// them correctly in memory and can properly do lookups
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/go-memdb"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
|
|
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type Config struct {
|
|
Model
|
|
|
|
Cid *string `gorm:"uniqueIndex"`
|
|
Name string
|
|
Scope *ProtoValue // TODO(spox): polymorphic needs to allow for runner
|
|
Value string
|
|
}
|
|
|
|
func init() {
|
|
models = append(models, &Config{})
|
|
dbIndexers = append(dbIndexers, (*State).configIndexInit)
|
|
schemas = append(schemas, configIndexSchema)
|
|
}
|
|
|
|
func (c *Config) ToProto() *vagrant_server.ConfigVar {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
|
|
var config vagrant_server.ConfigVar
|
|
if err := decode(c, &config); err != nil {
|
|
panic("failed to decode config: " + err.Error())
|
|
}
|
|
|
|
return &config
|
|
}
|
|
|
|
func (s *State) ConfigFromProto(p *vagrant_server.ConfigVar) (*Config, error) {
|
|
var c Config
|
|
cid := string(s.configVarId(p))
|
|
result := s.db.First(&c, &Config{Cid: &cid})
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
return &c, nil
|
|
}
|
|
|
|
// ConfigSet writes a configuration variable to the data store.
|
|
func (s *State) ConfigSet(vs ...*vagrant_server.ConfigVar) error {
|
|
memTxn := s.inmem.Txn(true)
|
|
defer memTxn.Abort()
|
|
var err error
|
|
|
|
for _, v := range vs {
|
|
if err := s.configSet(memTxn, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err == nil {
|
|
memTxn.Commit()
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// ConfigGet gets all the configuration for the given request.
|
|
func (s *State) ConfigGet(req *vagrant_server.ConfigGetRequest) ([]*vagrant_server.ConfigVar, error) {
|
|
return s.ConfigGetWatch(req, nil)
|
|
}
|
|
|
|
// ConfigGetWatch gets all the configuration for the given request. If a non-nil
|
|
// WatchSet is given, this can be watched for potential changes in the config.
|
|
func (s *State) ConfigGetWatch(req *vagrant_server.ConfigGetRequest, ws memdb.WatchSet) ([]*vagrant_server.ConfigVar, error) {
|
|
memTxn := s.inmem.Txn(false)
|
|
defer memTxn.Abort()
|
|
|
|
return s.configGetMerged(memTxn, ws, req)
|
|
}
|
|
|
|
func (s *State) configSet(
|
|
memTxn *memdb.Txn,
|
|
value *vagrant_server.ConfigVar,
|
|
) error {
|
|
id := s.configVarId(value)
|
|
|
|
// Persist the configuration in the db
|
|
c, err := s.ConfigFromProto(value)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
|
|
if err != nil {
|
|
cid := string(id)
|
|
c = &Config{Cid: &cid}
|
|
}
|
|
|
|
if err = s.softDecode(value, c); err != nil {
|
|
return saveErrorToStatus("config", err)
|
|
}
|
|
|
|
result := s.db.Save(c)
|
|
if result.Error != nil {
|
|
return saveErrorToStatus("config", result.Error)
|
|
}
|
|
|
|
// Create our index value and write that.
|
|
if err = s.configIndexSet(memTxn, id, value); err != nil {
|
|
return saveErrorToStatus("config", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *State) configGetMerged(
|
|
memTxn *memdb.Txn,
|
|
ws memdb.WatchSet,
|
|
req *vagrant_server.ConfigGetRequest,
|
|
) ([]*vagrant_server.ConfigVar, error) {
|
|
var mergeSet [][]*vagrant_server.ConfigVar
|
|
switch scope := req.Scope.(type) {
|
|
case *vagrant_server.ConfigGetRequest_Basis:
|
|
// For basis scope, we just return the basis scoped values
|
|
return s.configGetExact(memTxn, ws, scope.Basis, req.Prefix)
|
|
case *vagrant_server.ConfigGetRequest_Project:
|
|
// For project scope, we collect project and basis values
|
|
m, err := s.configGetExact(memTxn, ws, scope.Project.Basis, req.Prefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mergeSet = append(mergeSet, m)
|
|
m, err = s.configGetExact(memTxn, ws, scope.Project, req.Prefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mergeSet = append(mergeSet, m)
|
|
case *vagrant_server.ConfigGetRequest_Target:
|
|
// For project scope, we collect project and basis values
|
|
m, err := s.configGetExact(memTxn, ws, scope.Target.Project.Basis, req.Prefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mergeSet = append(mergeSet, m)
|
|
m, err = s.configGetExact(memTxn, ws, scope.Target.Project, req.Prefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mergeSet = append(mergeSet, m)
|
|
m, err = s.configGetExact(memTxn, ws, scope.Target, req.Prefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mergeSet = append(mergeSet, m)
|
|
case *vagrant_server.ConfigGetRequest_Runner:
|
|
var err error
|
|
mergeSet, err = s.configGetRunner(memTxn, ws, scope.Runner, req.Prefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown scope type provided (%T)", req.Scope)
|
|
}
|
|
|
|
// Merge our merge set
|
|
merged := make(map[string]*vagrant_server.ConfigVar)
|
|
for _, set := range mergeSet {
|
|
for _, v := range set {
|
|
merged[v.Name] = v
|
|
}
|
|
}
|
|
|
|
result := make([]*vagrant_server.ConfigVar, 0, len(merged))
|
|
for _, v := range merged {
|
|
result = append(result, v)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// configGetExact returns the list of config variables for a scope
|
|
// exactly. By "exactly" we mean without any merging logic: if you request
|
|
// target-scoped variables, you'll get target-scoped variables. If a project-scoped
|
|
// variable matches, it will not be merged in.
|
|
func (s *State) configGetExact(
|
|
memTxn *memdb.Txn,
|
|
ws memdb.WatchSet,
|
|
ref interface{}, // should be one of the *vagrant_plugin_sdk.Ref_ or *vagrant_server.Ref_ values.
|
|
prefix string,
|
|
) ([]*vagrant_server.ConfigVar, error) {
|
|
// We have to get the correct iterator based on the scope. We check the
|
|
// scope and use the proper index to get the iterator here.
|
|
var iter memdb.ResultIterator
|
|
var err error
|
|
switch v := ref.(type) {
|
|
case *vagrant_plugin_sdk.Ref_Basis:
|
|
iter, err = memTxn.Get(
|
|
configIndexTableName,
|
|
configIndexIdIndexName+"_prefix", // Enable a prefix match on lookup
|
|
fmt.Sprintf("%s/%s", v.ResourceId, prefix),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case *vagrant_plugin_sdk.Ref_Project:
|
|
iter, err = memTxn.Get(
|
|
configIndexTableName,
|
|
configIndexIdIndexName+"_prefix", // Enable a prefix match on lookup
|
|
fmt.Sprintf("%s/%s/%s", v.Basis.ResourceId, v.ResourceId, prefix),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case *vagrant_plugin_sdk.Ref_Target:
|
|
iter, err = memTxn.Get(
|
|
configIndexTableName,
|
|
configIndexIdIndexName+"_prefix", // Enable a prefix match on lookup
|
|
fmt.Sprintf("%s/%s/%s/%s",
|
|
v.Project.Basis.ResourceId,
|
|
v.Project.ResourceId,
|
|
v.ResourceId,
|
|
prefix,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown scope type provided (%T)", ref)
|
|
}
|
|
|
|
// Add to our watchset
|
|
ws.Add(iter.WatchCh())
|
|
|
|
// Go through the iterator and accumulate the results
|
|
var result []*vagrant_server.ConfigVar
|
|
|
|
for {
|
|
current := iter.Next()
|
|
if current == nil {
|
|
break
|
|
}
|
|
|
|
var value Config
|
|
record := current.(*configIndexRecord)
|
|
res := s.db.First(&value, &Config{Cid: &record.Id})
|
|
if res.Error != nil {
|
|
return nil, res.Error
|
|
}
|
|
result = append(result, value.ToProto())
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// configGetRunner gets the config vars for a runner.
|
|
func (s *State) configGetRunner(
|
|
memTxn *memdb.Txn,
|
|
ws memdb.WatchSet,
|
|
req *vagrant_server.Ref_RunnerId,
|
|
prefix string,
|
|
) ([][]*vagrant_server.ConfigVar, error) {
|
|
iter, err := memTxn.Get(
|
|
configIndexTableName,
|
|
configIndexRunnerIndexName+"_prefix", // Enable a prefix match on lookup
|
|
true,
|
|
prefix,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add to our watch set
|
|
ws.Add(iter.WatchCh())
|
|
|
|
// Results go into two buckets
|
|
result := make([][]*vagrant_server.ConfigVar, 2)
|
|
const (
|
|
idxAny = 0
|
|
idxId = 1
|
|
)
|
|
|
|
for {
|
|
current := iter.Next()
|
|
if current == nil {
|
|
break
|
|
}
|
|
record := current.(*configIndexRecord)
|
|
|
|
idx := -1
|
|
switch ref := record.RunnerRef.Target.(type) {
|
|
case *vagrant_server.Ref_Runner_Any:
|
|
idx = idxAny
|
|
|
|
case *vagrant_server.Ref_Runner_Id:
|
|
idx = idxId
|
|
|
|
// We need to match this ID
|
|
if ref.Id.Id != req.Id {
|
|
continue
|
|
}
|
|
|
|
default:
|
|
return nil, fmt.Errorf("config has unknown target type: %T", record.RunnerRef.Target)
|
|
}
|
|
|
|
var value Config
|
|
res := s.db.First(&value, &Config{Cid: &record.Id})
|
|
if res.Error != nil {
|
|
return nil, res.Error
|
|
}
|
|
|
|
result[idx] = append(result[idx], value.ToProto())
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// configIndexSet writes an index record for a single config var.
|
|
func (s *State) configIndexSet(txn *memdb.Txn, id []byte, value *vagrant_server.ConfigVar) error {
|
|
var basis, project, target string
|
|
var runner *vagrant_server.Ref_Runner
|
|
switch scope := value.Scope.(type) {
|
|
case *vagrant_server.ConfigVar_Basis:
|
|
basis = scope.Basis.ResourceId
|
|
case *vagrant_server.ConfigVar_Project:
|
|
project = scope.Project.ResourceId
|
|
case *vagrant_server.ConfigVar_Target:
|
|
target = scope.Target.ResourceId
|
|
case *vagrant_server.ConfigVar_Runner:
|
|
runner = scope.Runner
|
|
default:
|
|
panic("unknown scope")
|
|
}
|
|
|
|
record := &configIndexRecord{
|
|
Id: string(id),
|
|
Basis: basis,
|
|
Project: project,
|
|
Target: target,
|
|
Name: value.Name,
|
|
Runner: runner != nil,
|
|
RunnerRef: runner,
|
|
}
|
|
|
|
// If we have no value, we delete from the memdb index
|
|
if value.Value == "" {
|
|
return txn.Delete(configIndexTableName, record)
|
|
}
|
|
|
|
// Insert the index
|
|
return txn.Insert(configIndexTableName, record)
|
|
}
|
|
|
|
// configIndexInit initializes the config index from persisted data.
|
|
func (s *State) configIndexInit(memTxn *memdb.Txn) error {
|
|
var cfgs []Config
|
|
result := s.db.Find(&cfgs)
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
for _, c := range cfgs {
|
|
p := c.ToProto()
|
|
if err := s.configIndexSet(memTxn, s.configVarId(p), p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *State) configVarId(v *vagrant_server.ConfigVar) []byte {
|
|
switch scope := v.Scope.(type) {
|
|
case *vagrant_server.ConfigVar_Basis:
|
|
return []byte(
|
|
fmt.Sprintf("%v/%v",
|
|
scope.Basis.Name,
|
|
v.Name,
|
|
),
|
|
)
|
|
case *vagrant_server.ConfigVar_Project:
|
|
return []byte(
|
|
fmt.Sprintf("%v/%v/%v",
|
|
scope.Project.Basis.ResourceId,
|
|
scope.Project.ResourceId,
|
|
v.Name,
|
|
),
|
|
)
|
|
case *vagrant_server.ConfigVar_Target:
|
|
return []byte(
|
|
fmt.Sprintf("%v/%v/%v/%v",
|
|
scope.Target.Project.Basis.ResourceId,
|
|
scope.Target.Project.ResourceId,
|
|
scope.Target.ResourceId,
|
|
v.Name,
|
|
),
|
|
)
|
|
case *vagrant_server.ConfigVar_Runner:
|
|
var t string
|
|
switch scope.Runner.Target.(type) {
|
|
case *vagrant_server.Ref_Runner_Id:
|
|
t = "by-id"
|
|
|
|
case *vagrant_server.Ref_Runner_Any:
|
|
t = "any"
|
|
|
|
default:
|
|
panic(fmt.Sprintf("unknown runner target scope: %T", scope.Runner.Target))
|
|
}
|
|
|
|
return []byte(fmt.Sprintf("runner/%s/%s", t, v.Name))
|
|
|
|
default:
|
|
panic("unknown scope")
|
|
}
|
|
}
|
|
|
|
func configIndexSchema() *memdb.TableSchema {
|
|
return &memdb.TableSchema{
|
|
Name: configIndexTableName,
|
|
Indexes: map[string]*memdb.IndexSchema{
|
|
configIndexIdIndexName: {
|
|
Name: configIndexIdIndexName,
|
|
AllowMissing: false,
|
|
Unique: true,
|
|
Indexer: &memdb.StringFieldIndex{
|
|
Field: "Id",
|
|
Lowercase: false,
|
|
},
|
|
},
|
|
|
|
configIndexBasisIndexName: {
|
|
Name: configIndexBasisIndexName,
|
|
AllowMissing: true,
|
|
Unique: false,
|
|
Indexer: &memdb.CompoundIndex{
|
|
Indexes: []memdb.Indexer{
|
|
&memdb.StringFieldIndex{
|
|
Field: "Basis",
|
|
Lowercase: true,
|
|
},
|
|
|
|
&memdb.StringFieldIndex{
|
|
Field: "Name",
|
|
Lowercase: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
configIndexProjectIndexName: {
|
|
Name: configIndexProjectIndexName,
|
|
AllowMissing: true,
|
|
Unique: false,
|
|
Indexer: &memdb.CompoundIndex{
|
|
Indexes: []memdb.Indexer{
|
|
&memdb.StringFieldIndex{
|
|
Field: "Basis",
|
|
Lowercase: true,
|
|
},
|
|
|
|
&memdb.StringFieldIndex{
|
|
Field: "Project",
|
|
Lowercase: true,
|
|
},
|
|
|
|
&memdb.StringFieldIndex{
|
|
Field: "Name",
|
|
Lowercase: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
configIndexTargetIndexName: {
|
|
Name: configIndexTargetIndexName,
|
|
AllowMissing: true,
|
|
Unique: false,
|
|
Indexer: &memdb.CompoundIndex{
|
|
Indexes: []memdb.Indexer{
|
|
&memdb.StringFieldIndex{
|
|
Field: "Basis",
|
|
Lowercase: true,
|
|
},
|
|
|
|
&memdb.StringFieldIndex{
|
|
Field: "Project",
|
|
Lowercase: true,
|
|
},
|
|
|
|
&memdb.StringFieldIndex{
|
|
Field: "Target",
|
|
Lowercase: true,
|
|
},
|
|
|
|
&memdb.StringFieldIndex{
|
|
Field: "Name",
|
|
Lowercase: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
configIndexRunnerIndexName: {
|
|
Name: configIndexRunnerIndexName,
|
|
AllowMissing: true,
|
|
Unique: false,
|
|
Indexer: &memdb.CompoundIndex{
|
|
Indexes: []memdb.Indexer{
|
|
&memdb.BoolFieldIndex{
|
|
Field: "Runner",
|
|
},
|
|
|
|
&memdb.StringFieldIndex{
|
|
Field: "Name",
|
|
Lowercase: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
const (
|
|
configIndexTableName = "config-index"
|
|
configIndexIdIndexName = "id"
|
|
configIndexBasisIndexName = "basis"
|
|
configIndexProjectIndexName = "project"
|
|
configIndexTargetIndexName = "target"
|
|
configIndexRunnerIndexName = "runner"
|
|
)
|
|
|
|
type configIndexRecord struct {
|
|
Id string
|
|
Basis string
|
|
Project string
|
|
Target string
|
|
Name string
|
|
Runner bool // true if this is a runner config
|
|
RunnerRef *vagrant_server.Ref_Runner
|
|
}
|