diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go index 17a242338..d72d28550 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -65,6 +65,14 @@ type PlanInformation struct { PlanPromotionCode string `mapstructure:"plan_promotion_code"` } +type SharedImageGallery struct { + Subscription string `mapstructure:"subscription"` + ResourceGroup string `mapstructure:"resource_group"` + GalleryName string `mapstructure:"gallery_name"` + ImageName string `mapstructure:"image_name"` + ImageVersion string `mapstructure:"image_version"` +} + type Config struct { common.PackerConfig `mapstructure:",squash"` @@ -79,6 +87,9 @@ type Config struct { CaptureNamePrefix string `mapstructure:"capture_name_prefix"` CaptureContainerName string `mapstructure:"capture_container_name"` + // Shared Gallery + SharedGallery SharedImageGallery `mapstructure:"shared_image_gallery"` + // Compute ImagePublisher string `mapstructure:"image_publisher"` ImageOffer string `mapstructure:"image_offer"` @@ -572,19 +583,36 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) { isImageUrl := c.ImageUrl != "" isCustomManagedImage := c.CustomManagedImageName != "" || c.CustomManagedImageResourceGroupName != "" + isSharedGallery := c.SharedGallery.GalleryName != "" isPlatformImage := c.ImagePublisher != "" || c.ImageOffer != "" || c.ImageSku != "" - countSourceInputs := toInt(isImageUrl) + toInt(isCustomManagedImage) + toInt(isPlatformImage) + countSourceInputs := toInt(isImageUrl) + toInt(isCustomManagedImage) + toInt(isPlatformImage) + toInt(isSharedGallery) if countSourceInputs > 1 { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Specify either a VHD (image_url), Image Reference (image_publisher, image_offer, image_sku) or a Managed Disk (custom_managed_disk_image_name, custom_managed_disk_resource_group_name")) + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Specify either a VHD (image_url), Image Reference (image_publisher, image_offer, image_sku), a Managed Disk (custom_managed_disk_image_name, custom_managed_disk_resource_group_name), or a Shared Gallery Image (shared_image_gallery)")) } if isImageUrl && c.ManagedImageResourceGroupName != "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A managed image must be created from a managed image, it cannot be created from a VHD.")) } - if c.ImageUrl == "" && c.CustomManagedImageName == "" { + if c.SharedGallery.GalleryName != "" { + if c.SharedGallery.Subscription == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A shared_image_gallery.subscription must be specified")) + } + if c.SharedGallery.ResourceGroup == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A shared_image_gallery.resource_group must be specified")) + } + if c.SharedGallery.ImageName == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A shared_image_gallery.image_name must be specified")) + } + if c.CaptureContainerName != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("VHD Target [capture_container_name] is not supported when using Shared Image Gallery as source. Use managed_image_resource_group_name instead.")) + } + if c.CaptureNamePrefix != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("VHD Target [capture_name_prefix] is not supported when using Shared Image Gallery as source. Use managed_image_name instead.")) + } + } else if c.ImageUrl == "" && c.CustomManagedImageName == "" { if c.ImagePublisher == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("An image_publisher must be specified")) } diff --git a/builder/azure/arm/config_test.go b/builder/azure/arm/config_test.go index 343b85334..d25329dc7 100644 --- a/builder/azure/arm/config_test.go +++ b/builder/azure/arm/config_test.go @@ -1294,6 +1294,51 @@ func TestConfigShouldAllowAsyncResourceGroupOverrideBadValue(t *testing.T) { } +} +func TestConfigShouldAllowSharedImageGalleryOptions(t *testing.T) { + config := map[string]interface{}{ + "location": "ignore", + "subscription_id": "ignore", + "os_type": "linux", + "shared_image_gallery": map[string]string{ + "subscription": "ignore", + "resource_group": "ignore", + "gallery_name": "ignore", + "image_name": "ignore", + "image_version": "ignore", + }, + } + + _, _, err := newConfig(config, getPackerConfiguration()) + if err == nil { + t.Log("expected config to accept Shared Image Gallery options", err) + } + +} + +func TestConfigShouldRejectSharedImageGalleryWithVhdTarget(t *testing.T) { + config := map[string]interface{}{ + "location": "ignore", + "subscription_id": "ignore", + "os_type": "linux", + "shared_image_gallery": map[string]string{ + "subscription": "ignore", + "resource_group": "ignore", + "gallery_name": "ignore", + "image_name": "ignore", + "image_version": "ignore", + }, + "resource_group_name": "ignore", + "storage_account": "ignore", + "capture_container_name": "ignore", + "capture_name_prefix": "ignore", + } + + _, _, err := newConfig(config, getPackerConfiguration()) + if err != nil { + t.Log("expected an error if Shared Image Gallery source is used with VHD target", err) + } + } func getArmBuilderConfiguration() map[string]string { diff --git a/builder/azure/arm/template_factory.go b/builder/azure/arm/template_factory.go index a1a0dd943..00873e457 100644 --- a/builder/azure/arm/template_factory.go +++ b/builder/azure/arm/template_factory.go @@ -72,6 +72,18 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error) config.ImageVersion) builder.SetManagedMarketplaceImage(config.Location, config.ImagePublisher, config.ImageOffer, config.ImageSku, config.ImageVersion, imageID, config.managedImageStorageAccountType) + } else if config.SharedGallery.Subscription != "" { + imageID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/galleries/%s/images/%s", + config.SharedGallery.Subscription, + config.SharedGallery.ResourceGroup, + config.SharedGallery.GalleryName, + config.SharedGallery.ImageName) + if config.SharedGallery.ImageVersion != "" { + imageID += fmt.Sprintf("/versions/%s", + config.SharedGallery.ImageVersion) + } + + builder.SetSharedGalleryImage(config.Location, imageID) } else { builder.SetMarketPlaceImage(config.ImagePublisher, config.ImageOffer, config.ImageSku, config.ImageVersion) } diff --git a/builder/azure/common/template/template_builder.go b/builder/azure/common/template/template_builder.go index 4a8e6d444..a9455b81e 100644 --- a/builder/azure/common/template/template_builder.go +++ b/builder/azure/common/template/template_builder.go @@ -145,6 +145,21 @@ func (s *TemplateBuilder) SetManagedMarketplaceImage(location, publisher, offer, return nil } +func (s *TemplateBuilder) SetSharedGalleryImage(location, imageID string) error { + resource, err := s.getResourceByType(resourceVirtualMachine) + if err != nil { + return err + } + + s.setVariable("apiVersion", "2018-04-01") // Required for Shared Image Gallery + profile := resource.Properties.StorageProfile + profile.ImageReference = &compute.ImageReference{ID: &imageID} + profile.OsDisk.OsType = s.osType + profile.OsDisk.Vhd = nil + + return nil +} + func (s *TemplateBuilder) SetMarketPlaceImage(publisher, offer, sku, version string) error { resource, err := s.getResourceByType(resourceVirtualMachine) if err != nil { diff --git a/builder/azure/common/template/template_builder_test.TestSharedImageGallery00.approved.json b/builder/azure/common/template/template_builder_test.TestSharedImageGallery00.approved.json new file mode 100644 index 000000000..3d57c2b1f --- /dev/null +++ b/builder/azure/common/template/template_builder_test.TestSharedImageGallery00.approved.json @@ -0,0 +1,170 @@ +{ + "$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" + }, + "nicName": { + "type": "string" + }, + "osDiskName": { + "type": "string" + }, + "publicIPAddressName": { + "type": "string" + }, + "storageAccountBlobEndpoint": { + "type": "string" + }, + "subnetName": { + "type": "string" + }, + "virtualNetworkName": { + "type": "string" + }, + "vmName": { + "type": "string" + }, + "vmSize": { + "type": "string" + } + }, + "resources": [ + { + "apiVersion": "[variables('publicIPAddressApiVersion')]", + "location": "[variables('location')]", + "name": "[parameters('publicIPAddressName')]", + "properties": { + "dnsSettings": { + "domainNameLabel": "[parameters('dnsNameForPublicIP')]" + }, + "publicIPAllocationMethod": "[variables('publicIPAddressType')]" + }, + "type": "Microsoft.Network/publicIPAddresses" + }, + { + "apiVersion": "[variables('virtualNetworksApiVersion')]", + "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('networkInterfacesApiVersion')]", + "dependsOn": [ + "[concat('Microsoft.Network/publicIPAddresses/', parameters('publicIPAddressName'))]", + "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" + ], + "location": "[variables('location')]", + "name": "[parameters('nicName')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('publicIPAddressName'))]" + }, + "subnet": { + "id": "[variables('subnetRef')]" + } + } + } + ] + }, + "type": "Microsoft.Network/networkInterfaces" + }, + { + "apiVersion": "[variables('apiVersion')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', parameters('nicName'))]" + ], + "location": "[variables('location')]", + "name": "[parameters('vmName')]", + "properties": { + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": false + } + }, + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('nicName'))]" + } + ] + }, + "osProfile": { + "adminPassword": "[parameters('adminPassword')]", + "adminUsername": "[parameters('adminUsername')]", + "computerName": "[parameters('vmName')]", + "linuxConfiguration": { + "ssh": { + "publicKeys": [ + { + "keyData": "--test-ssh-authorized-key--", + "path": "[variables('sshKeyPath')]" + } + ] + } + } + }, + "storageProfile": { + "imageReference": { + "id": "/subscriptions/ignore/resourceGroups/ignore/providers/Microsoft.Compute/galleries/ignore/images/ignore" + }, + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "name": "[parameters('osDiskName')]", + "osType": "Linux" + } + } + }, + "type": "Microsoft.Compute/virtualMachines" + } + ], + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2018-04-01", + "location": "[resourceGroup().location]", + "managedDiskApiVersion": "2017-03-30", + "networkInterfacesApiVersion": "2017-04-01", + "publicIPAddressApiVersion": "2017-04-01", + "publicIPAddressType": "Dynamic", + "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", + "subnetAddressPrefix": "10.0.0.0/24", + "subnetName": "[parameters('subnetName')]", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "[parameters('virtualNetworkName')]", + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "virtualNetworksApiVersion": "2017-04-01", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } +} \ No newline at end of file diff --git a/builder/azure/common/template/template_builder_test.go b/builder/azure/common/template/template_builder_test.go index bc7baa175..cbe998c03 100644 --- a/builder/azure/common/template/template_builder_test.go +++ b/builder/azure/common/template/template_builder_test.go @@ -181,3 +181,32 @@ func TestBuildWindows02(t *testing.T) { t.Fatal(err) } } + +// Shared Image Gallery Build +func TestSharedImageGallery00(t *testing.T) { + testSubject, err := NewTemplateBuilder(BasicTemplate) + if err != nil { + t.Fatal(err) + } + + err = testSubject.BuildLinux("--test-ssh-authorized-key--") + if err != nil { + t.Fatal(err) + } + + imageID := "/subscriptions/ignore/resourceGroups/ignore/providers/Microsoft.Compute/galleries/ignore/images/ignore" + err = testSubject.SetSharedGalleryImage("westcentralus", imageID) + if err != nil { + t.Fatal(err) + } + + doc, err := testSubject.ToJSON() + if err != nil { + t.Fatal(err) + } + + err = approvaltests.VerifyJSONBytes(t, []byte(*doc)) + if err != nil { + t.Fatal(err) + } +} diff --git a/website/source/docs/builders/azure.html.md b/website/source/docs/builders/azure.html.md index de54109a5..b3318c2cc 100644 --- a/website/source/docs/builders/azure.html.md +++ b/website/source/docs/builders/azure.html.md @@ -186,6 +186,19 @@ Providing `temp_resource_group_name` or `location` in combination with `build_re 1. PlanPublisher 1. PlanPromotionCode +- `shared_image_gallery` (object) Use a [Shared Gallery image](https://azure.microsoft.com/en-us/blog/announcing-the-public-preview-of-shared-image-gallery/) as the source for this build. *VHD targets are incompatible with this build type* - the target must be a *Managed Image*. +``` +"shared_image_gallery": { + "subscription": "00000000-0000-0000-0000-00000000000", + "resource_group": "ResourceGroup", + "gallery_name": "GalleryName", + "image_name": "ImageName", + "image_version": "1.0.0" +} +"managed_image_name": "TargetImageName", +"managed_image_resource_group_name": "TargetResourceGroup" +``` + - `temp_compute_name` (string) temporary name assigned to the VM. If this value is not set, a random value will be assigned. Knowing the resource group and VM name allows one to execute commands to update the VM during a Packer build, e.g. attach a resource disk to the VM.