From f6372e8ac6d4d868cd2de62b3b51db2d4fab1b77 Mon Sep 17 00:00:00 2001 From: Marin Salinas Date: Tue, 29 Jan 2019 11:27:42 -0600 Subject: [PATCH] feature: add omi config to bsusurrogate config struct --- builder/osc/bsusurrogate/builder.go | 1 + builder/osc/common/omi_config.go | 171 +++++++++++++++++++ builder/osc/common/omi_config_test.go | 230 ++++++++++++++++++++++++++ builder/osc/common/template_funcs.go | 30 ++++ 4 files changed, 432 insertions(+) create mode 100644 builder/osc/common/omi_config.go create mode 100644 builder/osc/common/omi_config_test.go create mode 100644 builder/osc/common/template_funcs.go diff --git a/builder/osc/bsusurrogate/builder.go b/builder/osc/bsusurrogate/builder.go index 415b82e3b..3d86df9be 100644 --- a/builder/osc/bsusurrogate/builder.go +++ b/builder/osc/bsusurrogate/builder.go @@ -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 } diff --git a/builder/osc/common/omi_config.go b/builder/osc/common/omi_config.go new file mode 100644 index 000000000..11055c628 --- /dev/null +++ b/builder/osc/common/omi_config.go @@ -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 +} diff --git a/builder/osc/common/omi_config_test.go b/builder/osc/common/omi_config_test.go new file mode 100644 index 000000000..d52be2aae --- /dev/null +++ b/builder/osc/common/omi_config_test.go @@ -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.") + } + +} diff --git a/builder/osc/common/template_funcs.go b/builder/osc/common/template_funcs.go new file mode 100644 index 000000000..1f63f661f --- /dev/null +++ b/builder/osc/common/template_funcs.go @@ -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[:]) +}