Merge pull request #3764 from boumenot/pr-azure-tags

azure: tag all resources
This commit is contained in:
Christopher Boumenot 2016-08-02 11:30:39 -07:00 committed by GitHub
commit 6563bbc854
17 changed files with 590 additions and 133 deletions

View File

@ -224,6 +224,7 @@ func (b *Builder) configureStateBag(stateBag multistep.StateBag) {
stateBag.Put(constants.AuthorizedKey, b.config.sshAuthorizedKey) stateBag.Put(constants.AuthorizedKey, b.config.sshAuthorizedKey)
stateBag.Put(constants.PrivateKey, b.config.sshPrivateKey) stateBag.Put(constants.PrivateKey, b.config.sshPrivateKey)
stateBag.Put(constants.ArmTags, &b.config.AzureTags)
stateBag.Put(constants.ArmComputeName, b.config.tmpComputeName) stateBag.Put(constants.ArmComputeName, b.config.tmpComputeName)
stateBag.Put(constants.ArmDeploymentName, b.config.tmpDeploymentName) stateBag.Put(constants.ArmDeploymentName, b.config.tmpDeploymentName)
stateBag.Put(constants.ArmKeyVaultName, b.config.tmpKeyVaultName) stateBag.Put(constants.ArmKeyVaultName, b.config.tmpKeyVaultName)

View File

@ -4,8 +4,9 @@
package arm package arm
import ( import (
"github.com/mitchellh/packer/builder/azure/common/constants"
"testing" "testing"
"github.com/mitchellh/packer/builder/azure/common/constants"
) )
func TestStateBagShouldBePopulatedExpectedValues(t *testing.T) { func TestStateBagShouldBePopulatedExpectedValues(t *testing.T) {
@ -19,6 +20,7 @@ func TestStateBagShouldBePopulatedExpectedValues(t *testing.T) {
constants.AuthorizedKey, constants.AuthorizedKey,
constants.PrivateKey, constants.PrivateKey,
constants.ArmTags,
constants.ArmComputeName, constants.ArmComputeName,
constants.ArmDeploymentName, constants.ArmDeploymentName,
constants.ArmLocation, constants.ArmLocation,

View File

@ -70,8 +70,9 @@ type Config struct {
VMSize string `mapstructure:"vm_size"` VMSize string `mapstructure:"vm_size"`
// Deployment // Deployment
ResourceGroupName string `mapstructure:"resource_group_name"` AzureTags map[string]*string `mapstructure:"azure_tags"`
StorageAccount string `mapstructure:"storage_account"` ResourceGroupName string `mapstructure:"resource_group_name"`
StorageAccount string `mapstructure:"storage_account"`
storageAccountBlobEndpoint string storageAccountBlobEndpoint string
CloudEnvironmentName string `mapstructure:"cloud_environment_name"` CloudEnvironmentName string `mapstructure:"cloud_environment_name"`
cloudEnvironment *azure.Environment cloudEnvironment *azure.Environment
@ -222,6 +223,7 @@ func newConfig(raws ...interface{}) (*Config, []string, error) {
errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(c.ctx)...) errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(c.ctx)...)
assertRequiredParametersSet(&c, errs) assertRequiredParametersSet(&c, errs)
assertTagProperties(&c, errs)
if errs != nil && len(errs.Errors) > 0 { if errs != nil && len(errs.Errors) > 0 {
return nil, nil, errs 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) { func assertRequiredParametersSet(c *Config, errs *packer.MultiError) {
///////////////////////////////////////////// /////////////////////////////////////////////
// Authentication via OAUTH // Authentication via OAUTH

View File

@ -4,6 +4,7 @@
package arm package arm
import ( import (
"fmt"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -32,16 +33,16 @@ func TestConfigShouldProvideReasonableDefaultValues(t *testing.T) {
c, _, err := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) c, _, err := newConfig(getArmBuilderConfiguration(), getPackerConfiguration())
if err != nil { 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) t.Fatalf(" errors: %s\n", err)
} }
if c.UserName == "" { 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 == "" { 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 != "" { if c.ObjectID != "" {
@ -283,7 +284,7 @@ func TestUserShouldProvideRequiredValues(t *testing.T) {
// Ensure we can successfully create a config. // Ensure we can successfully create a config.
_, _, err := newConfig(builderValues, getPackerConfiguration()) _, _, err := newConfig(builderValues, getPackerConfiguration())
if err != nil { 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) t.Fatalf(" -> %+v\n", builderValues)
} }
@ -294,7 +295,7 @@ func TestUserShouldProvideRequiredValues(t *testing.T) {
_, _, err := newConfig(builderValues, getPackerConfiguration()) _, _, err := newConfig(builderValues, getPackerConfiguration())
if err == nil { 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) t.Fatalf(" -> %+v\n", builderValues)
} }
@ -374,7 +375,7 @@ func TestWinRMConfigShouldSetRoundTripDecorator(t *testing.T) {
} }
if c.Comm.WinRMTransportDecorator == nil { 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") { 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") { 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 { func getArmBuilderConfiguration() map[string]string {
m := make(map[string]string) m := make(map[string]string)
for _, v := range requiredConfigValues { for _, v := range requiredConfigValues {

View File

@ -14,7 +14,7 @@ import (
type StepCreateResourceGroup struct { type StepCreateResourceGroup struct {
client *AzureClient client *AzureClient
create func(resourceGroupName string, location string) error create func(resourceGroupName string, location string, tags *map[string]*string) error
say func(message string) say func(message string)
error func(e error) error func(e error)
} }
@ -30,9 +30,10 @@ func NewStepCreateResourceGroup(client *AzureClient, ui packer.Ui) *StepCreateRe
return step 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{ _, err := s.client.GroupsClient.CreateOrUpdate(resourceGroupName, resources.ResourceGroup{
Location: &location, Location: &location,
Tags: tags,
}) })
return err return err
@ -43,11 +44,16 @@ func (s *StepCreateResourceGroup) Run(state multistep.StateBag) multistep.StepAc
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
var location = state.Get(constants.ArmLocation).(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(" -> ResourceGroupName : '%s'", resourceGroupName))
s.say(fmt.Sprintf(" -> Location : '%s'", location)) 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 { if err == nil {
state.Put(constants.ArmIsResourceGroupCreated, true) state.Put(constants.ArmIsResourceGroupCreated, true)
} }

View File

@ -13,7 +13,7 @@ import (
func TestStepCreateResourceGroupShouldFailIfCreateFails(t *testing.T) { func TestStepCreateResourceGroupShouldFailIfCreateFails(t *testing.T) {
var testSubject = &StepCreateResourceGroup{ 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) {}, say: func(message string) {},
error: func(e error) {}, error: func(e error) {},
} }
@ -32,7 +32,7 @@ func TestStepCreateResourceGroupShouldFailIfCreateFails(t *testing.T) {
func TestStepCreateResourceGroupShouldPassIfCreatePasses(t *testing.T) { func TestStepCreateResourceGroupShouldPassIfCreatePasses(t *testing.T) {
var testSubject = &StepCreateResourceGroup{ var testSubject = &StepCreateResourceGroup{
create: func(string, string) error { return nil }, create: func(string, string, *map[string]*string) error { return nil },
say: func(message string) {}, say: func(message string) {},
error: func(e error) {}, error: func(e error) {},
} }
@ -52,11 +52,13 @@ func TestStepCreateResourceGroupShouldPassIfCreatePasses(t *testing.T) {
func TestStepCreateResourceGroupShouldTakeStepArgumentsFromStateBag(t *testing.T) { func TestStepCreateResourceGroupShouldTakeStepArgumentsFromStateBag(t *testing.T) {
var actualResourceGroupName string var actualResourceGroupName string
var actualLocation string var actualLocation string
var actualTags *map[string]*string
var testSubject = &StepCreateResourceGroup{ var testSubject = &StepCreateResourceGroup{
create: func(resourceGroupName string, location string) error { create: func(resourceGroupName string, location string, tags *map[string]*string) error {
actualResourceGroupName = resourceGroupName actualResourceGroupName = resourceGroupName
actualLocation = location actualLocation = location
actualTags = tags
return nil return nil
}, },
say: func(message string) {}, 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) 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 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 { if actualResourceGroupName != expectedResourceGroupName {
t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") 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.") 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) _, ok := stateBag.GetOk(constants.ArmIsResourceGroupCreated)
if !ok { if !ok {
t.Fatal("Expected the step to add item to stateBag['constants.ArmIsResourceGroupCreated'], but it did not.") 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.ArmLocation, "Unit Test: Location")
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName") stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
value := "Unit Test: Tags"
tags := map[string]*string{
"tag01": &value,
}
stateBag.Put(constants.ArmTags, &tags)
return stateBag return stateBag
} }

View File

@ -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')]"
}
}
]
}
]
}`

View File

@ -20,7 +20,11 @@ func GetKeyVaultDeployment(config *Config) (*resources.Deployment, error) {
TenantId: &template.TemplateParameter{Value: config.TenantID}, 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) { func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error) {
@ -34,7 +38,7 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error)
VMName: &template.TemplateParameter{Value: config.tmpComputeName}, VMName: &template.TemplateParameter{Value: config.tmpComputeName},
} }
builder, _ := template.NewTemplateBuilder() builder, _ := template.NewTemplateBuilder(template.BasicTemplate)
osType := compute.Linux osType := compute.Linux
switch config.OSType { switch config.OSType {
@ -58,6 +62,7 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error)
config.VirtualNetworkSubnetName) config.VirtualNetworkSubnetName)
} }
builder.SetTags(&config.AzureTags)
doc, _ := builder.ToJSON() doc, _ := builder.ToJSON()
return createDeploymentParameters(*doc, params) return createDeploymentParameters(*doc, params)
} }

View File

@ -56,6 +56,11 @@
"type": "secrets" "type": "secrets"
} }
], ],
"tags": {
"tag01": "value01",
"tag02": "value02",
"tag03": "value03"
},
"type": "Microsoft.KeyVault/vaults" "type": "Microsoft.KeyVault/vaults"
} }
], ],

View File

@ -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'))]"
}
}

View File

@ -23,19 +23,19 @@ func TestVirtualMachineDeployment00(t *testing.T) {
} }
if deployment.Properties.ParametersLink != nil { 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 { 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 { 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 { 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. // Ensure the link values are not set, and the concrete values are set.
func TestKeyVaultDeployment00(t *testing.T) { func TestKeyVaultDeployment00(t *testing.T) {
c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration())
@ -190,19 +225,19 @@ func TestKeyVaultDeployment00(t *testing.T) {
} }
if deployment.Properties.ParametersLink != nil { 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 { 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 { 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 { 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) { 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) deployment, err := GetKeyVaultDeployment(c)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -26,5 +26,6 @@ const (
ArmResourceGroupName string = "arm.ResourceGroupName" ArmResourceGroupName string = "arm.ResourceGroupName"
ArmIsResourceGroupCreated string = "arm.IsResourceGroupCreated" ArmIsResourceGroupCreated string = "arm.IsResourceGroupCreated"
ArmStorageAccountName string = "arm.StorageAccountName" ArmStorageAccountName string = "arm.StorageAccountName"
ArmTags string = "arm.Tags"
ArmVirtualMachineCaptureParameters string = "arm.VirtualMachineCaptureParameters" ArmVirtualMachineCaptureParameters string = "arm.VirtualMachineCaptureParameters"
) )

View File

@ -3,7 +3,6 @@ package template
import ( import (
"github.com/Azure/azure-sdk-for-go/arm/compute" "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/network"
//"github.com/Azure/azure-sdk-for-go/arm/resources/resources"
) )
///////////////////////////////////////////////// /////////////////////////////////////////////////
@ -26,25 +25,49 @@ type Parameters struct {
///////////////////////////////////////////////// /////////////////////////////////////////////////
// Template > Resource // Template > Resource
type Resource struct { type Resource struct {
ApiVersion *string `json:"apiVersion"` ApiVersion *string `json:"apiVersion"`
Name *string `json:"name"` Name *string `json:"name"`
Type *string `json:"type"` Type *string `json:"type"`
Location *string `json:"location"` Location *string `json:"location,omitempty"`
DependsOn *[]string `json:"dependsOn,omitempty"` DependsOn *[]string `json:"dependsOn,omitempty"`
Properties *Properties `json:"properties,omitempty"` Properties *Properties `json:"properties,omitempty"`
Tags *map[string]*string `json:"tags,omitempty"`
Resources *[]Resource `json:"resources,omitempty"`
} }
///////////////////////////////////////////////// /////////////////////////////////////////////////
// Template > Resource > Properties // Template > Resource > Properties
type Properties struct { type Properties struct {
AddressSpace *network.AddressSpace `json:"addressSpace,omitempty"` AccessPolicies *[]AccessPolicies `json:"accessPolicies,omitempty"`
DiagnosticsProfile *compute.DiagnosticsProfile `json:"diagnosticsProfile,omitempty"` AddressSpace *network.AddressSpace `json:"addressSpace,omitempty"`
DNSSettings *network.PublicIPAddressDNSSettings `json:"dnsSettings,omitempty"` DiagnosticsProfile *compute.DiagnosticsProfile `json:"diagnosticsProfile,omitempty"`
HardwareProfile *compute.HardwareProfile `json:"hardwareProfile,omitempty"` DNSSettings *network.PublicIPAddressDNSSettings `json:"dnsSettings,omitempty"`
IPConfigurations *[]network.IPConfiguration `json:"ipConfigurations,omitempty"` EnabledForDeployment *string `json:"enabledForDeployment,omitempty"`
NetworkProfile *compute.NetworkProfile `json:"networkProfile,omitempty"` EnabledForTemplateDeployment *string `json:"enabledForTemplateDeployment,omitempty"`
OsProfile *compute.OSProfile `json:"osProfile,omitempty"` HardwareProfile *compute.HardwareProfile `json:"hardwareProfile,omitempty"`
PublicIPAllocatedMethod *network.IPAllocationMethod `json:"publicIPAllocationMethod,omitempty"` IPConfigurations *[]network.IPConfiguration `json:"ipConfigurations,omitempty"`
StorageProfile *compute.StorageProfile `json:"storageProfile,omitempty"` NetworkProfile *compute.NetworkProfile `json:"networkProfile,omitempty"`
Subnets *[]network.Subnet `json:"subnets,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"`
} }

View File

@ -26,10 +26,10 @@ type TemplateBuilder struct {
template *Template template *Template
} }
func NewTemplateBuilder() (*TemplateBuilder, error) { func NewTemplateBuilder(template string) (*TemplateBuilder, error) {
var t Template var t Template
err := json.Unmarshal([]byte(basicTemplate), &t) err := json.Unmarshal([]byte(template), &t)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -150,6 +150,17 @@ func (s *TemplateBuilder) SetVirtualNetwork(virtualNetworkResourceGroup, virtual
return nil 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) { func (s *TemplateBuilder) ToJSON() (*string, error) {
bs, err := json.MarshalIndent(s.template, jsonPrefix, jsonIndent) bs, err := json.MarshalIndent(s.template, jsonPrefix, jsonIndent)
@ -210,7 +221,81 @@ func (s *TemplateBuilder) deleteResourceDependency(resource *Resource, predicate
*resource.DependsOn = deps *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", "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json",
"contentVersion": "1.0.0.0", "contentVersion": "1.0.0.0",
"parameters": { "parameters": {

View File

@ -10,7 +10,7 @@ import (
// Ensure that a Linux template is configured as expected. // Ensure that a Linux template is configured as expected.
// * Include SSH configuration: authorized key, and key path. // * Include SSH configuration: authorized key, and key path.
func TestBuildLinux00(t *testing.T) { func TestBuildLinux00(t *testing.T) {
testSubject, err := NewTemplateBuilder() testSubject, err := NewTemplateBuilder(BasicTemplate)
if err != nil { if err != nil {
t.Fatal(err) 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. // Ensure that a user can specify a custom VHD when building a Linux template.
func TestBuildLinux01(t *testing.T) { func TestBuildLinux01(t *testing.T) {
testSubject, err := NewTemplateBuilder() testSubject, err := NewTemplateBuilder(BasicTemplate)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -66,7 +66,7 @@ func TestBuildLinux01(t *testing.T) {
// Ensure that a user can specify an existing Virtual Network // Ensure that a user can specify an existing Virtual Network
func TestBuildLinux02(t *testing.T) { func TestBuildLinux02(t *testing.T) {
testSubject, err := NewTemplateBuilder() testSubject, err := NewTemplateBuilder(BasicTemplate)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -94,7 +94,7 @@ func TestBuildLinux02(t *testing.T) {
// * Include WinRM configuration. // * Include WinRM configuration.
// * Include KeyVault configuration, which is needed for WinRM. // * Include KeyVault configuration, which is needed for WinRM.
func TestBuildWindows00(t *testing.T) { func TestBuildWindows00(t *testing.T) {
testSubject, err := NewTemplateBuilder() testSubject, err := NewTemplateBuilder(BasicTemplate)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -23,6 +23,11 @@
"image_offer": "UbuntuServer", "image_offer": "UbuntuServer",
"image_sku": "16.04.0-LTS", "image_sku": "16.04.0-LTS",
"azure_tags": {
"dept": "engineering",
"task": "image deployment"
},
"location": "West US", "location": "West US",
"vm_size": "Standard_A2" "vm_size": "Standard_A2"
}], }],

View File

@ -57,6 +57,10 @@ builder.
### Optional: ### 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 - `cloud_environment_name` (string) One of `Public`, `China`, `Germany`, or
`USGovernment`. Defaults to `Public`. Long forms such as `USGovernment`. Defaults to `Public`. Long forms such as
`USGovernmentCloud` and `AzureUSGovernmentCloud` are also supported. `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_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. 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 - `object_id` (string) Specify an OAuth Object ID to protect WinRM certificates
created at runtime. This variable is required when creating images based on 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_publisher": "Canonical",
"image_offer": "UbuntuServer", "image_offer": "UbuntuServer",
"image_sku": "14.04.4-LTS", "image_sku": "14.04.4-LTS",
"azure_tags": {
"dept": "engineering"
},
"location": "West US", "location": "West US",
"vm_size": "Standard_A2" "vm_size": "Standard_A2"