HCL2: add a packer block with a required_version input setting (#10149)

* add the possibility to set the packer.required_version field; to make sure the template file works with that version of Packer
* add tests
* add documentation on packer.required_version

Example:

packer {
  required_version = ">= 1.2.0, < 2.0.0"
}
This commit is contained in:
Adrien Delorme 2020-10-27 10:03:36 +01:00 committed by GitHub
parent 73e842fab6
commit 4bc16455b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 322 additions and 1 deletions

View File

@ -0,0 +1,7 @@
packer {
required_version = ">= 700000.0.0"
}
block_from_the_future {
the_answer_to_life_the_universe_and_everything = 42
}

View File

@ -0,0 +1,3 @@
packer {
required_version = ">= 1.0.0"
}

View File

@ -0,0 +1,3 @@
packer {
version = ">= 700000.0.0"
}

View File

@ -19,6 +19,13 @@ func TestValidateCommand(t *testing.T) {
{path: filepath.Join(testFixture("validate-invalid"), "missing_build_block.pkr.hcl"), exitCode: 1},
{path: filepath.Join(testFixture("validate"), "null_var.json"), exitCode: 1},
{path: filepath.Join(testFixture("validate"), "var_foo_with_no_default.pkr.hcl"), exitCode: 1},
// wrong version fails
{path: filepath.Join(testFixture("version_req", "base_failure")), exitCode: 1},
{path: filepath.Join(testFixture("version_req", "base_success")), exitCode: 0},
// wrong version field
{path: filepath.Join(testFixture("version_req", "wrong_field_name")), exitCode: 1},
}
for _, tc := range tt {

View File

@ -13,6 +13,7 @@ import (
)
const (
packerLabel = "packer"
sourceLabel = "source"
variablesLabel = "variables"
variableLabel = "variable"
@ -23,6 +24,7 @@ const (
var configSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{Type: packerLabel},
{Type: sourceLabel, LabelNames: []string{"type", "name"}},
{Type: variablesLabel},
{Type: variableLabel, LabelNames: []string{"name"}},
@ -32,6 +34,14 @@ var configSchema = &hcl.BodySchema{
},
}
// packerBlockSchema is the schema for a top-level "packer" block in
// a configuration file.
var packerBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{Name: "required_version"},
},
}
// Parser helps you parse HCL folders. It will parse an hcl file or directory
// and start builders, provisioners and post-processors to configure them with
// the parsed HCL and then return a []packer.Build. Packer will use that list
@ -114,6 +124,21 @@ func (p *Parser) Parse(filename string, varFiles []string, argVars map[string]st
files: files,
}
for _, file := range files {
coreVersionConstraints, moreDiags := sniffCoreVersionRequirements(file.Body)
cfg.Packer.VersionConstraints = append(cfg.Packer.VersionConstraints, coreVersionConstraints...)
diags = append(diags, moreDiags...)
}
// Before we go further, we'll check to make sure this version can read
// that file, so we can produce a version-related error message rather than
// potentially-confusing downstream errors.
versionDiags := cfg.CheckCoreVersionRequirements()
diags = append(diags, versionDiags...)
if versionDiags.HasErrors() {
return cfg, diags
}
// Decode variable blocks so that they are available later on. Here locals
// can use input variables so we decode them firsthand.
{
@ -169,6 +194,50 @@ func (p *Parser) Parse(filename string, varFiles []string, argVars map[string]st
return cfg, diags
}
// sniffCoreVersionRequirements does minimal parsing of the given body for
// "packer" blocks with "required_version" attributes, returning the
// requirements found.
//
// This is intended to maximize the chance that we'll be able to read the
// requirements (syntax errors notwithstanding) even if the config file contains
// constructs that might've been added in future versions
//
// This is a "best effort" sort of method which will return constraints it is
// able to find, but may return no constraints at all if the given body is
// so invalid that it cannot be decoded at all.
func sniffCoreVersionRequirements(body hcl.Body) ([]VersionConstraint, hcl.Diagnostics) {
var sniffRootSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: packerLabel,
},
},
}
rootContent, _, diags := body.PartialContent(sniffRootSchema)
var constraints []VersionConstraint
for _, block := range rootContent.Blocks {
content, blockDiags := block.Body.Content(packerBlockSchema)
diags = append(diags, blockDiags...)
attr, exists := content.Attributes["required_version"]
if !exists {
continue
}
constraint, constraintDiags := decodeVersionConstraint(attr)
diags = append(diags, constraintDiags...)
if !constraintDiags.HasErrors() {
constraints = append(constraints, constraint)
}
}
return constraints, diags
}
func (cfg *PackerConfig) Initialize() hcl.Diagnostics {
var diags hcl.Diagnostics

View File

@ -16,6 +16,9 @@ import (
// PackerConfig represents a loaded Packer HCL config. It will contain
// references to all possible blocks of the allowed configuration.
type PackerConfig struct {
Packer struct {
VersionConstraints []VersionConstraint
}
// Directory where the config files are defined
Basedir string
// directory Packer was called from

View File

@ -12,7 +12,7 @@ import (
"github.com/zclconf/go-cty/cty/convert"
)
// Local represents a single entry from a "locals" block in a module or file.
// Local represents a single entry from a "locals" block in a file.
// The "locals" block itself is not represented, because it serves only to
// provide context for us to interpret its contents.
type LocalBlock struct {

70
hcl2template/version.go Normal file
View File

@ -0,0 +1,70 @@
package hcl2template
import (
"fmt"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
// VersionConstraint represents a version constraint on some resource that
// carries with it a source range so that a helpful diagnostic can be printed
// in the event that a particular constraint does not match.
type VersionConstraint struct {
Required version.Constraints
DeclRange hcl.Range
}
func decodeVersionConstraint(attr *hcl.Attribute) (VersionConstraint, hcl.Diagnostics) {
ret := VersionConstraint{
DeclRange: attr.Range,
}
val, diags := attr.Expr.Value(nil)
if diags.HasErrors() {
return ret, diags
}
var err error
val, err = convert.Convert(val, cty.String)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version constraint",
Detail: fmt.Sprintf("A string value is required for %s.", attr.Name),
Subject: attr.Expr.Range().Ptr(),
})
return ret, diags
}
if val.IsNull() {
// A null version constraint is strange, but we'll just treat it
// like an empty constraint set.
return ret, diags
}
if !val.IsWhollyKnown() {
// If there is a syntax error, HCL sets the value of the given attribute
// to cty.DynamicVal. A diagnostic for the syntax error will already
// bubble up, so we will move forward gracefully here.
return ret, diags
}
constraintStr := val.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. Check out the docs: https://packer.io/docs/from-1.5/blocks/packer#version-constraints",
Subject: attr.Expr.Range().Ptr(),
})
return ret, diags
}
ret.Required = constraints
return ret, diags
}

View File

@ -0,0 +1,39 @@
package hcl2template
import (
"fmt"
"github.com/hashicorp/hcl/v2"
pkrversion "github.com/hashicorp/packer/version"
)
// CheckCoreVersionRequirements visits each of the block in the given
// configuration and verifies that any given Core version constraints match
// with the version of Packer Core that is being used.
//
// The returned diagnostics will contain errors if any constraints do not match.
// The returned diagnostics might also return warnings, which should be
// displayed to the user.
func (cfg *PackerConfig) CheckCoreVersionRequirements() hcl.Diagnostics {
if cfg == nil {
return nil
}
var diags hcl.Diagnostics
for _, constraint := range cfg.Packer.VersionConstraints {
if !constraint.Required.Check(pkrversion.SemVer) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported Packer Core version",
Detail: fmt.Sprintf(
"This configuration does not support Packer version %s. To proceed, either choose another supported Packer version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.",
pkrversion.String(),
),
Subject: constraint.DeclRange.Ptr(),
})
}
}
return diags
}

View File

@ -3,6 +3,8 @@ package version
import (
"bytes"
"fmt"
"github.com/hashicorp/go-version"
)
// The git commit that was compiled. This will be filled in by the compiler.
@ -29,3 +31,20 @@ func FormattedVersion() string {
return versionString.String()
}
// SemVer is an instance of version.Version. This has the secondary
// benefit of verifying during tests and init time that our version is a
// proper semantic version, which should always be the case.
var SemVer *version.Version
func init() {
SemVer = version.Must(version.NewVersion(Version))
}
// String returns the complete version string, including prerelease
func String() string {
if VersionPrerelease != "" {
return fmt.Sprintf("%s-%s", Version, VersionPrerelease)
}
return Version
}

View File

@ -24,6 +24,7 @@ export default [
'locals',
'source',
'variable',
'packer',
],
},
{

View File

@ -0,0 +1,100 @@
---
layout: docs
page_title: packer - Blocks
sidebar_title: <tt>packer</tt>
description: |-
The "packer" configuration section is used to configure some behaviors
of Packer itself.
---
# Packer Settings
`@include 'from-1.5/beta-hcl2-note.mdx'`
The `packer` configuration block type is used to configure some
behaviors of Packer itself, such as the minimum required Packer version needed to
apply your configuration.
## Packer Block Syntax
Packer settings are gathered together into `packer` blocks:
```hcl
packer {
# ...
}
```
Each `packer` block can contain a number of settings related to Packer's
behavior. Within a `packer` block, only constant values can be used;
arguments may not refer to named objects such as resources, input variables,
etc, and may not use any of the Packer language built-in functions.
The various options supported within a `packer` block are described in the
following sections.
## Specifying a Required Packer Version
The `required_version` setting accepts a [version constraint
string,](#version-constraints) which specifies which versions of Packer
can be used with your configuration.
If the running version of Packer doesn't match the constraints specified,
Packer will produce an error and exit without taking any further actions.
Use Packer version constraints in a collaborative environment to
ensure that everyone is using a specific Packer version, or using at least
a minimum Packer version that has behavior expected by the configuration.
## Version Constraints
Anywhere that Packer lets you specify a range of acceptable versions for
something, it expects a specially formatted string known as a version
constraint.
### Version Constraint Syntax
Packer's syntax for version constraints is very similar to the syntax used by
other dependency management systems like Bundler and NPM.
```hcl
required_version = ">= 1.2.0, < 2.0.0"
```
A version constraint is a [string literal](/docs/from-1.5/expressions#string-literals)
containing one or more conditions, which are separated by commas.
Each condition consists of an operator and a version number.
Version numbers should be a series of numbers separated by periods (like
`1.2.0`), optionally with a suffix to indicate a beta release.
The following operators are valid:
- `=` (or no operator): Allows only one exact version number. Cannot be combined
with other conditions.
- `!=`: Excludes an exact version number.
- `>`, `>=`, `<`, `<=`: Comparisons against a specified version, allowing
versions for which the comparison is true. "Greater-than" requests newer
versions, and "less-than" requests older versions.
- `~>`: Allows the specified version, plus newer versions that only
increase the _most specific_ segment of the specified version number. For
example, `~> 0.9` is equivalent to `>= 0.9, < 1.0`, and `~> 0.8.4`, is
equivalent to `>= 0.8.4, < 0.9`. This is usually called the pessimistic
constraint operator.
### Version Constraint Behavior
A version number that meets every applicable constraint is considered acceptable.
Packer consults version constraints to determine whether it has acceptable
versions of itself.
A prerelease version is a version number that contains a suffix introduced by
a dash, like `1.2.0-beta`. A prerelease version can be selected only by an
_exact_ version constraint (the `=` operator or no operator). Prerelease
versions do not match inexact operators such as `>=`, `~>`, etc.