diff --git a/builder/azure/arm/builder.go b/builder/azure/arm/builder.go index 0d85d3fc8..db344acb3 100644 --- a/builder/azure/arm/builder.go +++ b/builder/azure/arm/builder.go @@ -224,6 +224,7 @@ func (b *Builder) configureStateBag(stateBag multistep.StateBag) { stateBag.Put(constants.AuthorizedKey, b.config.sshAuthorizedKey) stateBag.Put(constants.PrivateKey, b.config.sshPrivateKey) + stateBag.Put(constants.ArmTags, &b.config.AzureTags) stateBag.Put(constants.ArmComputeName, b.config.tmpComputeName) stateBag.Put(constants.ArmDeploymentName, b.config.tmpDeploymentName) stateBag.Put(constants.ArmKeyVaultName, b.config.tmpKeyVaultName) diff --git a/builder/azure/arm/builder_test.go b/builder/azure/arm/builder_test.go index 9f0d26670..436ddc04c 100644 --- a/builder/azure/arm/builder_test.go +++ b/builder/azure/arm/builder_test.go @@ -4,8 +4,9 @@ package arm import ( - "github.com/mitchellh/packer/builder/azure/common/constants" "testing" + + "github.com/mitchellh/packer/builder/azure/common/constants" ) func TestStateBagShouldBePopulatedExpectedValues(t *testing.T) { @@ -19,6 +20,7 @@ func TestStateBagShouldBePopulatedExpectedValues(t *testing.T) { constants.AuthorizedKey, constants.PrivateKey, + constants.ArmTags, constants.ArmComputeName, constants.ArmDeploymentName, constants.ArmLocation, diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go index 93b7f7b59..88f60b2ff 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -70,8 +70,9 @@ type Config struct { VMSize string `mapstructure:"vm_size"` // Deployment - ResourceGroupName string `mapstructure:"resource_group_name"` - StorageAccount string `mapstructure:"storage_account"` + AzureTags map[string]*string `mapstructure:"azure_tags"` + ResourceGroupName string `mapstructure:"resource_group_name"` + StorageAccount string `mapstructure:"storage_account"` storageAccountBlobEndpoint string CloudEnvironmentName string `mapstructure:"cloud_environment_name"` cloudEnvironment *azure.Environment @@ -222,6 +223,7 @@ func newConfig(raws ...interface{}) (*Config, []string, error) { errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(c.ctx)...) assertRequiredParametersSet(&c, errs) + assertTagProperties(&c, errs) if errs != nil && len(errs.Errors) > 0 { return nil, nil, errs } @@ -349,6 +351,21 @@ func provideDefaultValues(c *Config) { } } +func assertTagProperties(c *Config, errs *packer.MultiError) { + if len(c.AzureTags) > 15 { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("a max of 15 tags are supported, but %d were provided", len(c.AzureTags))) + } + + for k, v := range c.AzureTags { + if len(k) > 512 { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("the tag name %q exceeds (%d) the 512 character limit", k, len(k))) + } + if len(*v) > 256 { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("the tag name %q exceeds (%d) the 256 character limit", v, len(*v))) + } + } +} + func assertRequiredParametersSet(c *Config, errs *packer.MultiError) { ///////////////////////////////////////////// // Authentication via OAUTH diff --git a/builder/azure/arm/config_test.go b/builder/azure/arm/config_test.go index 9e5a89245..f54f10c2c 100644 --- a/builder/azure/arm/config_test.go +++ b/builder/azure/arm/config_test.go @@ -4,6 +4,7 @@ package arm import ( + "fmt" "strings" "testing" "time" @@ -32,16 +33,16 @@ func TestConfigShouldProvideReasonableDefaultValues(t *testing.T) { c, _, err := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) if err != nil { - t.Errorf("Expected configuration creation to succeed, but it failed!\n") + t.Error("Expected configuration creation to succeed, but it failed!\n") t.Fatalf(" errors: %s\n", err) } if c.UserName == "" { - t.Errorf("Expected 'UserName' to be populated, but it was empty!") + t.Error("Expected 'UserName' to be populated, but it was empty!") } if c.VMSize == "" { - t.Errorf("Expected 'VMSize' to be populated, but it was empty!") + t.Error("Expected 'VMSize' to be populated, but it was empty!") } if c.ObjectID != "" { @@ -283,7 +284,7 @@ func TestUserShouldProvideRequiredValues(t *testing.T) { // Ensure we can successfully create a config. _, _, err := newConfig(builderValues, getPackerConfiguration()) if err != nil { - t.Errorf("Expected configuration creation to succeed, but it failed!\n") + t.Error("Expected configuration creation to succeed, but it failed!\n") t.Fatalf(" -> %+v\n", builderValues) } @@ -294,7 +295,7 @@ func TestUserShouldProvideRequiredValues(t *testing.T) { _, _, err := newConfig(builderValues, getPackerConfiguration()) if err == nil { - t.Errorf("Expected configuration creation to fail, but it succeeded!\n") + t.Error("Expected configuration creation to fail, but it succeeded!\n") t.Fatalf(" -> %+v\n", builderValues) } @@ -374,7 +375,7 @@ func TestWinRMConfigShouldSetRoundTripDecorator(t *testing.T) { } if c.Comm.WinRMTransportDecorator == nil { - t.Errorf("Expected WinRMTransportDecorator to be set, but it was nil") + t.Error("Expected WinRMTransportDecorator to be set, but it was nil") } } @@ -425,10 +426,10 @@ func TestUseDeviceLoginIsDisabledForWindows(t *testing.T) { } if !strings.Contains(err.Error(), "client_id must be specified") { - t.Errorf("Expected to find error for 'client_id must be specified") + t.Error("Expected to find error for 'client_id must be specified") } if !strings.Contains(err.Error(), "client_secret must be specified") { - t.Errorf("Expected to find error for 'client_secret must be specified") + t.Error("Expected to find error for 'client_secret must be specified") } } @@ -533,6 +534,145 @@ func TestConfigShouldRejectMalformedCaptureContainerName(t *testing.T) { } } +func TestConfigShouldAcceptTags(t *testing.T) { + config := map[string]interface{}{ + "capture_name_prefix": "ignore", + "capture_container_name": "ignore", + "image_offer": "ignore", + "image_publisher": "ignore", + "image_sku": "ignore", + "location": "ignore", + "storage_account": "ignore", + "resource_group_name": "ignore", + "subscription_id": "ignore", + "communicator": "none", + // Does not matter for this test case, just pick one. + "os_type": constants.Target_Linux, + "azure_tags": map[string]string{ + "tag01": "value01", + "tag02": "value02", + }, + } + + c, _, err := newConfig(config, getPackerConfiguration()) + + if err != nil { + t.Fatal(err) + } + + if len(c.AzureTags) != 2 { + t.Fatalf("expected to find 2 tags, but got %d", len(c.AzureTags)) + } + + if _, ok := c.AzureTags["tag01"]; !ok { + t.Error("expected to find key=\"tag01\", but did not") + } + if _, ok := c.AzureTags["tag02"]; !ok { + t.Error("expected to find key=\"tag02\", but did not") + } + + value := c.AzureTags["tag01"] + if *value != "value01" { + t.Errorf("expected AzureTags[\"tag01\"] to have value \"value01\", but got %q", value) + } + + value = c.AzureTags["tag02"] + if *value != "value02" { + t.Errorf("expected AzureTags[\"tag02\"] to have value \"value02\", but got %q", value) + } +} + +func TestConfigShouldRejectTagsInExcessOf15AcceptTags(t *testing.T) { + tooManyTags := map[string]string{} + for i := 0; i < 16; i++ { + tooManyTags[fmt.Sprintf("tag%.2d", i)] = "ignored" + } + + config := map[string]interface{}{ + "capture_name_prefix": "ignore", + "capture_container_name": "ignore", + "image_offer": "ignore", + "image_publisher": "ignore", + "image_sku": "ignore", + "location": "ignore", + "storage_account": "ignore", + "resource_group_name": "ignore", + "subscription_id": "ignore", + "communicator": "none", + // Does not matter for this test case, just pick one. + "os_type": constants.Target_Linux, + "azure_tags": tooManyTags, + } + + _, _, err := newConfig(config, getPackerConfiguration()) + + if err == nil { + t.Fatal("expected config to reject based on an excessive amount of tags (> 15)") + } +} + +func TestConfigShouldRejectExcessiveTagNameLength(t *testing.T) { + nameTooLong := make([]byte, 513) + for i := range nameTooLong { + nameTooLong[i] = 'a' + } + + tags := map[string]string{} + tags[string(nameTooLong)] = "ignored" + + config := map[string]interface{}{ + "capture_name_prefix": "ignore", + "capture_container_name": "ignore", + "image_offer": "ignore", + "image_publisher": "ignore", + "image_sku": "ignore", + "location": "ignore", + "storage_account": "ignore", + "resource_group_name": "ignore", + "subscription_id": "ignore", + "communicator": "none", + // Does not matter for this test case, just pick one. + "os_type": constants.Target_Linux, + "azure_tags": tags, + } + + _, _, err := newConfig(config, getPackerConfiguration()) + if err == nil { + t.Fatal("expected config to reject tag name based on length (> 512)") + } +} + +func TestConfigShouldRejectExcessiveTagValueLength(t *testing.T) { + valueTooLong := make([]byte, 257) + for i := range valueTooLong { + valueTooLong[i] = 'a' + } + + tags := map[string]string{} + tags["tag01"] = string(valueTooLong) + + config := map[string]interface{}{ + "capture_name_prefix": "ignore", + "capture_container_name": "ignore", + "image_offer": "ignore", + "image_publisher": "ignore", + "image_sku": "ignore", + "location": "ignore", + "storage_account": "ignore", + "resource_group_name": "ignore", + "subscription_id": "ignore", + "communicator": "none", + // Does not matter for this test case, just pick one. + "os_type": constants.Target_Linux, + "azure_tags": tags, + } + + _, _, err := newConfig(config, getPackerConfiguration()) + if err == nil { + t.Fatal("expected config to reject tag value based on length (> 256)") + } +} + func getArmBuilderConfiguration() map[string]string { m := make(map[string]string) for _, v := range requiredConfigValues { diff --git a/builder/azure/arm/step_create_resource_group.go b/builder/azure/arm/step_create_resource_group.go index 4d32c4091..9295ff36d 100644 --- a/builder/azure/arm/step_create_resource_group.go +++ b/builder/azure/arm/step_create_resource_group.go @@ -14,7 +14,7 @@ import ( type StepCreateResourceGroup struct { client *AzureClient - create func(resourceGroupName string, location string) error + create func(resourceGroupName string, location string, tags *map[string]*string) error say func(message string) error func(e error) } @@ -30,9 +30,10 @@ func NewStepCreateResourceGroup(client *AzureClient, ui packer.Ui) *StepCreateRe return step } -func (s *StepCreateResourceGroup) createResourceGroup(resourceGroupName string, location string) error { +func (s *StepCreateResourceGroup) createResourceGroup(resourceGroupName string, location string, tags *map[string]*string) error { _, err := s.client.GroupsClient.CreateOrUpdate(resourceGroupName, resources.ResourceGroup{ Location: &location, + Tags: tags, }) return err @@ -43,11 +44,16 @@ func (s *StepCreateResourceGroup) Run(state multistep.StateBag) multistep.StepAc var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) var location = state.Get(constants.ArmLocation).(string) + var tags = state.Get(constants.ArmTags).(*map[string]*string) s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName)) s.say(fmt.Sprintf(" -> Location : '%s'", location)) + s.say(fmt.Sprintf(" -> Tags :")) + for k, v := range *tags { + s.say(fmt.Sprintf(" ->> %s : %s", k, *v)) + } - err := s.create(resourceGroupName, location) + err := s.create(resourceGroupName, location, tags) if err == nil { state.Put(constants.ArmIsResourceGroupCreated, true) } diff --git a/builder/azure/arm/step_create_resource_group_test.go b/builder/azure/arm/step_create_resource_group_test.go index ff53e59df..affab4a04 100644 --- a/builder/azure/arm/step_create_resource_group_test.go +++ b/builder/azure/arm/step_create_resource_group_test.go @@ -13,7 +13,7 @@ import ( func TestStepCreateResourceGroupShouldFailIfCreateFails(t *testing.T) { var testSubject = &StepCreateResourceGroup{ - create: func(string, string) error { return fmt.Errorf("!! Unit Test FAIL !!") }, + create: func(string, string, *map[string]*string) error { return fmt.Errorf("!! Unit Test FAIL !!") }, say: func(message string) {}, error: func(e error) {}, } @@ -32,7 +32,7 @@ func TestStepCreateResourceGroupShouldFailIfCreateFails(t *testing.T) { func TestStepCreateResourceGroupShouldPassIfCreatePasses(t *testing.T) { var testSubject = &StepCreateResourceGroup{ - create: func(string, string) error { return nil }, + create: func(string, string, *map[string]*string) error { return nil }, say: func(message string) {}, error: func(e error) {}, } @@ -52,11 +52,13 @@ func TestStepCreateResourceGroupShouldPassIfCreatePasses(t *testing.T) { func TestStepCreateResourceGroupShouldTakeStepArgumentsFromStateBag(t *testing.T) { var actualResourceGroupName string var actualLocation string + var actualTags *map[string]*string var testSubject = &StepCreateResourceGroup{ - create: func(resourceGroupName string, location string) error { + create: func(resourceGroupName string, location string, tags *map[string]*string) error { actualResourceGroupName = resourceGroupName actualLocation = location + actualTags = tags return nil }, say: func(message string) {}, @@ -70,8 +72,9 @@ func TestStepCreateResourceGroupShouldTakeStepArgumentsFromStateBag(t *testing.T t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) } - var expectedLocation = stateBag.Get(constants.ArmLocation).(string) var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string) + var expectedLocation = stateBag.Get(constants.ArmLocation).(string) + var expectedTags = stateBag.Get(constants.ArmTags).(*map[string]*string) if actualResourceGroupName != expectedResourceGroupName { t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") @@ -81,6 +84,10 @@ func TestStepCreateResourceGroupShouldTakeStepArgumentsFromStateBag(t *testing.T t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") } + if len(*expectedTags) != len(*actualTags) && *(*expectedTags)["tag01"] != *(*actualTags)["tag01"] { + t.Fatal("Expected the step to source 'constants.ArmTags' from the state bag, but it did not.") + } + _, ok := stateBag.GetOk(constants.ArmIsResourceGroupCreated) if !ok { t.Fatal("Expected the step to add item to stateBag['constants.ArmIsResourceGroupCreated'], but it did not.") @@ -93,5 +100,12 @@ func createTestStateBagStepCreateResourceGroup() multistep.StateBag { stateBag.Put(constants.ArmLocation, "Unit Test: Location") stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName") + value := "Unit Test: Tags" + tags := map[string]*string{ + "tag01": &value, + } + + stateBag.Put(constants.ArmTags, &tags) + return stateBag } diff --git a/builder/azure/arm/template.go b/builder/azure/arm/template.go deleted file mode 100644 index 66dfe006f..000000000 --- a/builder/azure/arm/template.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See the LICENSE file in builder/azure for license information. - -package arm - -// See https://github.com/Azure/azure-quickstart-templates for a extensive list of templates. - -// Template to deploy a KeyVault. -// -// This template is still hard-coded unlike the ARM templates used for VMs for -// a couple of reasons. -// -// 1. The SDK defines no types for a Key Vault -// 2. The Key Vault template is relatively simple, and is static. -// -const KeyVault = `{ - "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", - "contentVersion": "1.0.0.0", - "parameters": { - "keyVaultName": { - "type": "string" - }, - "keyVaultSecretValue": { - "type": "securestring" - }, - "objectId": { - "type": "string" - }, - "tenantId": { - "type": "string" - } - }, - "variables": { - "apiVersion": "2015-06-01", - "location": "[resourceGroup().location]", - "keyVaultSecretName": "packerKeyVaultSecret" - }, - "resources": [ - { - "apiVersion": "[variables('apiVersion')]", - "type": "Microsoft.KeyVault/vaults", - "name": "[parameters('keyVaultName')]", - "location": "[variables('location')]", - "properties": { - "enabledForDeployment": "true", - "enabledForTemplateDeployment": "true", - "tenantId": "[parameters('tenantId')]", - "accessPolicies": [ - { - "tenantId": "[parameters('tenantId')]", - "objectId": "[parameters('objectId')]", - "permissions": { - "keys": [ "all" ], - "secrets": [ "all" ] - } - } - ], - "sku": { - "name": "standard", - "family": "A" - } - }, - "resources": [ - { - "apiVersion": "[variables('apiVersion')]", - "type": "secrets", - "name": "[variables('keyVaultSecretName')]", - "dependsOn": [ - "[concat('Microsoft.KeyVault/vaults/', parameters('keyVaultName'))]" - ], - "properties": { - "value": "[parameters('keyVaultSecretValue')]" - } - } - ] - } - ] -}` diff --git a/builder/azure/arm/template_factory.go b/builder/azure/arm/template_factory.go index 31faa797f..887b39f50 100644 --- a/builder/azure/arm/template_factory.go +++ b/builder/azure/arm/template_factory.go @@ -20,7 +20,11 @@ func GetKeyVaultDeployment(config *Config) (*resources.Deployment, error) { TenantId: &template.TemplateParameter{Value: config.TenantID}, } - return createDeploymentParameters(KeyVault, params) + builder, _ := template.NewTemplateBuilder(template.KeyVault) + builder.SetTags(&config.AzureTags) + + doc, _ := builder.ToJSON() + return createDeploymentParameters(*doc, params) } func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error) { @@ -34,7 +38,7 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error) VMName: &template.TemplateParameter{Value: config.tmpComputeName}, } - builder, _ := template.NewTemplateBuilder() + builder, _ := template.NewTemplateBuilder(template.BasicTemplate) osType := compute.Linux switch config.OSType { @@ -58,6 +62,7 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error) config.VirtualNetworkSubnetName) } + builder.SetTags(&config.AzureTags) doc, _ := builder.ToJSON() return createDeploymentParameters(*doc, params) } diff --git a/builder/azure/arm/template_factory_test.TestKeyVaultDeployment03.approved.json b/builder/azure/arm/template_factory_test.TestKeyVaultDeployment03.approved.json index 6d4049800..4d846ad4a 100644 --- a/builder/azure/arm/template_factory_test.TestKeyVaultDeployment03.approved.json +++ b/builder/azure/arm/template_factory_test.TestKeyVaultDeployment03.approved.json @@ -56,6 +56,11 @@ "type": "secrets" } ], + "tags": { + "tag01": "value01", + "tag02": "value02", + "tag03": "value03" + }, "type": "Microsoft.KeyVault/vaults" } ], diff --git a/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment06.approved.json b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment06.approved.json new file mode 100644 index 000000000..a15c63f36 --- /dev/null +++ b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment06.approved.json @@ -0,0 +1,179 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "adminPassword": { + "type": "string" + }, + "adminUsername": { + "type": "string" + }, + "dnsNameForPublicIP": { + "type": "string" + }, + "osDiskName": { + "type": "string" + }, + "storageAccountBlobEndpoint": { + "type": "string" + }, + "vmName": { + "type": "string" + }, + "vmSize": { + "type": "string" + } + }, + "resources": [ + { + "apiVersion": "[variables('apiVersion')]", + "location": "[variables('location')]", + "name": "[variables('publicIPAddressName')]", + "properties": { + "dnsSettings": { + "domainNameLabel": "[parameters('dnsNameForPublicIP')]" + }, + "publicIPAllocationMethod": "[variables('publicIPAddressType')]" + }, + "tags": { + "tag01": "value01", + "tag02": "value02", + "tag03": "value03" + }, + "type": "Microsoft.Network/publicIPAddresses" + }, + { + "apiVersion": "[variables('apiVersion')]", + "location": "[variables('location')]", + "name": "[variables('virtualNetworkName')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('addressPrefix')]" + ] + }, + "subnets": [ + { + "name": "[variables('subnetName')]", + "properties": { + "addressPrefix": "[variables('subnetAddressPrefix')]" + } + } + ] + }, + "tags": { + "tag01": "value01", + "tag02": "value02", + "tag03": "value03" + }, + "type": "Microsoft.Network/virtualNetworks" + }, + { + "apiVersion": "[variables('apiVersion')]", + "dependsOn": [ + "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]", + "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" + ], + "location": "[variables('location')]", + "name": "[variables('nicName')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]" + }, + "subnet": { + "id": "[variables('subnetRef')]" + } + } + } + ] + }, + "tags": { + "tag01": "value01", + "tag02": "value02", + "tag03": "value03" + }, + "type": "Microsoft.Network/networkInterfaces" + }, + { + "apiVersion": "[variables('apiVersion')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + ], + "location": "[variables('location')]", + "name": "[parameters('vmName')]", + "properties": { + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": false + } + }, + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + } + ] + }, + "osProfile": { + "adminPassword": "[parameters('adminPassword')]", + "adminUsername": "[parameters('adminUsername')]", + "computerName": "[parameters('vmName')]", + "linuxConfiguration": { + "ssh": { + "publicKeys": [ + { + "keyData": "", + "path": "[variables('sshKeyPath')]" + } + ] + } + } + }, + "storageProfile": { + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "image": { + "uri": "https://localhost/custom.vhd" + }, + "name": "osdisk", + "osType": "Linux", + "vhd": { + "uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]" + } + } + } + }, + "tags": { + "tag01": "value01", + "tag02": "value02", + "tag03": "value03" + }, + "type": "Microsoft.Compute/virtualMachines" + } + ], + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2015-06-15", + "location": "[resourceGroup().location]", + "nicName": "packerNic", + "publicIPAddressName": "packerPublicIP", + "publicIPAddressType": "Dynamic", + "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", + "subnetAddressPrefix": "10.0.0.0/24", + "subnetName": "packerSubnet", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "packerNetwork", + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } +} \ No newline at end of file diff --git a/builder/azure/arm/template_factory_test.go b/builder/azure/arm/template_factory_test.go index 708d7f379..0e78a86ab 100644 --- a/builder/azure/arm/template_factory_test.go +++ b/builder/azure/arm/template_factory_test.go @@ -23,19 +23,19 @@ func TestVirtualMachineDeployment00(t *testing.T) { } if deployment.Properties.ParametersLink != nil { - t.Errorf("Expected the ParametersLink to be nil!") + t.Error("Expected the ParametersLink to be nil!") } if deployment.Properties.TemplateLink != nil { - t.Errorf("Expected the TemplateLink to be nil!") + t.Error("Expected the TemplateLink to be nil!") } if deployment.Properties.Parameters == nil { - t.Errorf("Expected the Parameters to not be nil!") + t.Error("Expected the Parameters to not be nil!") } if deployment.Properties.Template == nil { - t.Errorf("Expected the Template to not be nil!") + t.Error("Expected the Template to not be nil!") } } @@ -177,6 +177,41 @@ func TestVirtualMachineDeployment05(t *testing.T) { } } +// Verify that tags are properly applied to every resource +func TestVirtualMachineDeployment06(t *testing.T) { + config := map[string]interface{}{ + "capture_name_prefix": "ignore", + "capture_container_name": "ignore", + "location": "ignore", + "image_url": "https://localhost/custom.vhd", + "resource_group_name": "ignore", + "storage_account": "ignore", + "subscription_id": "ignore", + "os_type": constants.Target_Linux, + "communicator": "none", + "azure_tags": map[string]string{ + "tag01": "value01", + "tag02": "value02", + "tag03": "value03", + }, + } + + c, _, err := newConfig(config, getPackerConfiguration()) + if err != nil { + t.Fatal(err) + } + + deployment, err := GetVirtualMachineDeployment(c) + if err != nil { + t.Fatal(err) + } + + err = approvaltests.VerifyJSONStruct(t, deployment.Properties.Template) + if err != nil { + t.Fatal(err) + } +} + // Ensure the link values are not set, and the concrete values are set. func TestKeyVaultDeployment00(t *testing.T) { c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) @@ -190,19 +225,19 @@ func TestKeyVaultDeployment00(t *testing.T) { } if deployment.Properties.ParametersLink != nil { - t.Errorf("Expected the ParametersLink to be nil!") + t.Error("Expected the ParametersLink to be nil!") } if deployment.Properties.TemplateLink != nil { - t.Errorf("Expected the TemplateLink to be nil!") + t.Error("Expected the TemplateLink to be nil!") } if deployment.Properties.Parameters == nil { - t.Errorf("Expected the Parameters to not be nil!") + t.Error("Expected the Parameters to not be nil!") } if deployment.Properties.Template == nil { - t.Errorf("Expected the Template to not be nil!") + t.Error("Expected the Template to not be nil!") } } @@ -254,9 +289,17 @@ func TestKeyVaultDeployment02(t *testing.T) { } } -// Ensure the KeyVault template is correct. +// Ensure the KeyVault template is correct when tags are supplied. func TestKeyVaultDeployment03(t *testing.T) { - c, _, _ := newConfig(getArmBuilderConfigurationWithWindows(), getPackerConfiguration()) + tags := map[string]interface{}{ + "azure_tags": map[string]string{ + "tag01": "value01", + "tag02": "value02", + "tag03": "value03", + }, + } + + c, _, _ := newConfig(tags, getArmBuilderConfigurationWithWindows(), getPackerConfiguration()) deployment, err := GetKeyVaultDeployment(c) if err != nil { t.Fatal(err) diff --git a/builder/azure/common/constants/stateBag.go b/builder/azure/common/constants/stateBag.go index 0955994e6..c4dba0acc 100644 --- a/builder/azure/common/constants/stateBag.go +++ b/builder/azure/common/constants/stateBag.go @@ -26,5 +26,6 @@ const ( ArmResourceGroupName string = "arm.ResourceGroupName" ArmIsResourceGroupCreated string = "arm.IsResourceGroupCreated" ArmStorageAccountName string = "arm.StorageAccountName" + ArmTags string = "arm.Tags" ArmVirtualMachineCaptureParameters string = "arm.VirtualMachineCaptureParameters" ) diff --git a/builder/azure/common/template/template.go b/builder/azure/common/template/template.go index 24f643669..c7892e2cf 100644 --- a/builder/azure/common/template/template.go +++ b/builder/azure/common/template/template.go @@ -3,7 +3,6 @@ package template import ( "github.com/Azure/azure-sdk-for-go/arm/compute" "github.com/Azure/azure-sdk-for-go/arm/network" - //"github.com/Azure/azure-sdk-for-go/arm/resources/resources" ) ///////////////////////////////////////////////// @@ -26,25 +25,49 @@ type Parameters struct { ///////////////////////////////////////////////// // Template > Resource type Resource struct { - ApiVersion *string `json:"apiVersion"` - Name *string `json:"name"` - Type *string `json:"type"` - Location *string `json:"location"` - DependsOn *[]string `json:"dependsOn,omitempty"` - Properties *Properties `json:"properties,omitempty"` + ApiVersion *string `json:"apiVersion"` + Name *string `json:"name"` + Type *string `json:"type"` + Location *string `json:"location,omitempty"` + DependsOn *[]string `json:"dependsOn,omitempty"` + Properties *Properties `json:"properties,omitempty"` + Tags *map[string]*string `json:"tags,omitempty"` + Resources *[]Resource `json:"resources,omitempty"` } ///////////////////////////////////////////////// // Template > Resource > Properties type Properties struct { - AddressSpace *network.AddressSpace `json:"addressSpace,omitempty"` - DiagnosticsProfile *compute.DiagnosticsProfile `json:"diagnosticsProfile,omitempty"` - DNSSettings *network.PublicIPAddressDNSSettings `json:"dnsSettings,omitempty"` - HardwareProfile *compute.HardwareProfile `json:"hardwareProfile,omitempty"` - IPConfigurations *[]network.IPConfiguration `json:"ipConfigurations,omitempty"` - NetworkProfile *compute.NetworkProfile `json:"networkProfile,omitempty"` - OsProfile *compute.OSProfile `json:"osProfile,omitempty"` - PublicIPAllocatedMethod *network.IPAllocationMethod `json:"publicIPAllocationMethod,omitempty"` - StorageProfile *compute.StorageProfile `json:"storageProfile,omitempty"` - Subnets *[]network.Subnet `json:"subnets,omitempty"` + AccessPolicies *[]AccessPolicies `json:"accessPolicies,omitempty"` + AddressSpace *network.AddressSpace `json:"addressSpace,omitempty"` + DiagnosticsProfile *compute.DiagnosticsProfile `json:"diagnosticsProfile,omitempty"` + DNSSettings *network.PublicIPAddressDNSSettings `json:"dnsSettings,omitempty"` + EnabledForDeployment *string `json:"enabledForDeployment,omitempty"` + EnabledForTemplateDeployment *string `json:"enabledForTemplateDeployment,omitempty"` + HardwareProfile *compute.HardwareProfile `json:"hardwareProfile,omitempty"` + IPConfigurations *[]network.IPConfiguration `json:"ipConfigurations,omitempty"` + NetworkProfile *compute.NetworkProfile `json:"networkProfile,omitempty"` + OsProfile *compute.OSProfile `json:"osProfile,omitempty"` + PublicIPAllocatedMethod *network.IPAllocationMethod `json:"publicIPAllocationMethod,omitempty"` + Sku *Sku `json:"sku,omitempty"` + StorageProfile *compute.StorageProfile `json:"storageProfile,omitempty"` + Subnets *[]network.Subnet `json:"subnets,omitempty"` + TenantId *string `json:"tenantId,omitempty"` + Value *string `json:"value,omitempty"` +} + +type AccessPolicies struct { + ObjectId *string `json:"objectId,omitempty"` + TenantId *string `json:"tenantId,omitempty"` + Permissions *Permissions `json:"permissions,omitempty"` +} + +type Permissions struct { + Keys *[]string `json:"keys,omitempty"` + Secrets *[]string `json:"secrets,omitempty"` +} + +type Sku struct { + Family *string `json:"family,omitempty"` + Name *string `json:"name,omitempty"` } diff --git a/builder/azure/common/template/template_builder.go b/builder/azure/common/template/template_builder.go index c8cecfcab..db77e8328 100644 --- a/builder/azure/common/template/template_builder.go +++ b/builder/azure/common/template/template_builder.go @@ -26,10 +26,10 @@ type TemplateBuilder struct { template *Template } -func NewTemplateBuilder() (*TemplateBuilder, error) { +func NewTemplateBuilder(template string) (*TemplateBuilder, error) { var t Template - err := json.Unmarshal([]byte(basicTemplate), &t) + err := json.Unmarshal([]byte(template), &t) if err != nil { return nil, err } @@ -150,6 +150,17 @@ func (s *TemplateBuilder) SetVirtualNetwork(virtualNetworkResourceGroup, virtual return nil } +func (s *TemplateBuilder) SetTags(tags *map[string]*string) error { + if tags == nil || len(*tags) == 0 { + return nil + } + + for i := range *s.template.Resources { + (*s.template.Resources)[i].Tags = tags + } + return nil +} + func (s *TemplateBuilder) ToJSON() (*string, error) { bs, err := json.MarshalIndent(s.template, jsonPrefix, jsonIndent) @@ -210,7 +221,81 @@ func (s *TemplateBuilder) deleteResourceDependency(resource *Resource, predicate *resource.DependsOn = deps } -const basicTemplate = `{ +// See https://github.com/Azure/azure-quickstart-templates for a extensive list of templates. + +// Template to deploy a KeyVault. +// +// This template is still hard-coded unlike the ARM templates used for VMs for +// a couple of reasons. +// +// 1. The SDK defines no types for a Key Vault +// 2. The Key Vault template is relatively simple, and is static. +// +const KeyVault = `{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "keyVaultName": { + "type": "string" + }, + "keyVaultSecretValue": { + "type": "securestring" + }, + "objectId": { + "type": "string" + }, + "tenantId": { + "type": "string" + } + }, + "variables": { + "apiVersion": "2015-06-01", + "location": "[resourceGroup().location]", + "keyVaultSecretName": "packerKeyVaultSecret" + }, + "resources": [ + { + "apiVersion": "[variables('apiVersion')]", + "type": "Microsoft.KeyVault/vaults", + "name": "[parameters('keyVaultName')]", + "location": "[variables('location')]", + "properties": { + "enabledForDeployment": "true", + "enabledForTemplateDeployment": "true", + "tenantId": "[parameters('tenantId')]", + "accessPolicies": [ + { + "tenantId": "[parameters('tenantId')]", + "objectId": "[parameters('objectId')]", + "permissions": { + "keys": [ "all" ], + "secrets": [ "all" ] + } + } + ], + "sku": { + "name": "standard", + "family": "A" + } + }, + "resources": [ + { + "apiVersion": "[variables('apiVersion')]", + "type": "secrets", + "name": "[variables('keyVaultSecretName')]", + "dependsOn": [ + "[concat('Microsoft.KeyVault/vaults/', parameters('keyVaultName'))]" + ], + "properties": { + "value": "[parameters('keyVaultSecretValue')]" + } + } + ] + } + ] +}` + +const BasicTemplate = `{ "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", "contentVersion": "1.0.0.0", "parameters": { diff --git a/builder/azure/common/template/template_builder_test.go b/builder/azure/common/template/template_builder_test.go index 1097eab8b..10c94df7e 100644 --- a/builder/azure/common/template/template_builder_test.go +++ b/builder/azure/common/template/template_builder_test.go @@ -10,7 +10,7 @@ import ( // Ensure that a Linux template is configured as expected. // * Include SSH configuration: authorized key, and key path. func TestBuildLinux00(t *testing.T) { - testSubject, err := NewTemplateBuilder() + testSubject, err := NewTemplateBuilder(BasicTemplate) if err != nil { t.Fatal(err) } @@ -38,7 +38,7 @@ func TestBuildLinux00(t *testing.T) { // Ensure that a user can specify a custom VHD when building a Linux template. func TestBuildLinux01(t *testing.T) { - testSubject, err := NewTemplateBuilder() + testSubject, err := NewTemplateBuilder(BasicTemplate) if err != nil { t.Fatal(err) } @@ -66,7 +66,7 @@ func TestBuildLinux01(t *testing.T) { // Ensure that a user can specify an existing Virtual Network func TestBuildLinux02(t *testing.T) { - testSubject, err := NewTemplateBuilder() + testSubject, err := NewTemplateBuilder(BasicTemplate) if err != nil { t.Fatal(err) } @@ -94,7 +94,7 @@ func TestBuildLinux02(t *testing.T) { // * Include WinRM configuration. // * Include KeyVault configuration, which is needed for WinRM. func TestBuildWindows00(t *testing.T) { - testSubject, err := NewTemplateBuilder() + testSubject, err := NewTemplateBuilder(BasicTemplate) if err != nil { t.Fatal(err) } diff --git a/examples/azure/ubuntu.json b/examples/azure/ubuntu.json index eae2f24c7..e90ab8ede 100644 --- a/examples/azure/ubuntu.json +++ b/examples/azure/ubuntu.json @@ -23,6 +23,11 @@ "image_offer": "UbuntuServer", "image_sku": "16.04.0-LTS", + "azure_tags": { + "dept": "engineering", + "task": "image deployment" + }, + "location": "West US", "vm_size": "Standard_A2" }], diff --git a/website/source/docs/builders/azure.html.md b/website/source/docs/builders/azure.html.md index 71b6006db..5e5a12355 100644 --- a/website/source/docs/builders/azure.html.md +++ b/website/source/docs/builders/azure.html.md @@ -57,6 +57,10 @@ builder. ### Optional: +- `azure_tags` (object of name/value strings) - the user can define up to 15 tags. Tag names cannot exceed 512 + characters, and tag values cannot exceed 256 characters. Tags are applied to every resource deployed by a Packer + build, i.e. Resource Group, VM, NIC, VNET, Public IP, KeyVault, etc. + - `cloud_environment_name` (string) One of `Public`, `China`, `Germany`, or `USGovernment`. Defaults to `Public`. Long forms such as `USGovernmentCloud` and `AzureUSGovernmentCloud` are also supported. @@ -70,7 +74,8 @@ builder. - `image_url` (string) Specify a custom VHD to use. If this value is set, do not set image_publisher, image_offer, image_sku, or image_version. -- `tenant_id` (string) The account identifier with which your `client_id` and `subscription_id` are associated. If not specified, `tenant_id` will be looked up using `subscription_id`. +- `tenant_id` (string) The account identifier with which your `client_id` and `subscription_id` are associated. If not + specified, `tenant_id` will be looked up using `subscription_id`. - `object_id` (string) Specify an OAuth Object ID to protect WinRM certificates created at runtime. This variable is required when creating images based on @@ -125,6 +130,10 @@ Here is a basic example for Azure. "image_publisher": "Canonical", "image_offer": "UbuntuServer", "image_sku": "14.04.4-LTS", + + "azure_tags": { + "dept": "engineering" + }, "location": "West US", "vm_size": "Standard_A2"