2021-02-02 12:05:04 -05:00
package hcl2template
import (
"fmt"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/packer/hcl2template/addrs"
2021-03-24 06:31:39 -04:00
"github.com/hashicorp/packer/packer"
2021-02-02 12:05:04 -05:00
"github.com/zclconf/go-cty/cty"
)
func ( cfg * PackerConfig ) decodeRequiredPluginsBlock ( f * hcl . File ) hcl . Diagnostics {
var diags hcl . Diagnostics
content , moreDiags := f . Body . Content ( configSchema )
diags = append ( diags , moreDiags ... )
for _ , block := range content . Blocks {
switch block . Type {
case packerLabel :
content , contentDiags := block . Body . Content ( packerBlockSchema )
diags = append ( diags , contentDiags ... )
// We ignore "packer_version"" here because
// sniffCoreVersionRequirements already dealt with that
for _ , innerBlock := range content . Blocks {
switch innerBlock . Type {
case "required_plugins" :
reqs , reqsDiags := decodeRequiredPluginsBlock ( innerBlock )
diags = append ( diags , reqsDiags ... )
cfg . Packer . RequiredPlugins = append ( cfg . Packer . RequiredPlugins , reqs )
default :
continue
}
}
}
}
return diags
}
func ( cfg * PackerConfig ) decodeImplicitRequiredPluginsBlocks ( f * hcl . File ) hcl . Diagnostics {
2021-03-24 06:31:39 -04:00
// when a plugin is used but not available it should be 'implicitly
// required'. Here we read common configuration blocks to try to guess
// plugin usages.
// decodeRequiredPluginsBlock needs to be called before
// decodeImplicitRequiredPluginsBlocks; otherwise all required plugins will
// be implicitly required too.
2021-02-02 12:05:04 -05:00
var diags hcl . Diagnostics
content , moreDiags := f . Body . Content ( configSchema )
diags = append ( diags , moreDiags ... )
for _ , block := range content . Blocks {
2021-03-24 06:31:39 -04:00
2021-02-02 12:05:04 -05:00
switch block . Type {
case sourceLabel :
2021-03-24 06:31:39 -04:00
diags = append ( diags , cfg . decodeImplicitRequiredPluginsBlock ( Builder , block ) ... )
case dataSourceLabel :
diags = append ( diags , cfg . decodeImplicitRequiredPluginsBlock ( Datasource , block ) ... )
case buildLabel :
content , _ , moreDiags := block . Body . PartialContent ( buildSchema )
diags = append ( diags , moreDiags ... )
for _ , block := range content . Blocks {
switch block . Type {
case buildProvisionerLabel :
diags = append ( diags , cfg . decodeImplicitRequiredPluginsBlock ( Provisioner , block ) ... )
case buildPostProcessorLabel :
diags = append ( diags , cfg . decodeImplicitRequiredPluginsBlock ( PostProcessor , block ) ... )
case buildPostProcessorsLabel :
content , _ , moreDiags := block . Body . PartialContent ( postProcessorsSchema )
diags = append ( diags , moreDiags ... )
for _ , block := range content . Blocks {
switch block . Type {
case buildPostProcessorLabel :
diags = append ( diags , cfg . decodeImplicitRequiredPluginsBlock ( PostProcessor , block ) ... )
}
}
}
}
2021-02-02 12:05:04 -05:00
}
}
return diags
}
2021-03-24 06:31:39 -04:00
func ( cfg * PackerConfig ) decodeImplicitRequiredPluginsBlock ( k ComponentKind , block * hcl . Block ) hcl . Diagnostics {
if len ( block . Labels ) == 0 {
// malformed block ? Let's not panic :)
return nil
}
// Currently all block types are `type "component-kind" ["name"] {`
// this makes this simple.
componentName := block . Labels [ 0 ]
store := map [ ComponentKind ] packer . BasicStore {
Builder : cfg . parser . PluginConfig . Builders ,
PostProcessor : cfg . parser . PluginConfig . PostProcessors ,
Provisioner : cfg . parser . PluginConfig . Provisioners ,
Datasource : cfg . parser . PluginConfig . DataSources ,
} [ k ]
if store . Has ( componentName ) {
// If any core or pre-loaded plugin defines the `happycloud-uploader`
// pp, skip. This happens for core and manually installed plugins, as
// they will be listed in the PluginConfig before parsing any HCL.
return nil
}
redirect := map [ ComponentKind ] map [ string ] string {
Builder : cfg . parser . PluginConfig . BuilderRedirects ,
PostProcessor : cfg . parser . PluginConfig . PostProcessorRedirects ,
Provisioner : cfg . parser . PluginConfig . ProvisionerRedirects ,
Datasource : cfg . parser . PluginConfig . DatasourceRedirects ,
} [ k ] [ componentName ]
if redirect == "" {
// no known redirect for this component
return nil
}
redirectAddr , diags := addrs . ParsePluginSourceString ( redirect )
if diags . HasErrors ( ) {
// This should never happen, since the map is manually filled.
return diags
}
for _ , req := range cfg . Packer . RequiredPlugins {
if _ , found := req . RequiredPlugins [ redirectAddr . Type ] ; found {
// This could happen if a plugin was forked. For example, I forked
// the github.com/hashicorp/happycloud plugin into
// github.com/azr/happycloud that is required in my config file; and
// am using the `happycloud-uploader` pp component from it. In that
// case - and to avoid miss-requires - we won't implicitly import
// any other `happycloud` plugin.
return nil
}
}
cfg . implicitlyRequirePlugin ( redirectAddr )
return nil
}
func ( cfg * PackerConfig ) implicitlyRequirePlugin ( plugin * addrs . Plugin ) {
cfg . Packer . RequiredPlugins = append ( cfg . Packer . RequiredPlugins , & RequiredPlugins {
RequiredPlugins : map [ string ] * RequiredPlugin {
plugin . Type : {
Name : plugin . Type ,
Source : plugin . String ( ) ,
Type : plugin ,
Requirement : VersionConstraint {
Required : nil , // means latest
} ,
PluginDependencyReason : PluginDependencyImplicit ,
} ,
} ,
} )
}
2021-02-02 12:05:04 -05:00
// RequiredPlugin represents a declaration of a dependency on a particular
// Plugin version or source.
type RequiredPlugin struct {
2021-02-15 06:21:10 -05:00
Name string
// Source used to be able to tell how the template referenced this source,
// for example, "awesomecloud" instead of github.com/awesome/awesomecloud.
// This one is left here in case we want to go back to allowing inexplicit
// source url definitions.
2021-02-02 12:05:04 -05:00
Source string
Type * addrs . Plugin
Requirement VersionConstraint
DeclRange hcl . Range
2021-03-24 06:31:39 -04:00
PluginDependencyReason
2021-02-02 12:05:04 -05:00
}
2021-03-24 06:31:39 -04:00
// PluginDependencyReason is an enumeration of reasons why a dependency might be
// present.
type PluginDependencyReason int
const (
// PluginDependencyExplicit means that there is an explicit
// "required_plugin" block in the configuration.
PluginDependencyExplicit PluginDependencyReason = iota
// PluginDependencyImplicit means that there is no explicit
// "required_plugin" block but there is at least one resource that uses this
// plugin.
PluginDependencyImplicit
)
2021-02-02 12:05:04 -05:00
type RequiredPlugins struct {
RequiredPlugins map [ string ] * RequiredPlugin
DeclRange hcl . Range
}
func decodeRequiredPluginsBlock ( block * hcl . Block ) ( * RequiredPlugins , hcl . Diagnostics ) {
attrs , diags := block . Body . JustAttributes ( )
ret := & RequiredPlugins {
2021-02-15 06:21:10 -05:00
RequiredPlugins : nil ,
2021-02-02 12:05:04 -05:00
DeclRange : block . DefRange ,
}
for name , attr := range attrs {
expr , err := attr . Expr . Value ( nil )
if err != nil {
diags = append ( diags , err ... )
}
nameDiags := checkPluginNameNormalized ( name , attr . Expr . Range ( ) )
diags = append ( diags , nameDiags ... )
rp := & RequiredPlugin {
Name : name ,
DeclRange : attr . Expr . Range ( ) ,
}
switch {
case expr . Type ( ) . IsPrimitiveType ( ) :
2021-02-15 05:47:44 -05:00
c := "version"
if cs , _ := decodeVersionConstraint ( attr ) ; len ( cs . Required ) > 0 {
c = cs . Required . String ( )
2021-02-02 12:05:04 -05:00
}
2021-02-15 05:47:44 -05:00
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid plugin requirement" ,
Detail : fmt . Sprintf ( ` '%s = "%s"' plugin requirement calls are not possible. ` +
` You must define a whole block. For example: ` + "\n" +
` %[1]s = { ` + "\n" +
2021-02-15 07:41:23 -05:00
` source = "github.com/hashicorp/%[1]s" ` + "\n" +
` version = "%[2]s" ` + "\n" + ` } ` ,
2021-02-15 05:47:44 -05:00
name , c ) ,
Subject : attr . Range . Ptr ( ) ,
} )
2021-02-15 06:21:10 -05:00
continue
2021-02-15 05:47:44 -05:00
2021-02-02 12:05:04 -05:00
case expr . Type ( ) . IsObjectType ( ) :
if ! expr . Type ( ) . HasAttribute ( "version" ) {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "No version constraint was set" ,
Detail : "The version field must be specified as a string. Ex: `version = \">= 1.2.0, < 2.0.0\". See https://www.packer.io/docs/templates/hcl_templates/blocks/packer#version-constraints for docs" ,
Subject : attr . Expr . Range ( ) . Ptr ( ) ,
} )
continue
}
vc := VersionConstraint {
DeclRange : attr . Range ,
}
constraint := expr . GetAttr ( "version" )
if ! constraint . Type ( ) . Equals ( cty . String ) || constraint . IsNull ( ) {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid version constraint" ,
Detail : "Version must be specified as a string. See https://www.packer.io/docs/templates/hcl_templates/blocks/packer#version-constraint-syntax for docs." ,
Subject : attr . Expr . Range ( ) . Ptr ( ) ,
} )
continue
}
constraintStr := constraint . AsString ( )
constraints , err := version . NewConstraint ( constraintStr )
if err != nil {
// NewConstraint doesn't return user-friendly errors, so we'll just
// ignore the provided error and produce our own generic one.
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid version constraint" ,
2021-02-15 05:49:02 -05:00
Detail : "This string does not use correct version constraint syntax. " +
"See https://www.packer.io/docs/templates/hcl_templates/blocks/packer#version-constraint-syntax for docs.\n" +
err . Error ( ) ,
Subject : attr . Expr . Range ( ) . Ptr ( ) ,
2021-02-02 12:05:04 -05:00
} )
continue
}
vc . Required = constraints
rp . Requirement = vc
if ! expr . Type ( ) . HasAttribute ( "source" ) {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "No source was set" ,
Detail : "The source field must be specified as a string. Ex: `source = \"coolcloud\". See https://www.packer.io/docs/templates/hcl_templates/blocks/packer#specifying-plugin-requirements for docs" ,
Subject : attr . Expr . Range ( ) . Ptr ( ) ,
} )
continue
}
source := expr . GetAttr ( "source" )
if ! source . Type ( ) . Equals ( cty . String ) || source . IsNull ( ) {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid source" ,
Detail : "Source must be specified as a string. For example: " + ` source = "coolcloud" ` ,
Subject : attr . Expr . Range ( ) . Ptr ( ) ,
} )
continue
}
rp . Source = source . AsString ( )
p , sourceDiags := addrs . ParsePluginSourceString ( rp . Source )
if sourceDiags . HasErrors ( ) {
for _ , diag := range sourceDiags {
if diag . Subject == nil {
diag . Subject = attr . Expr . Range ( ) . Ptr ( )
}
}
diags = append ( diags , sourceDiags ... )
2021-02-15 06:21:10 -05:00
continue
2021-02-02 12:05:04 -05:00
} else {
rp . Type = p
}
attrTypes := expr . Type ( ) . AttributeTypes ( )
for name := range attrTypes {
if name == "version" || name == "source" {
continue
}
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid required_plugins object" ,
Detail : ` required_plugins objects can only contain "version" and "source" attributes. ` ,
Subject : attr . Expr . Range ( ) . Ptr ( ) ,
} )
break
}
default :
// should not happen
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid required_plugins syntax" ,
Detail : "required_plugins entries must be objects." ,
Subject : attr . Expr . Range ( ) . Ptr ( ) ,
} )
}
2021-02-15 06:21:10 -05:00
if ret . RequiredPlugins == nil {
ret . RequiredPlugins = make ( map [ string ] * RequiredPlugin )
}
2021-02-02 12:05:04 -05:00
ret . RequiredPlugins [ rp . Name ] = rp
}
return ret , diags
}
// checkPluginNameNormalized verifies that the given string is already
// normalized and returns an error if not.
func checkPluginNameNormalized ( name string , declrange hcl . Range ) hcl . Diagnostics {
var diags hcl . Diagnostics
// verify that the plugin local name is normalized
normalized , err := addrs . IsPluginPartNormalized ( name )
if err != nil {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid plugin local name" ,
Detail : fmt . Sprintf ( "%s is an invalid plugin local name: %s" , name , err ) ,
Subject : & declrange ,
} )
return diags
}
if ! normalized {
// we would have returned this error already
normalizedPlugin , _ := addrs . ParsePluginPart ( name )
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid plugin local name" ,
Detail : fmt . Sprintf ( "Plugin names must be normalized. Replace %q with %q to fix this error." , name , normalizedPlugin ) ,
Subject : & declrange ,
} )
}
return diags
}