297 lines
13 KiB
Go
297 lines
13 KiB
Go
//go:generate struct-markdown
|
|
|
|
package common
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"regexp"
|
|
|
|
"github.com/hashicorp/packer-plugin-sdk/template/config"
|
|
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
|
)
|
|
|
|
// AMIConfig is for common configuration related to creating AMIs.
|
|
type AMIConfig struct {
|
|
// The name of the resulting AMI that will appear when managing AMIs in the
|
|
// AWS console or via APIs. This must be unique. To help make this unique,
|
|
// use a function like timestamp (see [template
|
|
// engine](/docs/templates/legacy_json_templates/engine) for more info).
|
|
AMIName string `mapstructure:"ami_name" required:"true"`
|
|
// The description to set for the resulting
|
|
// AMI(s). By default this description is empty. This is a
|
|
// [template engine](/docs/templates/legacy_json_templates/engine), see [Build template
|
|
// data](#build-template-data) for more information.
|
|
AMIDescription string `mapstructure:"ami_description" required:"false"`
|
|
// The type of virtualization for the AMI
|
|
// you are building. This option is required to register HVM images. Can be
|
|
// paravirtual (default) or hvm.
|
|
AMIVirtType string `mapstructure:"ami_virtualization_type" required:"false"`
|
|
// A list of account IDs that have access to
|
|
// launch the resulting AMI(s). By default no additional users other than the
|
|
// user creating the AMI has permissions to launch it.
|
|
AMIUsers []string `mapstructure:"ami_users" required:"false"`
|
|
// A list of groups that have access to
|
|
// launch the resulting AMI(s). By default no groups have permission to launch
|
|
// the AMI. all will make the AMI publicly accessible.
|
|
AMIGroups []string `mapstructure:"ami_groups" required:"false"`
|
|
// A list of product codes to
|
|
// associate with the AMI. By default no product codes are associated with the
|
|
// AMI.
|
|
AMIProductCodes []string `mapstructure:"ami_product_codes" required:"false"`
|
|
// A list of regions to copy the AMI to.
|
|
// Tags and attributes are copied along with the AMI. AMI copying takes time
|
|
// depending on the size of the AMI, but will generally take many minutes.
|
|
AMIRegions []string `mapstructure:"ami_regions" required:"false"`
|
|
// Set to true if you want to skip
|
|
// validation of the ami_regions configuration option. Default false.
|
|
AMISkipRegionValidation bool `mapstructure:"skip_region_validation" required:"false"`
|
|
// Key/value pair tags applied to the AMI. This is a [template
|
|
// engine](/docs/templates/legacy_json_templates/engine), see [Build template
|
|
// data](#build-template-data) for more information.
|
|
AMITags map[string]string `mapstructure:"tags" required:"false"`
|
|
// Same as [`tags`](#tags) but defined as a singular repeatable block
|
|
// containing a `key` and a `value` field. In HCL2 mode the
|
|
// [`dynamic_block`](/docs/templates/hcl_templates/expressions#dynamic-blocks)
|
|
// will allow you to create those programatically.
|
|
AMITag config.KeyValues `mapstructure:"tag" required:"false"`
|
|
// Enable enhanced networking (ENA but not SriovNetSupport) on
|
|
// HVM-compatible AMIs. If set, add `ec2:ModifyInstanceAttribute` to your
|
|
// AWS IAM policy.
|
|
//
|
|
// Note: you must make sure enhanced networking is enabled on your
|
|
// instance. See [Amazon's documentation on enabling enhanced
|
|
// networking](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enhanced-networking.html#enabling_enhanced_networking).
|
|
AMIENASupport config.Trilean `mapstructure:"ena_support" required:"false"`
|
|
// Enable enhanced networking (SriovNetSupport but not ENA) on
|
|
// HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` to your
|
|
// AWS IAM policy. Note: you must make sure enhanced networking is enabled
|
|
// on your instance. See [Amazon's documentation on enabling enhanced
|
|
// networking](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enhanced-networking.html#enabling_enhanced_networking).
|
|
// Default `false`.
|
|
AMISriovNetSupport bool `mapstructure:"sriov_support" required:"false"`
|
|
// Force Packer to first deregister an existing
|
|
// AMI if one with the same name already exists. Default false.
|
|
AMIForceDeregister bool `mapstructure:"force_deregister" required:"false"`
|
|
// Force Packer to delete snapshots
|
|
// associated with AMIs, which have been deregistered by force_deregister.
|
|
// Default false.
|
|
AMIForceDeleteSnapshot bool `mapstructure:"force_delete_snapshot" required:"false"`
|
|
// Whether or not to encrypt the resulting AMI when
|
|
// copying a provisioned instance to an AMI. By default, Packer will keep
|
|
// the encryption setting to what it was in the source image. Setting false
|
|
// will result in an unencrypted image, and true will result in an encrypted
|
|
// one.
|
|
//
|
|
// If you have used the `launch_block_device_mappings` to set an encryption
|
|
// key and that key is the same as the one you want the image encrypted with
|
|
// at the end, then you don't need to set this field; leaving it empty will
|
|
// prevent an unnecessary extra copy step and save you some time.
|
|
//
|
|
// Please note that if you are using an account with the global "Always
|
|
// encrypt new EBS volumes" option set to `true`, Packer will be unable to
|
|
// override this setting, and the final image will be encryoted whether
|
|
// you set this value or not.
|
|
AMIEncryptBootVolume config.Trilean `mapstructure:"encrypt_boot" required:"false"`
|
|
// ID, alias or ARN of the KMS key to use for AMI encryption. This
|
|
// only applies to the main `region` -- any regions the AMI gets copied to
|
|
// copied will be encrypted by the default EBS KMS key for that region,
|
|
// unless you set region-specific keys in AMIRegionKMSKeyIDs.
|
|
//
|
|
// Set this value if you select `encrypt_boot`, but don't want to use the
|
|
// region's default KMS key.
|
|
//
|
|
// If you have a custom kms key you'd like to apply to the launch volume,
|
|
// and are only building in one region, it is more efficient to leave this
|
|
// and `encrypt_boot` empty and to instead set the key id in the
|
|
// launch_block_device_mappings (you can find an example below). This saves
|
|
// potentially many minutes at the end of the build by preventing Packer
|
|
// from having to copy and re-encrypt the image at the end of the build.
|
|
//
|
|
// For valid formats see *KmsKeyId* in the [AWS API docs -
|
|
// CopyImage](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html).
|
|
// This field is validated by Packer, when using an alias, you will have to
|
|
// prefix `kms_key_id` with `alias/`.
|
|
AMIKmsKeyId string `mapstructure:"kms_key_id" required:"false"`
|
|
// regions to copy the ami to, along with the custom kms key id (alias or
|
|
// arn) to use for encryption for that region. Keys must match the regions
|
|
// provided in `ami_regions`. If you just want to encrypt using a default
|
|
// ID, you can stick with `kms_key_id` and `ami_regions`. If you want a
|
|
// region to be encrypted with that region's default key ID, you can use an
|
|
// empty string `""` instead of a key id in this map. (e.g. `"us-east-1":
|
|
// ""`) However, you cannot use default key IDs if you are using this in
|
|
// conjunction with `snapshot_users` -- in that situation you must use
|
|
// custom keys. For valid formats see *KmsKeyId* in the [AWS API docs -
|
|
// CopyImage](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html).
|
|
//
|
|
// This option supercedes the `kms_key_id` option -- if you set both, and
|
|
// they are different, Packer will respect the value in
|
|
// `region_kms_key_ids` for your build region and silently disregard the
|
|
// value provided in `kms_key_id`.
|
|
AMIRegionKMSKeyIDs map[string]string `mapstructure:"region_kms_key_ids" required:"false"`
|
|
// If true, Packer will not check whether an AMI with the `ami_name` exists
|
|
// in the region it is building in. It will use an intermediary AMI name,
|
|
// which it will not convert to an AMI in the build region. It will copy
|
|
// the intermediary AMI into any regions provided in `ami_regions`, then
|
|
// delete the intermediary AMI. Default `false`.
|
|
AMISkipBuildRegion bool `mapstructure:"skip_save_build_region"`
|
|
|
|
SnapshotConfig `mapstructure:",squash"`
|
|
}
|
|
|
|
func stringInSlice(s []string, searchstr string) bool {
|
|
for _, item := range s {
|
|
if item == searchstr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *AMIConfig) Prepare(accessConfig *AccessConfig, ctx *interpolate.Context) []error {
|
|
var errs []error
|
|
|
|
errs = append(errs, c.SnapshotTag.CopyOn(&c.SnapshotTags)...)
|
|
errs = append(errs, c.AMITag.CopyOn(&c.AMITags)...)
|
|
|
|
if c.AMIName == "" {
|
|
errs = append(errs, fmt.Errorf("ami_name must be specified"))
|
|
}
|
|
|
|
// Make sure that if we have region_kms_key_ids defined,
|
|
// the regions in region_kms_key_ids are also in ami_regions
|
|
if len(c.AMIRegionKMSKeyIDs) > 0 {
|
|
for kmsKeyRegion := range c.AMIRegionKMSKeyIDs {
|
|
if !stringInSlice(c.AMIRegions, kmsKeyRegion) {
|
|
errs = append(errs, fmt.Errorf("Region %s is in region_kms_key_ids but not in ami_regions", kmsKeyRegion))
|
|
}
|
|
}
|
|
}
|
|
|
|
errs = append(errs, c.prepareRegions(accessConfig)...)
|
|
|
|
// Prevent sharing of default KMS key encrypted volumes with other aws users
|
|
if len(c.AMIUsers) > 0 {
|
|
if len(c.AMIKmsKeyId) == 0 && c.AMIEncryptBootVolume.True() {
|
|
errs = append(errs, fmt.Errorf("Cannot share AMI encrypted with default KMS key"))
|
|
}
|
|
if len(c.AMIRegionKMSKeyIDs) > 0 {
|
|
for _, kmsKey := range c.AMIRegionKMSKeyIDs {
|
|
if len(kmsKey) == 0 {
|
|
errs = append(errs, fmt.Errorf("Cannot share AMI encrypted with default KMS key for other regions"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
kmsKeys := make([]string, 0)
|
|
if len(c.AMIKmsKeyId) > 0 {
|
|
kmsKeys = append(kmsKeys, c.AMIKmsKeyId)
|
|
}
|
|
if len(c.AMIRegionKMSKeyIDs) > 0 {
|
|
for _, kmsKey := range c.AMIRegionKMSKeyIDs {
|
|
if len(kmsKey) > 0 {
|
|
kmsKeys = append(kmsKeys, kmsKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(kmsKeys) > 0 && !c.AMIEncryptBootVolume.True() {
|
|
errs = append(errs, fmt.Errorf("If you have set either "+
|
|
"region_kms_key_ids or kms_key_id, encrypt_boot must also be true."))
|
|
|
|
}
|
|
for _, kmsKey := range kmsKeys {
|
|
if !ValidateKmsKey(kmsKey) {
|
|
errs = append(errs, fmt.Errorf("%q is not a valid KMS Key Id.", kmsKey))
|
|
}
|
|
}
|
|
|
|
if len(c.SnapshotUsers) > 0 {
|
|
if len(c.AMIKmsKeyId) == 0 && len(c.AMIRegionKMSKeyIDs) == 0 && c.AMIEncryptBootVolume.True() {
|
|
errs = append(errs, fmt.Errorf("Cannot share snapshot encrypted "+
|
|
"with default KMS key, see https://www.packer.io/docs/builders/amazon-ebs#region_kms_key_ids for more information"))
|
|
}
|
|
if len(c.AMIRegionKMSKeyIDs) > 0 {
|
|
for _, kmsKey := range c.AMIRegionKMSKeyIDs {
|
|
if len(kmsKey) == 0 {
|
|
errs = append(errs, fmt.Errorf("Cannot share snapshot encrypted with default KMS key"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(c.AMIName) < 3 || len(c.AMIName) > 128 {
|
|
errs = append(errs, fmt.Errorf("ami_name must be between 3 and 128 characters long"))
|
|
}
|
|
|
|
if c.AMIName != templateCleanAMIName(c.AMIName) {
|
|
errs = append(errs, fmt.Errorf("AMIName should only contain "+
|
|
"alphanumeric characters, parentheses (()), square brackets ([]), spaces "+
|
|
"( ), periods (.), slashes (/), dashes (-), single quotes ('), at-signs "+
|
|
"(@), or underscores(_). You can use the `clean_resource_name` template "+
|
|
"filter to automatically clean your ami name."))
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return errs
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *AMIConfig) prepareRegions(accessConfig *AccessConfig) (errs []error) {
|
|
if len(c.AMIRegions) > 0 {
|
|
regionSet := make(map[string]struct{})
|
|
regions := make([]string, 0, len(c.AMIRegions))
|
|
|
|
for _, region := range c.AMIRegions {
|
|
// If we already saw the region, then don't look again
|
|
if _, ok := regionSet[region]; ok {
|
|
continue
|
|
}
|
|
|
|
// Mark that we saw the region
|
|
regionSet[region] = struct{}{}
|
|
|
|
// Make sure that if we have region_kms_key_ids defined,
|
|
// the regions in ami_regions are also in region_kms_key_ids
|
|
if len(c.AMIRegionKMSKeyIDs) > 0 {
|
|
if _, ok := c.AMIRegionKMSKeyIDs[region]; !ok {
|
|
errs = append(errs, fmt.Errorf("Region %s is in ami_regions but not in region_kms_key_ids", region))
|
|
}
|
|
}
|
|
if (accessConfig != nil) && (region == accessConfig.RawRegion) {
|
|
// make sure we don't try to copy to the region we originally
|
|
// create the AMI in.
|
|
log.Printf("Cannot copy AMI to AWS session region '%s', deleting it from `ami_regions`.", region)
|
|
continue
|
|
}
|
|
regions = append(regions, region)
|
|
}
|
|
|
|
c.AMIRegions = regions
|
|
}
|
|
return errs
|
|
}
|
|
|
|
// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html
|
|
func ValidateKmsKey(kmsKey string) (valid bool) {
|
|
kmsKeyIdPattern := `[a-f0-9-]+$`
|
|
aliasPattern := `alias/[a-zA-Z0-9:/_-]+$`
|
|
kmsArnStartPattern := `^arn:aws(-us-gov)?:kms:([a-z]{2}-(gov-)?[a-z]+-\d{1})?:(\d{12}):`
|
|
if regexp.MustCompile(fmt.Sprintf("^%s", kmsKeyIdPattern)).MatchString(kmsKey) {
|
|
return true
|
|
}
|
|
if regexp.MustCompile(fmt.Sprintf("^%s", aliasPattern)).MatchString(kmsKey) {
|
|
return true
|
|
}
|
|
if regexp.MustCompile(fmt.Sprintf("%skey/%s", kmsArnStartPattern, kmsKeyIdPattern)).MatchString(kmsKey) {
|
|
return true
|
|
}
|
|
if regexp.MustCompile(fmt.Sprintf("%s%s", kmsArnStartPattern, aliasPattern)).MatchString(kmsKey) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|