Merge pull request #3764 from boumenot/pr-azure-tags
azure: tag all resources
This commit is contained in:
commit
6563bbc854
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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')]"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -56,6 +56,11 @@
|
|||
"type": "secrets"
|
||||
}
|
||||
],
|
||||
"tags": {
|
||||
"tag01": "value01",
|
||||
"tag02": "value02",
|
||||
"tag03": "value03"
|
||||
},
|
||||
"type": "Microsoft.KeyVault/vaults"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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'))]"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}],
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue