From 8e21597e3042ef1564cb0aed8830a98243ecede0 Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Fri, 30 Aug 2024 13:25:33 -0400 Subject: [PATCH] hcl2template: add refstring type for generic refs The hcl2template package contains references already, but these are linked to a particular type. This becomes problematic if we want to support cross-type references, so this commit adds a new abstraction: refString. A refString contains the component type, its type (if applicable), and its name, so that the combination of those points to a cty object that can be linked to a block in the configuration. Right now, only `var`, `local` and `data` are supported, but the type is extensible enough that anything else that fits this model such as sources can be supported in the future potentially. --- hcl2template/types.refstring.go | 144 ++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 hcl2template/types.refstring.go diff --git a/hcl2template/types.refstring.go b/hcl2template/types.refstring.go new file mode 100644 index 000000000..3199c9905 --- /dev/null +++ b/hcl2template/types.refstring.go @@ -0,0 +1,144 @@ +package hcl2template + +import ( + "fmt" + "strings" + + "github.com/hashicorp/hcl/v2" +) + +// A refstring is any reference string that can point to a component of a config +// +// This includes anything in the following format: +// +// * data.. +// * var. +// * local. +type refString struct { + // RefMType is the top-level label, that is used to know which type of + // component we are to look for (i.e. data for Datasource, local for + // Locals, etc.) + MType string + // RefType is the type of component, as in the name of the plugin that + // will handle evaluation for the component. + // + // Only for Datasources now as var/local do not have a component type + // (they're always evaluated by Packer itself, not a plugin). + Type string + // RefName is the name of the component to get. + // For locals/vars this is the name of the variable to look for, while + // for datasources this is the name of the block, which coupled with the + // type is the identity of the datasource's execution. + Name string +} + +func NewRefStringFromDep(t hcl.Traversal) (refString, error) { + root := t.RootName() + + switch root { + case "local", "var": + return NewRefString(fmt.Sprintf("%s.%s", root, t[1].(hcl.TraverseAttr).Name)) + case "data": + return NewRefString(fmt.Sprintf("%s.%s.%s", root, + t[1].(hcl.TraverseAttr).Name, + t[2].(hcl.TraverseAttr).Name)) + } + + return refString{}, fmt.Errorf("unsupported refstring %q, must be of 'data', 'local' or 'var' type", t) +} + +func NewRefString(rs string) (refString, error) { + parts := strings.Split(rs, ".") + + switch parts[0] { + case "local", "var": + return refString{ + MType: parts[0], + Name: parts[1], + }, nil + case "data": + return newDataSourceRefString(parts) + } + + return refString{}, fmt.Errorf("unsupported reftype %q, must be either 'data', 'local' or 'var'", parts[0]) +} + +func (rs refString) String() string { + if rs.Type == "" { + return fmt.Sprintf("%s.%s", rs.MType, rs.Name) + } + + return fmt.Sprintf("%s.%s.%s", rs.MType, rs.Type, rs.Name) +} + +func newDataSourceRefString(parts []string) (refString, error) { + if len(parts) != 3 { + return refString{}, fmt.Errorf("malformed datasource reference %q, data sources must be composed of 3 parts", + strings.Join(parts, ".")) + } + + return refString{ + MType: "data", + Type: parts[1], + Name: parts[2], + }, nil +} + +// getComponentByRef gets a registered component from the configuration from a refString +func (cfg *PackerConfig) getComponentByRef(rs refString) (interface{}, error) { + switch rs.MType { + case "data": + for _, ds := range cfg.Datasources { + if ds.Type != rs.Type { + continue + } + if ds.DSName != rs.Name { + continue + } + return ds, nil + } + return nil, fmt.Errorf("failed to get datasource '%s.%s': component unknown", rs.Type, rs.Name) + case "local": + for _, loc := range cfg.LocalBlocks { + if loc.Name != rs.Name { + continue + } + return loc, nil + } + case "var": + for _, val := range cfg.InputVariables { + if val.Name != rs.Name { + continue + } + return val, nil + } + } + + return nil, fmt.Errorf("Unsupported component: %q, only vars, locals and datasources can be fetched by their refString", rs) +} + +func (ds *DatasourceBlock) RegisterDependency(rs refString) error { + switch rs.MType { + case "data", "local": + ds.Dependencies = append(ds.Dependencies, rs) + // NOOP: vars are always evaluated beforehand for datasources + case "var": + default: + return fmt.Errorf("unsupported dependency type %q; datasources can only depend on local, var or data.", rs.MType) + } + + return nil +} + +func (loc *LocalBlock) RegisterDependency(rs refString) error { + switch rs.MType { + case "data", "local": + loc.dependencies = append(loc.dependencies, rs) + // NOOP: vars are always evaluated beforehand for locals + case "var": + default: + return fmt.Errorf("unsupported dependency type %q; locals can only depend on local, var or data.", rs.MType) + } + + return nil +}