diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go index f293cde4e..5e6e8d1e7 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -103,6 +103,13 @@ type Config struct { // Authentication via OAUTH ClientConfig client.Config `mapstructure:",squash"` + // If set with one or more resource ids of user assigned managed identities, they will be configured on the VM. + // See [documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token) + // for how to acquire tokens within the VM. + // To assign a user assigned managed identity to a VM, the provided account or service principal must have [Managed Identity Operator](https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#managed-identity-operator) + // and [Virtual Machine Contributor](https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#virtual-machine-contributor) role assignments. + UserAssignedManagedIdentities []string `mapstructure:"user_assigned_managed_identities" required:"false"` + // VHD prefix. CaptureNamePrefix string `mapstructure:"capture_name_prefix"` // Destination container name. @@ -774,6 +781,24 @@ func assertTagProperties(c *Config, errs *packer.MultiError) { func assertRequiredParametersSet(c *Config, errs *packer.MultiError) { c.ClientConfig.Validate(errs) + ///////////////////////////////////////////// + // Identity + if len(c.UserAssignedManagedIdentities) != 0 { + for _, rid := range c.UserAssignedManagedIdentities { + r, err := client.ParseResourceID(rid) + if err != nil { + errs = packer.MultiErrorAppend(errs, err) + } else { + if !strings.EqualFold(r.Provider, "Microsoft.ManagedIdentity") { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A valid user assigned managed identity resource id must have a correct resource provider")) + } + if !strings.EqualFold(r.ResourceType.String(), "userAssignedIdentities") { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A valid user assigned managed identity resource id must have a correct resource type")) + } + } + } + } + ///////////////////////////////////////////// // Capture if c.CaptureContainerName == "" && c.ManagedImageName == "" { diff --git a/builder/azure/arm/config.hcl2spec.go b/builder/azure/arm/config.hcl2spec.go index 9413caf2f..9064de4ef 100644 --- a/builder/azure/arm/config.hcl2spec.go +++ b/builder/azure/arm/config.hcl2spec.go @@ -25,6 +25,7 @@ type FlatConfig struct { ObjectID *string `mapstructure:"object_id" cty:"object_id" hcl:"object_id"` TenantID *string `mapstructure:"tenant_id" required:"false" cty:"tenant_id" hcl:"tenant_id"` SubscriptionID *string `mapstructure:"subscription_id" cty:"subscription_id" hcl:"subscription_id"` + UserAssignedManagedIdentities []string `mapstructure:"user_assigned_managed_identities" required:"false" cty:"user_assigned_managed_identities" hcl:"user_assigned_managed_identities"` CaptureNamePrefix *string `mapstructure:"capture_name_prefix" cty:"capture_name_prefix" hcl:"capture_name_prefix"` CaptureContainerName *string `mapstructure:"capture_container_name" cty:"capture_container_name" hcl:"capture_container_name"` SharedGallery *FlatSharedImageGallery `mapstructure:"shared_image_gallery" required:"false" cty:"shared_image_gallery" hcl:"shared_image_gallery"` @@ -128,110 +129,111 @@ func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } // The decoded values from this spec will then be applied to a FlatConfig. func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { s := map[string]hcldec.Spec{ - "packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false}, - "packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false}, - "packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false}, - "packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false}, - "packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false}, - "packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false}, - "packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false}, - "cloud_environment_name": &hcldec.AttrSpec{Name: "cloud_environment_name", Type: cty.String, Required: false}, - "client_id": &hcldec.AttrSpec{Name: "client_id", Type: cty.String, Required: false}, - "client_secret": &hcldec.AttrSpec{Name: "client_secret", Type: cty.String, Required: false}, - "client_cert_path": &hcldec.AttrSpec{Name: "client_cert_path", Type: cty.String, Required: false}, - "client_jwt": &hcldec.AttrSpec{Name: "client_jwt", Type: cty.String, Required: false}, - "object_id": &hcldec.AttrSpec{Name: "object_id", Type: cty.String, Required: false}, - "tenant_id": &hcldec.AttrSpec{Name: "tenant_id", Type: cty.String, Required: false}, - "subscription_id": &hcldec.AttrSpec{Name: "subscription_id", Type: cty.String, Required: false}, - "capture_name_prefix": &hcldec.AttrSpec{Name: "capture_name_prefix", Type: cty.String, Required: false}, - "capture_container_name": &hcldec.AttrSpec{Name: "capture_container_name", Type: cty.String, Required: false}, - "shared_image_gallery": &hcldec.BlockSpec{TypeName: "shared_image_gallery", Nested: hcldec.ObjectSpec((*FlatSharedImageGallery)(nil).HCL2Spec())}, - "shared_image_gallery_destination": &hcldec.BlockSpec{TypeName: "shared_image_gallery_destination", Nested: hcldec.ObjectSpec((*FlatSharedImageGalleryDestination)(nil).HCL2Spec())}, - "shared_image_gallery_timeout": &hcldec.AttrSpec{Name: "shared_image_gallery_timeout", Type: cty.String, Required: false}, + "packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false}, + "packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false}, + "packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false}, + "packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false}, + "packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false}, + "packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false}, + "packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false}, + "cloud_environment_name": &hcldec.AttrSpec{Name: "cloud_environment_name", Type: cty.String, Required: false}, + "client_id": &hcldec.AttrSpec{Name: "client_id", Type: cty.String, Required: false}, + "client_secret": &hcldec.AttrSpec{Name: "client_secret", Type: cty.String, Required: false}, + "client_cert_path": &hcldec.AttrSpec{Name: "client_cert_path", Type: cty.String, Required: false}, + "client_jwt": &hcldec.AttrSpec{Name: "client_jwt", Type: cty.String, Required: false}, + "object_id": &hcldec.AttrSpec{Name: "object_id", Type: cty.String, Required: false}, + "tenant_id": &hcldec.AttrSpec{Name: "tenant_id", Type: cty.String, Required: false}, + "subscription_id": &hcldec.AttrSpec{Name: "subscription_id", Type: cty.String, Required: false}, + "user_assigned_managed_identities": &hcldec.AttrSpec{Name: "user_assigned_managed_identities", Type: cty.List(cty.String), Required: false}, + "capture_name_prefix": &hcldec.AttrSpec{Name: "capture_name_prefix", Type: cty.String, Required: false}, + "capture_container_name": &hcldec.AttrSpec{Name: "capture_container_name", Type: cty.String, Required: false}, + "shared_image_gallery": &hcldec.BlockSpec{TypeName: "shared_image_gallery", Nested: hcldec.ObjectSpec((*FlatSharedImageGallery)(nil).HCL2Spec())}, + "shared_image_gallery_destination": &hcldec.BlockSpec{TypeName: "shared_image_gallery_destination", Nested: hcldec.ObjectSpec((*FlatSharedImageGalleryDestination)(nil).HCL2Spec())}, + "shared_image_gallery_timeout": &hcldec.AttrSpec{Name: "shared_image_gallery_timeout", Type: cty.String, Required: false}, "shared_gallery_image_version_end_of_life_date": &hcldec.AttrSpec{Name: "shared_gallery_image_version_end_of_life_date", Type: cty.String, Required: false}, "shared_image_gallery_replica_count": &hcldec.AttrSpec{Name: "shared_image_gallery_replica_count", Type: cty.Number, Required: false}, "shared_gallery_image_version_exclude_from_latest": &hcldec.AttrSpec{Name: "shared_gallery_image_version_exclude_from_latest", Type: cty.Bool, Required: false}, - "image_publisher": &hcldec.AttrSpec{Name: "image_publisher", Type: cty.String, Required: false}, - "image_offer": &hcldec.AttrSpec{Name: "image_offer", Type: cty.String, Required: false}, - "image_sku": &hcldec.AttrSpec{Name: "image_sku", Type: cty.String, Required: false}, - "image_version": &hcldec.AttrSpec{Name: "image_version", Type: cty.String, Required: false}, - "image_url": &hcldec.AttrSpec{Name: "image_url", Type: cty.String, Required: false}, - "custom_managed_image_name": &hcldec.AttrSpec{Name: "custom_managed_image_name", Type: cty.String, Required: false}, - "custom_managed_image_resource_group_name": &hcldec.AttrSpec{Name: "custom_managed_image_resource_group_name", Type: cty.String, Required: false}, - "location": &hcldec.AttrSpec{Name: "location", Type: cty.String, Required: false}, - "vm_size": &hcldec.AttrSpec{Name: "vm_size", Type: cty.String, Required: false}, - "managed_image_resource_group_name": &hcldec.AttrSpec{Name: "managed_image_resource_group_name", Type: cty.String, Required: false}, - "managed_image_name": &hcldec.AttrSpec{Name: "managed_image_name", Type: cty.String, Required: false}, - "managed_image_storage_account_type": &hcldec.AttrSpec{Name: "managed_image_storage_account_type", Type: cty.String, Required: false}, - "managed_image_os_disk_snapshot_name": &hcldec.AttrSpec{Name: "managed_image_os_disk_snapshot_name", Type: cty.String, Required: false}, - "managed_image_data_disk_snapshot_prefix": &hcldec.AttrSpec{Name: "managed_image_data_disk_snapshot_prefix", Type: cty.String, Required: false}, - "managed_image_zone_resilient": &hcldec.AttrSpec{Name: "managed_image_zone_resilient", Type: cty.Bool, Required: false}, - "azure_tags": &hcldec.AttrSpec{Name: "azure_tags", Type: cty.Map(cty.String), Required: false}, - "azure_tag": &hcldec.BlockListSpec{TypeName: "azure_tag", Nested: hcldec.ObjectSpec((*hcl2template.FlatNameValue)(nil).HCL2Spec())}, - "resource_group_name": &hcldec.AttrSpec{Name: "resource_group_name", Type: cty.String, Required: false}, - "storage_account": &hcldec.AttrSpec{Name: "storage_account", Type: cty.String, Required: false}, - "temp_compute_name": &hcldec.AttrSpec{Name: "temp_compute_name", Type: cty.String, Required: false}, - "temp_resource_group_name": &hcldec.AttrSpec{Name: "temp_resource_group_name", Type: cty.String, Required: false}, - "build_resource_group_name": &hcldec.AttrSpec{Name: "build_resource_group_name", Type: cty.String, Required: false}, - "build_key_vault_name": &hcldec.AttrSpec{Name: "build_key_vault_name", Type: cty.String, Required: false}, - "build_key_vault_sku": &hcldec.AttrSpec{Name: "build_key_vault_sku", Type: cty.String, Required: false}, - "private_virtual_network_with_public_ip": &hcldec.AttrSpec{Name: "private_virtual_network_with_public_ip", Type: cty.Bool, Required: false}, - "virtual_network_name": &hcldec.AttrSpec{Name: "virtual_network_name", Type: cty.String, Required: false}, - "virtual_network_subnet_name": &hcldec.AttrSpec{Name: "virtual_network_subnet_name", Type: cty.String, Required: false}, - "virtual_network_resource_group_name": &hcldec.AttrSpec{Name: "virtual_network_resource_group_name", Type: cty.String, Required: false}, - "custom_data_file": &hcldec.AttrSpec{Name: "custom_data_file", Type: cty.String, Required: false}, - "plan_info": &hcldec.BlockSpec{TypeName: "plan_info", Nested: hcldec.ObjectSpec((*FlatPlanInformation)(nil).HCL2Spec())}, - "polling_duration_timeout": &hcldec.AttrSpec{Name: "polling_duration_timeout", Type: cty.String, Required: false}, - "os_type": &hcldec.AttrSpec{Name: "os_type", Type: cty.String, Required: false}, - "os_disk_size_gb": &hcldec.AttrSpec{Name: "os_disk_size_gb", Type: cty.Number, Required: false}, - "disk_additional_size": &hcldec.AttrSpec{Name: "disk_additional_size", Type: cty.List(cty.Number), Required: false}, - "disk_caching_type": &hcldec.AttrSpec{Name: "disk_caching_type", Type: cty.String, Required: false}, - "allowed_inbound_ip_addresses": &hcldec.AttrSpec{Name: "allowed_inbound_ip_addresses", Type: cty.List(cty.String), Required: false}, - "boot_diag_storage_account": &hcldec.AttrSpec{Name: "boot_diag_storage_account", Type: cty.String, Required: false}, - "custom_resource_build_prefix": &hcldec.AttrSpec{Name: "custom_resource_build_prefix", Type: cty.String, Required: false}, - "communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false}, - "pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false}, - "ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false}, - "ssh_port": &hcldec.AttrSpec{Name: "ssh_port", Type: cty.Number, Required: false}, - "ssh_username": &hcldec.AttrSpec{Name: "ssh_username", Type: cty.String, Required: false}, - "ssh_password": &hcldec.AttrSpec{Name: "ssh_password", Type: cty.String, Required: false}, - "ssh_keypair_name": &hcldec.AttrSpec{Name: "ssh_keypair_name", Type: cty.String, Required: false}, - "temporary_key_pair_name": &hcldec.AttrSpec{Name: "temporary_key_pair_name", Type: cty.String, Required: false}, - "ssh_clear_authorized_keys": &hcldec.AttrSpec{Name: "ssh_clear_authorized_keys", Type: cty.Bool, Required: false}, - "ssh_private_key_file": &hcldec.AttrSpec{Name: "ssh_private_key_file", Type: cty.String, Required: false}, - "ssh_pty": &hcldec.AttrSpec{Name: "ssh_pty", Type: cty.Bool, Required: false}, - "ssh_timeout": &hcldec.AttrSpec{Name: "ssh_timeout", Type: cty.String, Required: false}, - "ssh_wait_timeout": &hcldec.AttrSpec{Name: "ssh_wait_timeout", Type: cty.String, Required: false}, - "ssh_agent_auth": &hcldec.AttrSpec{Name: "ssh_agent_auth", Type: cty.Bool, Required: false}, - "ssh_disable_agent_forwarding": &hcldec.AttrSpec{Name: "ssh_disable_agent_forwarding", Type: cty.Bool, Required: false}, - "ssh_handshake_attempts": &hcldec.AttrSpec{Name: "ssh_handshake_attempts", Type: cty.Number, Required: false}, - "ssh_bastion_host": &hcldec.AttrSpec{Name: "ssh_bastion_host", Type: cty.String, Required: false}, - "ssh_bastion_port": &hcldec.AttrSpec{Name: "ssh_bastion_port", Type: cty.Number, Required: false}, - "ssh_bastion_agent_auth": &hcldec.AttrSpec{Name: "ssh_bastion_agent_auth", Type: cty.Bool, Required: false}, - "ssh_bastion_username": &hcldec.AttrSpec{Name: "ssh_bastion_username", Type: cty.String, Required: false}, - "ssh_bastion_password": &hcldec.AttrSpec{Name: "ssh_bastion_password", Type: cty.String, Required: false}, - "ssh_bastion_interactive": &hcldec.AttrSpec{Name: "ssh_bastion_interactive", Type: cty.Bool, Required: false}, - "ssh_bastion_private_key_file": &hcldec.AttrSpec{Name: "ssh_bastion_private_key_file", Type: cty.String, Required: false}, - "ssh_file_transfer_method": &hcldec.AttrSpec{Name: "ssh_file_transfer_method", Type: cty.String, Required: false}, - "ssh_proxy_host": &hcldec.AttrSpec{Name: "ssh_proxy_host", Type: cty.String, Required: false}, - "ssh_proxy_port": &hcldec.AttrSpec{Name: "ssh_proxy_port", Type: cty.Number, Required: false}, - "ssh_proxy_username": &hcldec.AttrSpec{Name: "ssh_proxy_username", Type: cty.String, Required: false}, - "ssh_proxy_password": &hcldec.AttrSpec{Name: "ssh_proxy_password", Type: cty.String, Required: false}, - "ssh_keep_alive_interval": &hcldec.AttrSpec{Name: "ssh_keep_alive_interval", Type: cty.String, Required: false}, - "ssh_read_write_timeout": &hcldec.AttrSpec{Name: "ssh_read_write_timeout", Type: cty.String, Required: false}, - "ssh_remote_tunnels": &hcldec.AttrSpec{Name: "ssh_remote_tunnels", Type: cty.List(cty.String), Required: false}, - "ssh_local_tunnels": &hcldec.AttrSpec{Name: "ssh_local_tunnels", Type: cty.List(cty.String), Required: false}, - "ssh_public_key": &hcldec.AttrSpec{Name: "ssh_public_key", Type: cty.List(cty.Number), Required: false}, - "ssh_private_key": &hcldec.AttrSpec{Name: "ssh_private_key", Type: cty.List(cty.Number), Required: false}, - "winrm_username": &hcldec.AttrSpec{Name: "winrm_username", Type: cty.String, Required: false}, - "winrm_password": &hcldec.AttrSpec{Name: "winrm_password", Type: cty.String, Required: false}, - "winrm_host": &hcldec.AttrSpec{Name: "winrm_host", Type: cty.String, Required: false}, - "winrm_port": &hcldec.AttrSpec{Name: "winrm_port", Type: cty.Number, Required: false}, - "winrm_timeout": &hcldec.AttrSpec{Name: "winrm_timeout", Type: cty.String, Required: false}, - "winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false}, - "winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false}, - "winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false}, - "async_resourcegroup_delete": &hcldec.AttrSpec{Name: "async_resourcegroup_delete", Type: cty.Bool, Required: false}, + "image_publisher": &hcldec.AttrSpec{Name: "image_publisher", Type: cty.String, Required: false}, + "image_offer": &hcldec.AttrSpec{Name: "image_offer", Type: cty.String, Required: false}, + "image_sku": &hcldec.AttrSpec{Name: "image_sku", Type: cty.String, Required: false}, + "image_version": &hcldec.AttrSpec{Name: "image_version", Type: cty.String, Required: false}, + "image_url": &hcldec.AttrSpec{Name: "image_url", Type: cty.String, Required: false}, + "custom_managed_image_name": &hcldec.AttrSpec{Name: "custom_managed_image_name", Type: cty.String, Required: false}, + "custom_managed_image_resource_group_name": &hcldec.AttrSpec{Name: "custom_managed_image_resource_group_name", Type: cty.String, Required: false}, + "location": &hcldec.AttrSpec{Name: "location", Type: cty.String, Required: false}, + "vm_size": &hcldec.AttrSpec{Name: "vm_size", Type: cty.String, Required: false}, + "managed_image_resource_group_name": &hcldec.AttrSpec{Name: "managed_image_resource_group_name", Type: cty.String, Required: false}, + "managed_image_name": &hcldec.AttrSpec{Name: "managed_image_name", Type: cty.String, Required: false}, + "managed_image_storage_account_type": &hcldec.AttrSpec{Name: "managed_image_storage_account_type", Type: cty.String, Required: false}, + "managed_image_os_disk_snapshot_name": &hcldec.AttrSpec{Name: "managed_image_os_disk_snapshot_name", Type: cty.String, Required: false}, + "managed_image_data_disk_snapshot_prefix": &hcldec.AttrSpec{Name: "managed_image_data_disk_snapshot_prefix", Type: cty.String, Required: false}, + "managed_image_zone_resilient": &hcldec.AttrSpec{Name: "managed_image_zone_resilient", Type: cty.Bool, Required: false}, + "azure_tags": &hcldec.AttrSpec{Name: "azure_tags", Type: cty.Map(cty.String), Required: false}, + "azure_tag": &hcldec.BlockListSpec{TypeName: "azure_tag", Nested: hcldec.ObjectSpec((*hcl2template.FlatNameValue)(nil).HCL2Spec())}, + "resource_group_name": &hcldec.AttrSpec{Name: "resource_group_name", Type: cty.String, Required: false}, + "storage_account": &hcldec.AttrSpec{Name: "storage_account", Type: cty.String, Required: false}, + "temp_compute_name": &hcldec.AttrSpec{Name: "temp_compute_name", Type: cty.String, Required: false}, + "temp_resource_group_name": &hcldec.AttrSpec{Name: "temp_resource_group_name", Type: cty.String, Required: false}, + "build_resource_group_name": &hcldec.AttrSpec{Name: "build_resource_group_name", Type: cty.String, Required: false}, + "build_key_vault_name": &hcldec.AttrSpec{Name: "build_key_vault_name", Type: cty.String, Required: false}, + "build_key_vault_sku": &hcldec.AttrSpec{Name: "build_key_vault_sku", Type: cty.String, Required: false}, + "private_virtual_network_with_public_ip": &hcldec.AttrSpec{Name: "private_virtual_network_with_public_ip", Type: cty.Bool, Required: false}, + "virtual_network_name": &hcldec.AttrSpec{Name: "virtual_network_name", Type: cty.String, Required: false}, + "virtual_network_subnet_name": &hcldec.AttrSpec{Name: "virtual_network_subnet_name", Type: cty.String, Required: false}, + "virtual_network_resource_group_name": &hcldec.AttrSpec{Name: "virtual_network_resource_group_name", Type: cty.String, Required: false}, + "custom_data_file": &hcldec.AttrSpec{Name: "custom_data_file", Type: cty.String, Required: false}, + "plan_info": &hcldec.BlockSpec{TypeName: "plan_info", Nested: hcldec.ObjectSpec((*FlatPlanInformation)(nil).HCL2Spec())}, + "polling_duration_timeout": &hcldec.AttrSpec{Name: "polling_duration_timeout", Type: cty.String, Required: false}, + "os_type": &hcldec.AttrSpec{Name: "os_type", Type: cty.String, Required: false}, + "os_disk_size_gb": &hcldec.AttrSpec{Name: "os_disk_size_gb", Type: cty.Number, Required: false}, + "disk_additional_size": &hcldec.AttrSpec{Name: "disk_additional_size", Type: cty.List(cty.Number), Required: false}, + "disk_caching_type": &hcldec.AttrSpec{Name: "disk_caching_type", Type: cty.String, Required: false}, + "allowed_inbound_ip_addresses": &hcldec.AttrSpec{Name: "allowed_inbound_ip_addresses", Type: cty.List(cty.String), Required: false}, + "boot_diag_storage_account": &hcldec.AttrSpec{Name: "boot_diag_storage_account", Type: cty.String, Required: false}, + "custom_resource_build_prefix": &hcldec.AttrSpec{Name: "custom_resource_build_prefix", Type: cty.String, Required: false}, + "communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false}, + "pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false}, + "ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false}, + "ssh_port": &hcldec.AttrSpec{Name: "ssh_port", Type: cty.Number, Required: false}, + "ssh_username": &hcldec.AttrSpec{Name: "ssh_username", Type: cty.String, Required: false}, + "ssh_password": &hcldec.AttrSpec{Name: "ssh_password", Type: cty.String, Required: false}, + "ssh_keypair_name": &hcldec.AttrSpec{Name: "ssh_keypair_name", Type: cty.String, Required: false}, + "temporary_key_pair_name": &hcldec.AttrSpec{Name: "temporary_key_pair_name", Type: cty.String, Required: false}, + "ssh_clear_authorized_keys": &hcldec.AttrSpec{Name: "ssh_clear_authorized_keys", Type: cty.Bool, Required: false}, + "ssh_private_key_file": &hcldec.AttrSpec{Name: "ssh_private_key_file", Type: cty.String, Required: false}, + "ssh_pty": &hcldec.AttrSpec{Name: "ssh_pty", Type: cty.Bool, Required: false}, + "ssh_timeout": &hcldec.AttrSpec{Name: "ssh_timeout", Type: cty.String, Required: false}, + "ssh_wait_timeout": &hcldec.AttrSpec{Name: "ssh_wait_timeout", Type: cty.String, Required: false}, + "ssh_agent_auth": &hcldec.AttrSpec{Name: "ssh_agent_auth", Type: cty.Bool, Required: false}, + "ssh_disable_agent_forwarding": &hcldec.AttrSpec{Name: "ssh_disable_agent_forwarding", Type: cty.Bool, Required: false}, + "ssh_handshake_attempts": &hcldec.AttrSpec{Name: "ssh_handshake_attempts", Type: cty.Number, Required: false}, + "ssh_bastion_host": &hcldec.AttrSpec{Name: "ssh_bastion_host", Type: cty.String, Required: false}, + "ssh_bastion_port": &hcldec.AttrSpec{Name: "ssh_bastion_port", Type: cty.Number, Required: false}, + "ssh_bastion_agent_auth": &hcldec.AttrSpec{Name: "ssh_bastion_agent_auth", Type: cty.Bool, Required: false}, + "ssh_bastion_username": &hcldec.AttrSpec{Name: "ssh_bastion_username", Type: cty.String, Required: false}, + "ssh_bastion_password": &hcldec.AttrSpec{Name: "ssh_bastion_password", Type: cty.String, Required: false}, + "ssh_bastion_interactive": &hcldec.AttrSpec{Name: "ssh_bastion_interactive", Type: cty.Bool, Required: false}, + "ssh_bastion_private_key_file": &hcldec.AttrSpec{Name: "ssh_bastion_private_key_file", Type: cty.String, Required: false}, + "ssh_file_transfer_method": &hcldec.AttrSpec{Name: "ssh_file_transfer_method", Type: cty.String, Required: false}, + "ssh_proxy_host": &hcldec.AttrSpec{Name: "ssh_proxy_host", Type: cty.String, Required: false}, + "ssh_proxy_port": &hcldec.AttrSpec{Name: "ssh_proxy_port", Type: cty.Number, Required: false}, + "ssh_proxy_username": &hcldec.AttrSpec{Name: "ssh_proxy_username", Type: cty.String, Required: false}, + "ssh_proxy_password": &hcldec.AttrSpec{Name: "ssh_proxy_password", Type: cty.String, Required: false}, + "ssh_keep_alive_interval": &hcldec.AttrSpec{Name: "ssh_keep_alive_interval", Type: cty.String, Required: false}, + "ssh_read_write_timeout": &hcldec.AttrSpec{Name: "ssh_read_write_timeout", Type: cty.String, Required: false}, + "ssh_remote_tunnels": &hcldec.AttrSpec{Name: "ssh_remote_tunnels", Type: cty.List(cty.String), Required: false}, + "ssh_local_tunnels": &hcldec.AttrSpec{Name: "ssh_local_tunnels", Type: cty.List(cty.String), Required: false}, + "ssh_public_key": &hcldec.AttrSpec{Name: "ssh_public_key", Type: cty.List(cty.Number), Required: false}, + "ssh_private_key": &hcldec.AttrSpec{Name: "ssh_private_key", Type: cty.List(cty.Number), Required: false}, + "winrm_username": &hcldec.AttrSpec{Name: "winrm_username", Type: cty.String, Required: false}, + "winrm_password": &hcldec.AttrSpec{Name: "winrm_password", Type: cty.String, Required: false}, + "winrm_host": &hcldec.AttrSpec{Name: "winrm_host", Type: cty.String, Required: false}, + "winrm_port": &hcldec.AttrSpec{Name: "winrm_port", Type: cty.Number, Required: false}, + "winrm_timeout": &hcldec.AttrSpec{Name: "winrm_timeout", Type: cty.String, Required: false}, + "winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false}, + "winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false}, + "winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false}, + "async_resourcegroup_delete": &hcldec.AttrSpec{Name: "async_resourcegroup_delete", Type: cty.Bool, Required: false}, } return s } diff --git a/builder/azure/arm/config_test.go b/builder/azure/arm/config_test.go index adc726390..fa03ecd97 100644 --- a/builder/azure/arm/config_test.go +++ b/builder/azure/arm/config_test.go @@ -2092,3 +2092,41 @@ func getPackerCommunicatorConfiguration() map[string]string { return config } + +func TestConfigShouldRejectMalformedUserAssignedManagedIdentities(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, + } + + config["user_assigned_managed_identities"] = []string{"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id"} + var c Config + if _, err := c.Prepare(config, getPackerConfiguration()); err != nil { + t.Error("Expected test to pass, but it failed with the well-formed user_assigned_managed_identities.") + } + + malformedUserAssignedManagedIdentityResourceIds := []string{ + "not_a_resource_id", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.ManagedIdentity/userAssignedIdentities/", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.Compute/images/im", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.ManagedIdentity/userAssignedIdentitie/id", + } + + for _, x := range malformedUserAssignedManagedIdentityResourceIds { + config["user_assigned_managed_identities"] = x + var c Config + if _, err := c.Prepare(config, getPackerConfiguration()); err == nil { + t.Errorf("Expected test to fail, but it succeeded with the malformed user_assigned_managed_identities set to %q.", x) + } + } +} diff --git a/builder/azure/arm/template_factory.go b/builder/azure/arm/template_factory.go index 5680f1003..dd2ae5637 100644 --- a/builder/azure/arm/template_factory.go +++ b/builder/azure/arm/template_factory.go @@ -61,6 +61,12 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error) builder.BuildWindows(config.tmpKeyVaultName, config.tmpWinRMCertificateUrl) } + if len(config.UserAssignedManagedIdentities) != 0 { + if err := builder.SetIdentity(config.UserAssignedManagedIdentities); err != nil { + return nil, err + } + } + if config.ImageUrl != "" { builder.SetImageUrl(config.ImageUrl, osType, config.diskCachingType) } else if config.CustomManagedImageName != "" { diff --git a/builder/azure/common/template/template.go b/builder/azure/common/template/template.go index b90d0104b..dad684735 100644 --- a/builder/azure/common/template/template.go +++ b/builder/azure/common/template/template.go @@ -12,7 +12,7 @@ type Template struct { ContentVersion *string `json:"contentVersion"` Parameters *map[string]Parameters `json:"parameters"` Variables *map[string]string `json:"variables"` - Resources *[]Resource `json:"resources"` + Resources []*Resource `json:"resources"` } ///////////////////////////////////////////////// @@ -34,6 +34,7 @@ type Resource struct { Properties *Properties `json:"properties,omitempty"` Tags *map[string]*string `json:"tags,omitempty"` Resources *[]Resource `json:"resources,omitempty"` + Identity *Identity `json:"identity,omitempty"` } type Plan struct { @@ -98,6 +99,13 @@ type Properties struct { Value *string `json:"value,omitempty"` } +// Template > Resource > Identity +// The map values are simplified to struct{} since they are read-only and cannot be set +type Identity struct { + Type *string `json:"type,omitempty"` + UserAssignedIdentities map[string]struct{} `json:"userAssignedIdentities,omitempty"` +} + type AccessPolicies struct { ObjectId *string `json:"objectId,omitempty"` TenantId *string `json:"tenantId,omitempty"` diff --git a/builder/azure/common/template/template_builder.go b/builder/azure/common/template/template_builder.go index fdd298e57..9c2e02d32 100644 --- a/builder/azure/common/template/template_builder.go +++ b/builder/azure/common/template/template_builder.go @@ -103,6 +103,29 @@ func (s *TemplateBuilder) BuildWindows(keyVaultName, winRMCertificateUrl string) return nil } +func (s *TemplateBuilder) SetIdentity(userAssignedManagedIdentities []string) error { + resource, err := s.getResourceByType(resourceVirtualMachine) + if err != nil { + return err + } + + var id *Identity + + if len(userAssignedManagedIdentities) != 0 { + s.setVariable("apiVersion", "2018-06-01") // Required for user assigned managed identity + id = &Identity{ + Type: to.StringPtr("UserAssigned"), + UserAssignedIdentities: make(map[string]struct{}), + } + for _, uid := range userAssignedManagedIdentities { + id.UserAssignedIdentities[uid] = struct{}{} + } + } + + resource.Identity = id + return nil +} + func (s *TemplateBuilder) SetManagedDiskUrl(managedImageId string, storageAccountType compute.StorageAccountTypes, cachingType compute.CachingTypes) error { resource, err := s.getResourceByType(resourceVirtualMachine) if err != nil { @@ -154,7 +177,7 @@ func (s *TemplateBuilder) SetSharedGalleryImage(location, imageID string, cachin return err } - s.setVariable("apiVersion", "2018-04-01") // Required for Shared Image Gallery + s.setVariable("apiVersion", "2018-06-01") // Required for Shared Image Gallery profile := resource.Properties.StorageProfile profile.ImageReference = &compute.ImageReference{ID: &imageID} profile.OsDisk.OsType = s.osType @@ -205,9 +228,9 @@ func (s *TemplateBuilder) SetPlanInfo(name, product, publisher, promotionCode st promotionCodeVal = to.StringPtr(promotionCode) } - for i, x := range *s.template.Resources { + for i, x := range s.template.Resources { if strings.EqualFold(*x.Type, resourceVirtualMachine) { - (*s.template.Resources)[i].Plan = &Plan{ + s.template.Resources[i].Plan = &Plan{ Name: to.StringPtr(name), Product: to.StringPtr(product), Publisher: to.StringPtr(publisher), @@ -351,8 +374,8 @@ func (s *TemplateBuilder) SetTags(tags *map[string]*string) error { return nil } - for i := range *s.template.Resources { - (*s.template.Resources)[i].Tags = tags + for i := range s.template.Resources { + s.template.Resources[i].Tags = tags } return nil } @@ -383,9 +406,9 @@ func (s *TemplateBuilder) ToJSON() (*string, error) { } func (s *TemplateBuilder) getResourceByType(t string) (*Resource, error) { - for _, x := range *s.template.Resources { + for _, x := range s.template.Resources { if strings.EqualFold(*x.Type, t) { - return &x, nil + return x, nil } } @@ -405,28 +428,28 @@ func (s *TemplateBuilder) toVariable(name string) string { } func (s *TemplateBuilder) addResource(newResource *Resource) error { - for _, resource := range *s.template.Resources { + for _, resource := range s.template.Resources { if *resource.Type == *newResource.Type { return fmt.Errorf("template: found an existing resource of type %s", *resource.Type) } } - resources := append(*s.template.Resources, *newResource) - s.template.Resources = &resources + resources := append(s.template.Resources, newResource) + s.template.Resources = resources return nil } func (s *TemplateBuilder) deleteResourceByType(resourceType string) { - resources := make([]Resource, 0) + resources := make([]*Resource, 0) - for _, resource := range *s.template.Resources { + for _, resource := range s.template.Resources { if *resource.Type == resourceType { continue } resources = append(resources, resource) } - s.template.Resources = &resources + s.template.Resources = resources } func (s *TemplateBuilder) addResourceDependency(resource *Resource, dep string) { diff --git a/builder/azure/common/template/template_builder_test.TestSetIdentity00.approved.json b/builder/azure/common/template/template_builder_test.TestSetIdentity00.approved.json new file mode 100644 index 000000000..a182d7e8f --- /dev/null +++ b/builder/azure/common/template/template_builder_test.TestSetIdentity00.approved.json @@ -0,0 +1,188 @@ +{ + "$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" + }, + "dataDiskName": { + "type": "string" + }, + "dnsNameForPublicIP": { + "type": "string" + }, + "nicName": { + "type": "string" + }, + "nsgName": { + "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'))]" + ], + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id": {} + } + }, + "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": { + "offer": "UbuntuServer", + "publisher": "Canonical", + "sku": "16.04", + "version": "latest" + }, + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "name": "[parameters('osDiskName')]", + "vhd": { + "uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]" + } + } + } + }, + "type": "Microsoft.Compute/virtualMachines" + } + ], + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2018-06-01", + "location": "[resourceGroup().location]", + "managedDiskApiVersion": "2017-03-30", + "networkInterfacesApiVersion": "2017-04-01", + "networkSecurityGroupsApiVersion": "2019-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.TestSharedImageGallery00.approved.json b/builder/azure/common/template/template_builder_test.TestSharedImageGallery00.approved.json index 4c83d090d..98602fd16 100644 --- a/builder/azure/common/template/template_builder_test.TestSharedImageGallery00.approved.json +++ b/builder/azure/common/template/template_builder_test.TestSharedImageGallery00.approved.json @@ -157,7 +157,7 @@ ], "variables": { "addressPrefix": "10.0.0.0/16", - "apiVersion": "2018-04-01", + "apiVersion": "2018-06-01", "location": "[resourceGroup().location]", "managedDiskApiVersion": "2017-03-30", "networkInterfacesApiVersion": "2017-04-01", diff --git a/builder/azure/common/template/template_builder_test.go b/builder/azure/common/template/template_builder_test.go index 39e51fd53..2532bcc06 100644 --- a/builder/azure/common/template/template_builder_test.go +++ b/builder/azure/common/template/template_builder_test.go @@ -243,3 +243,32 @@ func TestNetworkSecurityGroup00(t *testing.T) { t.Fatal(err) } } + +// Linux with user assigned managed identity configured +func TestSetIdentity00(t *testing.T) { + testSubject, err := NewTemplateBuilder(BasicTemplate) + if err != nil { + t.Fatal(err) + } + + if err = testSubject.BuildLinux("--test-ssh-authorized-key--"); err != nil { + t.Fatal(err) + } + + if err = testSubject.SetMarketPlaceImage("Canonical", "UbuntuServer", "16.04", "latest", compute.CachingTypesReadWrite); err != nil { + t.Fatal(err) + } + + if err = testSubject.SetIdentity([]string{"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id"}); err != nil { + t.Fatal(err) + } + + doc, err := testSubject.ToJSON() + if err != nil { + t.Fatal(err) + } + + if err = approvaltests.VerifyJSONBytes(t, []byte(*doc)); err != nil { + t.Fatal(err) + } +} diff --git a/website/pages/partials/builder/azure/arm/Config-not-required.mdx b/website/pages/partials/builder/azure/arm/Config-not-required.mdx index 2be7541e8..e4cdb0516 100644 --- a/website/pages/partials/builder/azure/arm/Config-not-required.mdx +++ b/website/pages/partials/builder/azure/arm/Config-not-required.mdx @@ -1,5 +1,11 @@ +- `user_assigned_managed_identities` ([]string) - If set with one or more resource ids of user assigned managed identities, they will be configured on the VM. + See [documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token) + for how to acquire tokens within the VM. + To assign a user assigned managed identity to a VM, the provided account or service principal must have [Managed Identity Operator](https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#managed-identity-operator) + and [Virtual Machine Contributor](https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#virtual-machine-contributor) role assignments. + - `capture_name_prefix` (string) - VHD prefix. - `capture_container_name` (string) - Destination container name.