Chris Roberts e958c6183a Adds initial HCP config support
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.
2023-09-07 17:26:10 -07:00

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
}