mirror of
https://github.com/hashicorp/packer.git
synced 2026-10-11 00:41:38 -04:00
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.
294 lines
7.9 KiB
Go
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))
|
|
}
|