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:
parent
73e842fab6
commit
4bc16455b4
|
@ -0,0 +1,7 @@
|
|||
packer {
|
||||
required_version = ">= 700000.0.0"
|
||||
}
|
||||
|
||||
block_from_the_future {
|
||||
the_answer_to_life_the_universe_and_everything = 42
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
packer {
|
||||
required_version = ">= 1.0.0"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
packer {
|
||||
version = ">= 700000.0.0"
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ export default [
|
|||
'locals',
|
||||
'source',
|
||||
'variable',
|
||||
'packer',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue