Files
Packer-Cn/hcl2template/types.datasource.go
T
Lucas Bajolet 4eae2df2f1 hcl2template: break down datasource execution
Since we want to allow for independently executing datasources when
needed, we break down the logic to do so.

Each datasource will now hold a list of dependencies. For now, these
dependencies are all datasources themselves, but this may change in the
future.

Then, actual execution of the datasource is self-contained within a
method of the structure, so that we can individually execute them.

Finally, we replace the current sequential execution by a method of the
PackerConfig with an approach akin to local variable evaluation, where
we execute all the datasources one-by-one, and if they cannot be
executed, move along.
We retry this loop for as many times as we don't reach either a state
where all the datasources have been executed; or when we reach a
terminal state where datasources cannot be evaluated because of
dependencies remaining, and no changes have occurred after a try.
2023-10-11 14:28:27 -04:00

294 lines
7.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package hcl2template
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
hcl2shim "github.com/hashicorp/packer/hcl2template/shim"
"github.com/hashicorp/packer/packer"
"github.com/zclconf/go-cty/cty"
)
// DatasourceBlock references an HCL 'data' block.
type DatasourceBlock struct {
Type string
Name string
value cty.Value
block *hcl.Block
// dependencies is the list of datasources to execute before this one
dependencies []DatasourceRef
}
type DatasourceRef struct {
Type string
Name string
}
type Datasources map[DatasourceRef]*DatasourceBlock
func (data *DatasourceBlock) Ref() DatasourceRef {
return DatasourceRef{
Type: data.Type,
Name: data.Name,
}
}
func (ds *DatasourceBlock) getDependencies() {
var dependencies []DatasourceRef
// Note: when looking at the expressions, we only need to care about
// attributes, as HCL2 expressions are not allowed in a block's labels.
vars := GetVarsByType(ds.block, "data")
for _, v := range vars {
// construct, backwards, the data source type and name we
// need to evaluate before this one can be evaluated.
dependencies = append(dependencies, DatasourceRef{
Type: v[1].(hcl.TraverseAttr).Name,
Name: v[2].(hcl.TraverseAttr).Name,
})
}
ds.dependencies = dependencies
}
const notReadyDataSourceError = "Dependencies not ready"
// executed returns whether or not the datasource was executed
//
// Having a non-empty cty.Value object means this was filled-up after the
// datasource has been executed, so this is what we use for this test.
func (ds DatasourceBlock) executed() bool {
return ds.value != cty.Value{}
}
// Execute starts the datasource and executes it immediately.
//
// If its dependencies are not ready for execution, this will return an error
// and will only execute when all its dependencies have executed.
func (ds *DatasourceBlock) Execute(cfg *PackerConfig, skipExecution bool) hcl.Diagnostics {
var diags hcl.Diagnostics
ok := true
for _, depRef := range ds.dependencies {
dep := cfg.Datasources[depRef]
if dep == nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Nonexistent dependency referenced",
Detail: fmt.Sprintf("The referenced datasource %s.%s is not defined in the configuration, so this datasource won't be able to execute.", dep.Type, dep.Name),
Subject: &ds.block.DefRange,
})
ok = false
continue
}
if !dep.executed() {
ok = false
}
}
if !ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: notReadyDataSourceError,
Detail: "At least one dependency for the datasource is not executed already",
Subject: &ds.block.DefRange,
})
return diags
}
// If we've gotten here, then it means ref doesn't seem to have any further
// dependencies we need to evaluate first. Evaluate it, with the cfg's full
// data source context.
datasource, diags := cfg.startDatasource(*ds)
if diags.HasErrors() {
return diags
}
if skipExecution {
placeholderValue := cty.UnknownVal(hcldec.ImpliedType(datasource.OutputSpec()))
ds.value = placeholderValue
return diags
}
opts, _ := decodeHCL2Spec(ds.block.Body, cfg.EvalContext(DatasourceContext, nil), datasource)
sp := packer.CheckpointReporter.AddSpan(ds.Type, "datasource", opts)
realValue, err := datasource.Execute()
sp.End(err)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Summary: err.Error(),
Subject: &ds.block.DefRange,
Severity: hcl.DiagError,
})
return diags
}
ds.value = realValue
return diags
}
func (ds *Datasources) Values() (map[string]cty.Value, hcl.Diagnostics) {
var diags hcl.Diagnostics
res := map[string]cty.Value{}
valuesMap := map[string]map[string]cty.Value{}
for ref, datasource := range *ds {
if datasource.value == (cty.Value{}) {
diags = append(diags, &hcl.Diagnostic{
Summary: "empty value",
Subject: &datasource.block.DefRange,
Severity: hcl.DiagError,
})
continue
}
inner := valuesMap[ref.Type]
if inner == nil {
inner = map[string]cty.Value{}
}
inner[ref.Name] = datasource.value
res[ref.Type] = cty.MapVal(inner)
// Keeps values of different datasources from same type
valuesMap[ref.Type] = inner
}
return res, diags
}
func (cfg *PackerConfig) startDatasource(ds DatasourceBlock) (packersdk.Datasource, hcl.Diagnostics) {
var diags hcl.Diagnostics
block := ds.block
dataSourceStore := cfg.parser.PluginConfig.DataSources
if dataSourceStore == nil {
diags = append(diags, &hcl.Diagnostic{
Summary: "Unknown " + dataSourceLabel + " type " + ds.Type,
Subject: block.LabelRanges[0].Ptr(),
Detail: "packer does not currently know any data source.",
Severity: hcl.DiagError,
})
return nil, diags
}
if !dataSourceStore.Has(ds.Type) {
diags = append(diags, &hcl.Diagnostic{
Summary: "Unknown " + dataSourceLabel + " type " + ds.Type,
Subject: block.LabelRanges[0].Ptr(),
Detail: fmt.Sprintf("known data sources: %v", dataSourceStore.List()),
Severity: hcl.DiagError,
})
return nil, diags
}
datasource, err := dataSourceStore.Start(ds.Type)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Summary: err.Error(),
Subject: &block.DefRange,
Severity: hcl.DiagError,
})
}
if datasource == nil {
diags = append(diags, &hcl.Diagnostic{
Summary: fmt.Sprintf("failed to start datasource plugin %q.%q", ds.Type, ds.Name),
Subject: &block.DefRange,
Severity: hcl.DiagError,
})
}
var decoded cty.Value
var moreDiags hcl.Diagnostics
body := block.Body
decoded, moreDiags = decodeHCL2Spec(body, cfg.EvalContext(DatasourceContext, nil), datasource)
diags = append(diags, moreDiags...)
if moreDiags.HasErrors() {
return nil, diags
}
// In case of cty.Unknown values, this will write a equivalent placeholder
// of the same type. Unknown types are not recognized by the json marshal
// during the RPC call and we have to do this here to avoid json parsing
// failures when running the validate command. We don't do this before so
// we can validate if variable type matches correctly on decodeHCL2Spec.
decoded = hcl2shim.WriteUnknownPlaceholderValues(decoded)
if err := datasource.Configure(decoded); err != nil {
diags = append(diags, &hcl.Diagnostic{
Summary: err.Error(),
Subject: &block.DefRange,
Severity: hcl.DiagError,
})
}
return datasource, diags
}
// datasourcesDone checks whether all the datasources have been executed or not
func (cfg *PackerConfig) datasourcesDone() bool {
for _, ds := range cfg.Datasources {
if !ds.executed() {
return false
}
}
return true
}
func (cfg *PackerConfig) executeDatasources(skipExecution bool) hcl.Diagnostics {
// If we are done with datasources execution, we leave immediately
if cfg.datasourcesDone() {
return nil
}
var outDiags hcl.Diagnostics
foundSomething := false
outerDSEval:
for _, ds := range cfg.Datasources {
if ds.executed() {
continue
}
diags := ds.Execute(cfg, skipExecution)
for _, diag := range diags {
if diag.Summary == notReadyDataSourceError {
// If we have a not ready error in the
// datasource list, we should attempt to run the
// rest, and eventually settle if we cannot move
// any further
continue outerDSEval
}
}
foundSomething = true
outDiags = append(outDiags, diags...)
}
if !foundSomething {
return append(outDiags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "No datasource could be executed",
Detail: `While trying to recurisvely evaluating datasources, we could not find a next datasource to execute.
This is likely due to a cyclic dependency in your datasources`,
})
}
// If we couldn't execute a datasource for whatever reason, we leave
if outDiags.HasErrors() {
return outDiags
}
// If we still found something to execute, we recursively execute the
// remainder of the datasources
return outDiags.Extend(cfg.executeDatasources(skipExecution))
}