Skip to content

Commit

Permalink
Support patching an ARM template.
Browse files Browse the repository at this point in the history
Patching support is based on RFC 6902.  Users have the ability to patch
the ARM virtual machine template before it is submitted to Azure for
processing.

Add a dependency on evanphx/json-patch to support JSON patching.
  • Loading branch information
boumenot committed Jul 29, 2016
1 parent 163e993 commit ccd10ae
Show file tree
Hide file tree
Showing 11 changed files with 1,247 additions and 1 deletion.
4 changes: 4 additions & 0 deletions Godeps/Godeps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions builder/azure/arm/TestVirtualMachineDeployment05.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,47 @@
"resources": [
{
"apiVersion": "[variables('apiVersion')]",
<<<<<<< b7954bc5d9bffce6f22085a3808dea6b48090ab6
"dependsOn": [],
=======
"location": "[variables('location')]",
"name": "[variables('publicIPAddressName')]",
"properties": {
"dnsSettings": {
"domainNameLabel": "[parameters('dnsNameForPublicIP')]"
},
"publicIPAllocationMethod": "[variables('publicIPAddressType')]"
},
"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')]"
}
}
]
},
"type": "Microsoft.Network/virtualNetworks"
},
{
"apiVersion": "[variables('apiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
"[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
],
>>>>>>> Support patching an ARM template.
"location": "[variables('location')]",
"name": "[variables('nicName')]",
"properties": {
Expand All @@ -36,6 +76,12 @@
"name": "ipconfig",
"properties": {
"privateIPAllocationMethod": "Dynamic",
<<<<<<< b7954bc5d9bffce6f22085a3808dea6b48090ab6
=======
"publicIPAddress": {
"id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
},
>>>>>>> Support patching an ARM template.
"subnet": {
"id": "[variables('subnetRef')]"
}
Expand All @@ -52,6 +98,14 @@
],
"location": "[variables('location')]",
"name": "[parameters('vmName')]",
<<<<<<< b7954bc5d9bffce6f22085a3808dea6b48090ab6
=======
"plan": {
"name": "MySKU",
"product": "MyOffer",
"publisher": "MyPublisher"
},
>>>>>>> Support patching an ARM template.
"properties": {
"diagnosticsProfile": {
"bootDiagnostics": {
Expand Down Expand Up @@ -110,11 +164,19 @@
"publicIPAddressType": "Dynamic",
"sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]",
"subnetAddressPrefix": "10.0.0.0/24",
<<<<<<< b7954bc5d9bffce6f22085a3808dea6b48090ab6
"subnetName": "ignore",
"subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]",
"virtualNetworkName": "ignore",
"virtualNetworkResourceGroup": "ignore",
"vmStorageAccountContainerName": "images",
"vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
=======
"subnetName": "packerSubnet",
"subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]",
"virtualNetworkName": "packerNetwork",
"vmStorageAccountContainerName": "images",
"vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
>>>>>>> Support patching an ARM template.
}
}
36 changes: 35 additions & 1 deletion builder/azure/arm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import (
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"

"github.com/evanphx/json-patch"
"golang.org/x/crypto/ssh"
"os"
)

const (
Expand Down Expand Up @@ -78,6 +80,8 @@ type Config struct {
VirtualNetworkName string `mapstructure:"virtual_network_name"`
VirtualNetworkSubnetName string `mapstructure:"virtual_network_subnet_name"`
VirtualNetworkResourceGroupName string `mapstructure:"virtual_network_resource_group_name"`
ArmTemplatePatchFile string `mapstructure:"arm_template_patch"`
armTemplatePatch *jsonpatch.Patch

// OS
OSType string `mapstructure:"os_type"`
Expand Down Expand Up @@ -196,9 +200,13 @@ func newConfig(raws ...interface{}) (*Config, []string, error) {
provideDefaultValues(&c)
setRuntimeValues(&c)
setUserNamePassword(&c)
err = setArmTemplatePatch(&c)
if err != nil {
return nil, nil, fmt.Errorf("patch failure: %s", err)
}
err = setCloudEnvironment(&c)
if err != nil {
return nil, nil, err
return nil, nil, fmt.Errorf("cloud environment: %s", err)
}

// NOTE: if the user did not specify a communicator, then default to both
Expand Down Expand Up @@ -301,6 +309,32 @@ func setUserNamePassword(c *Config) {
}
}

// Set the ARM template patch if there is one. If the user did set one
// process it completely so the code fails fast.
func setArmTemplatePatch(c *Config) error {
if c.ArmTemplatePatchFile == "" {
return nil
}

fh, err := os.Open(c.ArmTemplatePatchFile)
if err != nil {
return err
}

bs, err := ioutil.ReadAll(fh)
if err != nil {
return err
}

patch, err := jsonpatch.DecodePatch(bs)
if err != nil {
return err
}

c.armTemplatePatch = &patch
return nil
}

func setCloudEnvironment(c *Config) error {
lookup := map[string]string{
"CHINA": "AzureChinaCloud",
Expand Down
64 changes: 64 additions & 0 deletions builder/azure/arm/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package arm

import (
"io/ioutil"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -533,6 +534,69 @@ func TestConfigShouldRejectMalformedCaptureContainerName(t *testing.T) {
}
}

// newConfig should fail if the ARM template patch is invalid.
func TestConfigShouldValidateArmTemplatePatchIsInvalid(t *testing.T) {
config := map[string]string{
"capture_container_name": "ignore",
"capture_name_prefix": "ignore",
"image_offer": "ignore",
"image_publisher": "ignore",
"image_sku": "ignore",
"location": "ignore",
"storage_account": "ignore",
"resource_group_name": "ignore",
"subscription_id": "ignore",
// Does not matter for this test case, just pick one.
"os_type": constants.Target_Linux,
}

config["arm_template_patch"] = "__patch_file_does_not_exist__"
_, _, err := newConfig(config, getPackerConfiguration())
if err == nil {
t.Fatal("expected newConfig to fail")
}
}

// newConfig should succeed if the patch file is valid
func TestConfigShouldValidateArmTemplatePatchIsJsonPatch(t *testing.T) {
config := map[string]string{
"capture_container_name": "ignore",
"capture_name_prefix": "ignore",
"image_offer": "ignore",
"image_publisher": "ignore",
"image_sku": "ignore",
"location": "ignore",
"storage_account": "ignore",
"resource_group_name": "ignore",
"subscription_id": "ignore",
// Does not matter for this test case, just pick one.
"os_type": constants.Target_Linux,
}

patch := `[{
"op": "add",
"path": "/resources/3/plan",
"value": {
"name": "MyName",
"product": "MyProduct",
"publisher": "MyPublisher"
}
}]`

filename := "test_arm_template_patch.json"
ioutil.WriteFile(filename, []byte(patch), 0644)
config["arm_template_patch"] = filename

c, _, err := newConfig(config, getPackerConfiguration())
if err != nil {
t.Fatal(err)
}

if c.armTemplatePatch == nil {
t.Fatal("expect c.armTemplatePatch to be non-nil")
}
}

func getArmBuilderConfiguration() map[string]string {
m := make(map[string]string)
for _, v := range requiredConfigValues {
Expand Down
19 changes: 19 additions & 0 deletions builder/azure/arm/template_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,28 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error)
}

doc, _ := builder.ToJSON()
doc, err := patchArmTemplate(config, doc)
if err != nil {
return nil, err
}

return createDeploymentParameters(*doc, params)
}

func patchArmTemplate(config *Config, doc *string) (*string, error) {
if config.armTemplatePatch == nil {
return doc, nil
}

bs, err := config.armTemplatePatch.ApplyIndent([]byte(*doc), " ")
if err != nil {
return nil, err
}

patched := string(bs)
return &patched, nil
}

func createDeploymentParameters(doc string, parameters *template.TemplateParameters) (*resources.Deployment, error) {
var template map[string]interface{}
err := json.Unmarshal(([]byte)(doc), &template)
Expand Down
51 changes: 51 additions & 0 deletions builder/azure/arm/template_factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package arm

import (
"encoding/json"
"io/ioutil"
"testing"

"github.com/Azure/azure-sdk-for-go/arm/resources/resources"
Expand Down Expand Up @@ -177,6 +178,56 @@ func TestVirtualMachineDeployment05(t *testing.T) {
}
}

// Ensure that patches are applied.
func TestVirtualMachineDeployment06(t *testing.T) {
config := map[string]string{
"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",
}

patch := `[{
"op": "add",
"path": "/resources/3/plan",
"value": {
"name": "MySKU",
"product": "MyOffer",
"publisher": "MyPublisher"
}
}]`

filename := "test_virtual_machine_deployment05.json"
ioutil.WriteFile(filename, []byte(patch), 0644)
config["arm_template_patch"] = filename

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)
}
}

// TODO(chrboum): create a test case for negative behavior.
// Validate that GetVirtualMachineDeployment properly handles the case when a
// patch fails. All of the failures that I can think of now would be caught by
// newConfig, which is desirable because it fails faster. Not so good, when
// you are trying to create a failing test case.

// Ensure the link values are not set, and the concrete values are set.
func TestKeyVaultDeployment00(t *testing.T) {
c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration())
Expand Down
25 changes: 25 additions & 0 deletions vendor/github.com/evanphx/json-patch/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ccd10ae

Please sign in to comment.