package hcl2template import ( "fmt" "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/packer/hcl2template/addrs" "github.com/hashicorp/packer/packer" "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 { // 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. var diags hcl.Diagnostics content, moreDiags := f.Body.Content(configSchema) diags = append(diags, moreDiags...) for _, block := range content.Blocks { switch block.Type { case sourceLabel: 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)...) } } } } } } return diags } 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, }, }, }) } // RequiredPlugin represents a declaration of a dependency on a particular // Plugin version or source. type RequiredPlugin struct { 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. Source string Type *addrs.Plugin Requirement VersionConstraint DeclRange hcl.Range PluginDependencyReason } // 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 ) 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{ RequiredPlugins: nil, 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(): c := "version" if cs, _ := decodeVersionConstraint(attr); len(cs.Required) > 0 { c = cs.Required.String() } 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"+ ` source = "github.com/hashicorp/%[1]s"`+"\n"+ ` version = "%[2]s"`+"\n"+`}`, name, c), Subject: attr.Range.Ptr(), }) continue 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", 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(), }) 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...) continue } 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(), }) } if ret.RequiredPlugins == nil { ret.RequiredPlugins = make(map[string]*RequiredPlugin) } 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 }