feature: add omi config to bsusurrogate config struct
This commit is contained in:
parent
bb848366b6
commit
f6372e8ac6
|
@ -19,6 +19,7 @@ type Config struct {
|
|||
osccommon.AccessConfig `mapstructure:",squash"`
|
||||
osccommon.RunConfig `mapstructure:",squash"`
|
||||
osccommon.BlockDevices `mapstructure:",squash"`
|
||||
osccommon.OMIConfig `mapstructure:",squash"`
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
)
|
||||
|
||||
//Temporal
|
||||
type TagMap map[string]string
|
||||
|
||||
// OMIConfig is for common configuration related to creating OMIs.
|
||||
type OMIConfig struct {
|
||||
OMIName string `mapstructure:"ami_name"`
|
||||
OMIDescription string `mapstructure:"ami_description"`
|
||||
OMIVirtType string `mapstructure:"ami_virtualization_type"`
|
||||
OMIUsers []string `mapstructure:"ami_users"`
|
||||
OMIGroups []string `mapstructure:"ami_groups"`
|
||||
OMIProductCodes []string `mapstructure:"ami_product_codes"`
|
||||
OMIRegions []string `mapstructure:"ami_regions"`
|
||||
OMISkipRegionValidation bool `mapstructure:"skip_region_validation"`
|
||||
OMITags TagMap `mapstructure:"tags"`
|
||||
OMIENASupport *bool `mapstructure:"ena_support"`
|
||||
OMISriovNetSupport bool `mapstructure:"sriov_support"`
|
||||
OMIForceDeregister bool `mapstructure:"force_deregister"`
|
||||
OMIForceDeleteSnapshot bool `mapstructure:"force_delete_snapshot"`
|
||||
OMIEncryptBootVolume bool `mapstructure:"encrypt_boot"`
|
||||
OMIKmsKeyId string `mapstructure:"kms_key_id"`
|
||||
OMIRegionKMSKeyIDs map[string]string `mapstructure:"region_kms_key_ids"`
|
||||
SnapshotTags TagMap `mapstructure:"snapshot_tags"`
|
||||
SnapshotUsers []string `mapstructure:"snapshot_users"`
|
||||
SnapshotGroups []string `mapstructure:"snapshot_groups"`
|
||||
}
|
||||
|
||||
func stringInSlice(s []string, searchstr string) bool {
|
||||
for _, item := range s {
|
||||
if item == searchstr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *OMIConfig) Prepare(accessConfig *AccessConfig, ctx *interpolate.Context) []error {
|
||||
var errs []error
|
||||
|
||||
if c.OMIName == "" {
|
||||
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.OMIRegionKMSKeyIDs) > 0 {
|
||||
for kmsKeyRegion := range c.OMIRegionKMSKeyIDs {
|
||||
if !stringInSlice(c.OMIRegions, 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)...)
|
||||
|
||||
if len(c.OMIUsers) > 0 && c.OMIEncryptBootVolume {
|
||||
errs = append(errs, fmt.Errorf("Cannot share OMI with encrypted boot volume"))
|
||||
}
|
||||
|
||||
var kmsKeys []string
|
||||
if len(c.OMIKmsKeyId) > 0 {
|
||||
kmsKeys = append(kmsKeys, c.OMIKmsKeyId)
|
||||
}
|
||||
if len(c.OMIRegionKMSKeyIDs) > 0 {
|
||||
for _, kmsKey := range c.OMIRegionKMSKeyIDs {
|
||||
if len(kmsKey) == 0 {
|
||||
kmsKeys = append(kmsKeys, c.OMIKmsKeyId)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, kmsKey := range kmsKeys {
|
||||
if !validateKmsKey(kmsKey) {
|
||||
errs = append(errs, fmt.Errorf("%s is not a valid KMS Key Id.", kmsKey))
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.SnapshotUsers) > 0 {
|
||||
if len(c.OMIKmsKeyId) == 0 && c.OMIEncryptBootVolume {
|
||||
errs = append(errs, fmt.Errorf("Cannot share snapshot encrypted with default KMS key"))
|
||||
}
|
||||
if len(c.OMIRegionKMSKeyIDs) > 0 {
|
||||
for _, kmsKey := range c.OMIRegionKMSKeyIDs {
|
||||
if len(kmsKey) == 0 {
|
||||
errs = append(errs, fmt.Errorf("Cannot share snapshot encrypted with default KMS key"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.OMIName) < 3 || len(c.OMIName) > 128 {
|
||||
errs = append(errs, fmt.Errorf("ami_name must be between 3 and 128 characters long"))
|
||||
}
|
||||
|
||||
if c.OMIName != templateCleanOMIName(c.OMIName) {
|
||||
errs = append(errs, fmt.Errorf("OMIName should only contain "+
|
||||
"alphanumeric characters, parentheses (()), square brackets ([]), spaces "+
|
||||
"( ), periods (.), slashes (/), dashes (-), single quotes ('), at-signs "+
|
||||
"(@), or underscores(_). You can use the `clean_ami_name` template "+
|
||||
"filter to automatically clean your ami name."))
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *OMIConfig) prepareRegions(accessConfig *AccessConfig) (errs []error) {
|
||||
if len(c.OMIRegions) > 0 {
|
||||
regionSet := make(map[string]struct{})
|
||||
regions := make([]string, 0, len(c.OMIRegions))
|
||||
|
||||
for _, region := range c.OMIRegions {
|
||||
// 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.OMIRegionKMSKeyIDs) > 0 {
|
||||
if _, ok := c.OMIRegionKMSKeyIDs[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 OMI in.
|
||||
log.Printf("Cannot copy OMI to AWS session region '%s', deleting it from `ami_regions`.", region)
|
||||
continue
|
||||
}
|
||||
regions = append(regions, region)
|
||||
}
|
||||
|
||||
c.OMIRegions = 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: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
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testOMIConfig() *OMIConfig {
|
||||
return &OMIConfig{
|
||||
OMIName: "foo",
|
||||
}
|
||||
}
|
||||
|
||||
func getFakeAccessConfig(region string) *AccessConfig {
|
||||
c := testAccessConfig()
|
||||
c.RawRegion = region
|
||||
return c
|
||||
}
|
||||
|
||||
func TestOMIConfigPrepare_name(t *testing.T) {
|
||||
c := testOMIConfig()
|
||||
accessConf := testAccessConfig()
|
||||
if err := c.Prepare(accessConf, nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
|
||||
c.OMIName = ""
|
||||
if err := c.Prepare(accessConf, nil); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOMIConfigPrepare_regions(t *testing.T) {
|
||||
c := testOMIConfig()
|
||||
c.OMIRegions = nil
|
||||
|
||||
var errs []error
|
||||
var err error
|
||||
accessConf := testAccessConfig()
|
||||
mockConn := &mockOAPIClient{}
|
||||
if errs = c.prepareRegions(accessConf); len(errs) > 0 {
|
||||
t.Fatalf("shouldn't have err: %#v", errs)
|
||||
}
|
||||
|
||||
c.OMIRegions, err = listOAPIRegions(mockConn)
|
||||
if err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err.Error())
|
||||
}
|
||||
if errs = c.prepareRegions(accessConf); len(errs) > 0 {
|
||||
t.Fatalf("shouldn't have err: %#v", errs)
|
||||
}
|
||||
errs = errs[:0]
|
||||
|
||||
c.OMIRegions = []string{"us-east-1", "us-west-1", "us-east-1"}
|
||||
if errs = c.prepareRegions(accessConf); len(errs) > 0 {
|
||||
t.Fatalf("bad: %s", errs[0])
|
||||
}
|
||||
|
||||
expected := []string{"us-east-1", "us-west-1"}
|
||||
if !reflect.DeepEqual(c.OMIRegions, expected) {
|
||||
t.Fatalf("bad: %#v", c.OMIRegions)
|
||||
}
|
||||
|
||||
c.OMIRegions = []string{"custom"}
|
||||
if errs = c.prepareRegions(accessConf); len(errs) > 0 {
|
||||
t.Fatal("shouldn't have error")
|
||||
}
|
||||
|
||||
c.OMIRegions = []string{"us-east-1", "us-east-2", "us-west-1"}
|
||||
c.OMIRegionKMSKeyIDs = map[string]string{
|
||||
"us-east-1": "123-456-7890",
|
||||
"us-west-1": "789-012-3456",
|
||||
"us-east-2": "456-789-0123",
|
||||
}
|
||||
if errs = c.prepareRegions(accessConf); len(errs) > 0 {
|
||||
t.Fatal(fmt.Sprintf("shouldn't have error: %s", errs[0]))
|
||||
}
|
||||
|
||||
c.OMIRegions = []string{"us-east-1", "us-east-2", "us-west-1"}
|
||||
c.OMIRegionKMSKeyIDs = map[string]string{
|
||||
"us-east-1": "123-456-7890",
|
||||
"us-west-1": "789-012-3456",
|
||||
"us-east-2": "",
|
||||
}
|
||||
if errs = c.prepareRegions(accessConf); len(errs) > 0 {
|
||||
t.Fatal("should have passed; we are able to use default KMS key if not sharing")
|
||||
}
|
||||
|
||||
c.SnapshotUsers = []string{"user-foo", "user-bar"}
|
||||
c.OMIRegions = []string{"us-east-1", "us-east-2", "us-west-1"}
|
||||
c.OMIRegionKMSKeyIDs = map[string]string{
|
||||
"us-east-1": "123-456-7890",
|
||||
"us-west-1": "789-012-3456",
|
||||
"us-east-2": "",
|
||||
}
|
||||
if errs = c.prepareRegions(accessConf); len(errs) > 0 {
|
||||
t.Fatal("should have an error b/c can't use default KMS key if sharing")
|
||||
}
|
||||
|
||||
c.OMIRegions = []string{"us-east-1", "us-west-1"}
|
||||
c.OMIRegionKMSKeyIDs = map[string]string{
|
||||
"us-east-1": "123-456-7890",
|
||||
"us-west-1": "789-012-3456",
|
||||
"us-east-2": "456-789-0123",
|
||||
}
|
||||
if errs = c.prepareRegions(accessConf); len(errs) > 0 {
|
||||
t.Fatal("should have error b/c theres a region in the key map that isn't in omi_regions")
|
||||
}
|
||||
|
||||
c.OMIRegions = []string{"us-east-1", "us-west-1", "us-east-2"}
|
||||
c.OMIRegionKMSKeyIDs = map[string]string{
|
||||
"us-east-1": "123-456-7890",
|
||||
"us-west-1": "789-012-3456",
|
||||
}
|
||||
|
||||
if err := c.Prepare(accessConf, nil); err == nil {
|
||||
t.Fatal("should have error b/c theres a region in in omi_regions that isn't in the key map")
|
||||
}
|
||||
|
||||
c.SnapshotUsers = []string{"foo", "bar"}
|
||||
c.OMIKmsKeyId = "123-abc-456"
|
||||
c.OMIEncryptBootVolume = true
|
||||
c.OMIRegions = []string{"us-east-1", "us-west-1"}
|
||||
c.OMIRegionKMSKeyIDs = map[string]string{
|
||||
"us-east-1": "123-456-7890",
|
||||
"us-west-1": "",
|
||||
}
|
||||
if errs = c.prepareRegions(accessConf); len(errs) > 0 {
|
||||
t.Fatal("should have error b/c theres a region in in omi_regions that isn't in the key map")
|
||||
}
|
||||
|
||||
// allow rawregion to exist in omi_regions list.
|
||||
accessConf = getFakeAccessConfig("us-east-1")
|
||||
c.OMIRegions = []string{"us-east-1", "us-west-1", "us-east-2"}
|
||||
c.OMIRegionKMSKeyIDs = nil
|
||||
if errs = c.prepareRegions(accessConf); len(errs) > 0 {
|
||||
t.Fatal("should allow user to have the raw region in omi_regions")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestOMIConfigPrepare_Share_EncryptedBoot(t *testing.T) {
|
||||
c := testOMIConfig()
|
||||
c.OMIUsers = []string{"testAccountID"}
|
||||
c.OMIEncryptBootVolume = true
|
||||
|
||||
accessConf := testAccessConfig()
|
||||
|
||||
c.OMIKmsKeyId = ""
|
||||
if err := c.Prepare(accessConf, nil); err == nil {
|
||||
t.Fatal("shouldn't be able to share omi with encrypted boot volume")
|
||||
}
|
||||
|
||||
c.OMIKmsKeyId = "89c3fb9a-de87-4f2a-aedc-fddc5138193c"
|
||||
if err := c.Prepare(accessConf, nil); err == nil {
|
||||
t.Fatal("shouldn't be able to share omi with encrypted boot volume")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOMIConfigPrepare_ValidateKmsKey(t *testing.T) {
|
||||
c := testOMIConfig()
|
||||
c.OMIEncryptBootVolume = true
|
||||
|
||||
accessConf := testAccessConfig()
|
||||
|
||||
validCases := []string{
|
||||
"abcd1234-e567-890f-a12b-a123b4cd56ef",
|
||||
"alias/foo/bar",
|
||||
"arn:aws:kms:us-east-1:012345678910:key/abcd1234-a123-456a-a12b-a123b4cd56ef",
|
||||
"arn:aws:kms:us-east-1:012345678910:alias/foo/bar",
|
||||
}
|
||||
for _, validCase := range validCases {
|
||||
c.OMIKmsKeyId = validCase
|
||||
if err := c.Prepare(accessConf, nil); err != nil {
|
||||
t.Fatalf("%s should not have failed KMS key validation", validCase)
|
||||
}
|
||||
}
|
||||
|
||||
invalidCases := []string{
|
||||
"ABCD1234-e567-890f-a12b-a123b4cd56ef",
|
||||
"ghij1234-e567-890f-a12b-a123b4cd56ef",
|
||||
"ghij1234+e567_890f-a12b-a123b4cd56ef",
|
||||
"foo/bar",
|
||||
"arn:aws:kms:us-east-1:012345678910:foo/bar",
|
||||
}
|
||||
for _, invalidCase := range invalidCases {
|
||||
c.OMIKmsKeyId = invalidCase
|
||||
if err := c.Prepare(accessConf, nil); err == nil {
|
||||
t.Fatalf("%s should have failed KMS key validation", invalidCase)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestOMINameValidation(t *testing.T) {
|
||||
c := testOMIConfig()
|
||||
|
||||
accessConf := testAccessConfig()
|
||||
|
||||
c.OMIName = "aa"
|
||||
if err := c.Prepare(accessConf, nil); err == nil {
|
||||
t.Fatal("shouldn't be able to have an omi name with less than 3 characters")
|
||||
}
|
||||
|
||||
var longOmiName string
|
||||
for i := 0; i < 129; i++ {
|
||||
longOmiName += "a"
|
||||
}
|
||||
c.OMIName = longOmiName
|
||||
if err := c.Prepare(accessConf, nil); err == nil {
|
||||
t.Fatal("shouldn't be able to have an omi name with great than 128 characters")
|
||||
}
|
||||
|
||||
c.OMIName = "+aaa"
|
||||
if err := c.Prepare(accessConf, nil); err == nil {
|
||||
t.Fatal("shouldn't be able to have an omi name with invalid characters")
|
||||
}
|
||||
|
||||
c.OMIName = "fooBAR1()[] ./-'@_"
|
||||
if err := c.Prepare(accessConf, nil); err != nil {
|
||||
t.Fatal("should be able to use all of the allowed OMI characters")
|
||||
}
|
||||
|
||||
c.OMIName = `xyz-base-2017-04-05-1934`
|
||||
if err := c.Prepare(accessConf, nil); err != nil {
|
||||
t.Fatalf("expected `xyz-base-2017-04-05-1934` to pass validation.")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package common
|
||||
|
||||
import "bytes"
|
||||
|
||||
func isalphanumeric(b byte) bool {
|
||||
if '0' <= b && b <= '9' {
|
||||
return true
|
||||
}
|
||||
if 'a' <= b && b <= 'z' {
|
||||
return true
|
||||
}
|
||||
if 'A' <= b && b <= 'Z' {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func templateCleanOMIName(s string) string {
|
||||
allowed := []byte{'(', ')', '[', ']', ' ', '.', '/', '-', '\'', '@', '_'}
|
||||
b := []byte(s)
|
||||
newb := make([]byte, len(b))
|
||||
for i, c := range b {
|
||||
if isalphanumeric(c) || bytes.IndexByte(allowed, c) != -1 {
|
||||
newb[i] = c
|
||||
} else {
|
||||
newb[i] = '-'
|
||||
}
|
||||
}
|
||||
return string(newb[:])
|
||||
}
|
Loading…
Reference in New Issue