Compare commits

..

2 Commits

Author SHA1 Message Date
Wilken Rivera 4a091b7bd3 Fix up nav 2021-04-23 14:36:51 -04:00
Wilken Rivera c56677d399 Initial commit of plugin FAQs 2021-04-23 11:47:36 -04:00
440 changed files with 42068 additions and 5044 deletions

View File

@ -1,5 +0,0 @@
bug:
- 'panic:'
crash:
- 'panic:'

View File

@ -50,14 +50,6 @@ async function checkPluginDocs() {
);
}
}
// Validate that local zip files are not used in production
if (typeof pluginEntry.zipFile !== "undefined") {
throw new Error(
`Local ZIP file being used for "${
title || pluginEntry.path || repo
}". The zipFile option should only be used for local development. Please omit the zipFile attribute and ensure the plugin entry points to a remote repository.`
);
}
// Attempt to fetch plugin docs files
const docsMdxFiles = await fetchPluginDocs({ repo, tag: version });
const mdxFilesByComponent = docsMdxFiles.reduce((acc, mdxFile) => {

View File

@ -1,17 +0,0 @@
name: Issue Comment Created Triage
on:
issue_comment:
types: [created]
jobs:
issue_comment_triage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-ecosystem/action-remove-labels@v1
with:
github_token: "${{ secrets.GITHUB_TOKEN }}"
labels: |
stale
waiting-reply

View File

@ -1,16 +0,0 @@
name: Issue Opened Triage
on:
issues:
types: [opened]
jobs:
issue_triage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: github/issue-labeler@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/labeler-issue-triage.yml

View File

@ -1,29 +0,0 @@
name: 'Lock Threads'
on:
schedule:
- cron: '50 1 * * *'
# Only 50 issues will be handled during a given run.
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-comment: >
I'm going to lock this issue because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.
issue-lock-inactive-days: '30'
# Issues older than 180 days ago should be ignored
issue-exclude-created-before: '2020-11-01'
pr-lock-comment: >
I'm going to lock this pull request because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.
pr-lock-inactive-days: '30'
# Issues older than 180 days ago should be ignored
pr-exclude-created-before: '2020-11-01'

View File

@ -1,3 +1,28 @@
behavior "regexp_issue_labeler" "panic_label" {
regexp = "panic:"
labels = ["crash", "bug"]
}
behavior "remove_labels_on_reply" "remove_stale" {
labels = ["waiting-reply", "stale"]
only_non_maintainers = true
}
poll "closed_issue_locker" "locker" {
schedule = "0 50 1 * * *"
closed_for = "720h" # 30 days
max_issues = 500
sleep_between_issues = "5s"
no_comment_if_no_activity_for = "4320h" # 180 days
message = <<-EOF
I'm going to lock this issue because it has been closed for _30 days_ . This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.
EOF
}
poll "label_issue_migrater" "remote_plugin_migrater" {
schedule = "0 20 * * * *"
new_owner = "hashicorp"

View File

@ -1,100 +1,5 @@
## 1.7.3 (Upcoming)
### IMPROVEMENTS:
Major refactor: Extracted a majority of HashiCorp-maintained and community plugins from the Packer Core repository. They now live in their own multi-component plugin repositiores. The following repositories have been created, and their components have been deleted from the "github.com/hashicorp/packer" repository.
* "github.com/hashicorp/packer-plugin-alicloud" [GH-10932]
* "github.com/hashicorp/packer-plugin-amazon" [GH-10800]
* "github.com/hashicorp/packer-plugin-ansible" [GH-10912]
* "github.com/hashicorp/packer-plugin-azure" [GH-10979]
* "github.com/hashicorp/packer-plugin-chef" [GH-10921]
* "github.com/hashicorp/packer-plugin-cloudstack" [GH-10934]
* "github.com/hashicorp/packer-plugin-converge" [GH-10956]
* "github.com/hashicorp/packer-plugin-digitalocean" [GH-10961]
* "github.com/hashicorp/packer-plugin-docker" [GH-10695]
* "github.com/hashicorp/packer-plugin-googlecompute" [GH-10890]
* "github.com/hashicorp/packer-plugin-hcloud" [GH-10966]
* "github.com/hashicorp/packer-plugin-hyperone" [GH-10949]
* "github.com/hashicorp/packer-plugin-hyperv" [GH-10949]
* "github.com/hashicorp/packer-plugin-inspec"
* "github.com/hashicorp/packer-plugin-ionos-cloud"
* "github.com/hashicorp/packer-plugin-jdcloud" [GH-10946]
* "github.com/hashicorp/packer-plugin-linode" [GH-10947]
* "github.com/hashicorp/packer-plugin-lxc" [GH-10965]
* "github.com/hashicorp/packer-plugin-lxd" [GH-10965]
* "github.com/hashicorp/packer-plugin-ncloud" [GH-10937]
* "github.com/hashicorp/packer-plugin-openstack" [GH-10933]
* "github.com/hashicorp/packer-plugin-oracle" [GH-10962]
* "github.com/hashicorp/packer-plugin-outscale" [GH-10941]
* "github.com/hashicorp/packer-plugin-parallels" [GH-10936]
* "github.com/hashicorp/packer-plugin-proxmox" [GH-10930]
* "github.com/hashicorp/packer-plugin-puppet" [GH-10943]
* "github.com/hashicorp/packer-plugin-qemu" [GH-10929]
* "github.com/hashicorp/packer-plugin-salt"
* "github.com/hashicorp/packer-plugin-scaleway" [GH-10939]
* "github.com/hashicorp/packer-plugin-tencentcloud" [GH-10967]
* "github.com/hashicorp/packer-plugin-triton" [GH-10963]
* "github.com/hashicorp/packer-plugin-ucloud" [GH-10953]
* "github.com/hashicorp/packer-plugin-vagrant" [GH-10960]
* "github.com/hashicorp/packer-plugin-virtualbox" [GH-10910]
* "github.com/hashicorp/packer-plugin-vmware" [GH-10920]
* "github.com/hashicorp/packer-plugin-vsphere" [GH-10896]
* "github.com/hashicorp/packer-plugin-yandex" [GH-10970]
_this will not be a backwards-breaking change in v1.7.3_ because the extracted
components are being vendored back into Packer. However, we encourage users to
begin using `packer init` to download and install plugins to get the latest
updates to each plugin, and to prepare for Packer v2.0 when we will stop
vendoring the above plugins into the main Packer binary. The following
components will not be removed from the main packer binary:
* `null` builder
* `file` builder
* `breakpoint` provisioner
* `file` provisioner
* `powershell` provisioner
* `shell` provisioner
* `shell-local` provisioner
* `sleep` provisioner
* `windows-restart` provisioner
* `windows-shell` provisioner
* `artifice` post-processor
* `checksum` post-processor
* `compress` post-processor
* `manifest` post-processor
* `shell-local` post-processor
### Bug Fixes:
* builder/azure: Add `keep_os_disk` parameter to control OS disk deletion
[GH-10045]
* builder/azure: Stop SIG timout from being overridden by PollingDuration
[GH-10816]
* builder/azure: Support shared image gallery storage account type [GH-10863]
* builder/proxmox: Proxmox builder use ipv4 address instead of always ipv6.
[GH-10858]
* core/hcl: Fix Invalid provisioner pause_before panic [GH-10978]
* core: HCL "index" function now actually returns the index of the element
[GH-11008]
* core: Implemented DEFAULT_NAME handling for datasource plugins [GH-11026]
### Enhancements:
* builder/azure: Added custom nicname and osdiskname [GH-10938]
* builder/azure: Add support for shared image gallery storage account type
[GH-10863]
* builder/digitalocean: support ecdsa, ed25519, dsa temporary key types.
[GH-10856]
* builder/ncloud: Support ncloud vpc version [GH-10870]
* post-processor/compress: Add bzip2 support to post-processor [GH-10867]
* post-processor/googlecompute-import: Add Image Storage Locations field
[GH-10864]
* Removed the golang "vendor" directory in favor of go modules. This should not
affect end users. [GH-10916]
## 1.7.2 (April 05, 2021)
### IMPROVEMENTS:

21
builder/azure/LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

16
builder/azure/TODO.md Normal file
View File

@ -0,0 +1,16 @@
Here's a list of things we like to get done in no particular order:
- [ ] Blob/image copy post-processor
- [ ] Blob/image rename post-processor
- [ ] SSH to private ip through subnet
- [ ] chroot builder
- [ ] support cross-storage account image source (i.e. pre-build blob copy)
- [ ] look up object id when using device code (graph api /me ?)
- [ ] device flow support for Windows
- [x] look up tenant id in all cases (see device flow code)
- [ ] look up resource group of storage account
- [ ] include all _data_ disks in artifact too
- [ ] windows sysprep provisioner (since it seems to generate a certain issue volume)
- [ ] allow arbitrary json patching for deployment document
- [ ] tag all resources with user-supplied tag
- [ ] managed disk support

View File

@ -0,0 +1,118 @@
{
"$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')]",
"dependsOn": [],
"location": "[variables('location')]",
"name": "[variables('nicName')]",
"properties": {
"ipConfigurations": [
{
"name": "ipconfig",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[variables('subnetRef')]"
}
}
}
]
},
"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": "[parameters('osDiskName')]",
"osType": "Linux",
"vhd": {
"uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]"
}
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2015-06-15",
"location": "[resourceGroup().location]",
"publicIPAddressType": "Dynamic",
"sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]",
"subnetAddressPrefix": "10.0.0.0/24",
"subnetName": "ignore",
"subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]",
"virtualNetworkName": "ignore",
"virtualNetworkResourceGroup": "ignore",
"vmStorageAccountContainerName": "images",
"vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
}
}

View File

@ -0,0 +1,231 @@
package arm
import (
"bytes"
"fmt"
"net/url"
"path"
"strings"
)
const (
BuilderId = "Azure.ResourceManagement.VMImage"
)
type AdditionalDiskArtifact struct {
AdditionalDiskUri string
AdditionalDiskUriReadOnlySas string
}
type Artifact struct {
// OS type: Linux, Windows
OSType string
// VHD
StorageAccountLocation string
OSDiskUri string
TemplateUri string
OSDiskUriReadOnlySas string
TemplateUriReadOnlySas string
// Managed Image
ManagedImageResourceGroupName string
ManagedImageName string
ManagedImageLocation string
ManagedImageId string
ManagedImageOSDiskSnapshotName string
ManagedImageDataDiskSnapshotPrefix string
// ARM resource id for Shared Image Gallery
ManagedImageSharedImageGalleryId string
// Additional Disks
AdditionalDisks *[]AdditionalDiskArtifact
// StateData should store data such as GeneratedData
// to be shared with post-processors
StateData map[string]interface{}
}
func NewManagedImageArtifact(osType, resourceGroup, name, location, id, osDiskSnapshotName, dataDiskSnapshotPrefix string, generatedData map[string]interface{}) (*Artifact, error) {
return &Artifact{
ManagedImageResourceGroupName: resourceGroup,
ManagedImageName: name,
ManagedImageLocation: location,
ManagedImageId: id,
OSType: osType,
ManagedImageOSDiskSnapshotName: osDiskSnapshotName,
ManagedImageDataDiskSnapshotPrefix: dataDiskSnapshotPrefix,
StateData: generatedData,
}, nil
}
func NewManagedImageArtifactWithSIGAsDestination(osType, resourceGroup, name, location, id, osDiskSnapshotName, dataDiskSnapshotPrefix, destinationSharedImageGalleryId string, generatedData map[string]interface{}) (*Artifact, error) {
return &Artifact{
ManagedImageResourceGroupName: resourceGroup,
ManagedImageName: name,
ManagedImageLocation: location,
ManagedImageId: id,
OSType: osType,
ManagedImageOSDiskSnapshotName: osDiskSnapshotName,
ManagedImageDataDiskSnapshotPrefix: dataDiskSnapshotPrefix,
ManagedImageSharedImageGalleryId: destinationSharedImageGalleryId,
StateData: generatedData,
}, nil
}
func NewArtifact(template *CaptureTemplate, getSasUrl func(name string) string, osType string, generatedData map[string]interface{}) (*Artifact, error) {
if template == nil {
return nil, fmt.Errorf("nil capture template")
}
if len(template.Resources) != 1 {
return nil, fmt.Errorf("malformed capture template, expected one resource")
}
vhdUri, err := url.Parse(template.Resources[0].Properties.StorageProfile.OSDisk.Image.Uri)
if err != nil {
return nil, err
}
templateUri, err := storageUriToTemplateUri(vhdUri)
if err != nil {
return nil, err
}
var additional_disks *[]AdditionalDiskArtifact
if template.Resources[0].Properties.StorageProfile.DataDisks != nil {
data_disks := make([]AdditionalDiskArtifact, len(template.Resources[0].Properties.StorageProfile.DataDisks))
for i, additionaldisk := range template.Resources[0].Properties.StorageProfile.DataDisks {
additionalVhdUri, err := url.Parse(additionaldisk.Image.Uri)
if err != nil {
return nil, err
}
data_disks[i].AdditionalDiskUri = additionalVhdUri.String()
data_disks[i].AdditionalDiskUriReadOnlySas = getSasUrl(getStorageUrlPath(additionalVhdUri))
}
additional_disks = &data_disks
}
return &Artifact{
OSType: osType,
OSDiskUri: vhdUri.String(),
OSDiskUriReadOnlySas: getSasUrl(getStorageUrlPath(vhdUri)),
TemplateUri: templateUri.String(),
TemplateUriReadOnlySas: getSasUrl(getStorageUrlPath(templateUri)),
AdditionalDisks: additional_disks,
StorageAccountLocation: template.Resources[0].Location,
StateData: generatedData,
}, nil
}
func getStorageUrlPath(u *url.URL) string {
parts := strings.Split(u.Path, "/")
return strings.Join(parts[3:], "/")
}
func storageUriToTemplateUri(su *url.URL) (*url.URL, error) {
// packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd -> 4085bb15-3644-4641-b9cd-f575918640b4
filename := path.Base(su.Path)
parts := strings.Split(filename, ".")
if len(parts) < 3 {
return nil, fmt.Errorf("malformed URL")
}
// packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd -> packer
prefixParts := strings.Split(parts[0], "-")
prefix := strings.Join(prefixParts[:len(prefixParts)-1], "-")
templateFilename := fmt.Sprintf("%s-vmTemplate.%s.json", prefix, parts[1])
// https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd"
// ->
// https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json"
return url.Parse(strings.Replace(su.String(), filename, templateFilename, 1))
}
func (a *Artifact) isManagedImage() bool {
return a.ManagedImageResourceGroupName != ""
}
func (*Artifact) BuilderId() string {
return BuilderId
}
func (*Artifact) Files() []string {
return []string{}
}
func (a *Artifact) Id() string {
if a.OSDiskUri != "" {
return a.OSDiskUri
}
return a.ManagedImageId
}
func (a *Artifact) State(name string) interface{} {
if _, ok := a.StateData[name]; ok {
return a.StateData[name]
}
switch name {
case "atlas.artifact.metadata":
return a.stateAtlasMetadata()
default:
return nil
}
}
func (a *Artifact) String() string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("%s:\n\n", a.BuilderId()))
buf.WriteString(fmt.Sprintf("OSType: %s\n", a.OSType))
if a.isManagedImage() {
buf.WriteString(fmt.Sprintf("ManagedImageResourceGroupName: %s\n", a.ManagedImageResourceGroupName))
buf.WriteString(fmt.Sprintf("ManagedImageName: %s\n", a.ManagedImageName))
buf.WriteString(fmt.Sprintf("ManagedImageId: %s\n", a.ManagedImageId))
buf.WriteString(fmt.Sprintf("ManagedImageLocation: %s\n", a.ManagedImageLocation))
if a.ManagedImageOSDiskSnapshotName != "" {
buf.WriteString(fmt.Sprintf("ManagedImageOSDiskSnapshotName: %s\n", a.ManagedImageOSDiskSnapshotName))
}
if a.ManagedImageDataDiskSnapshotPrefix != "" {
buf.WriteString(fmt.Sprintf("ManagedImageDataDiskSnapshotPrefix: %s\n", a.ManagedImageDataDiskSnapshotPrefix))
}
if a.ManagedImageSharedImageGalleryId != "" {
buf.WriteString(fmt.Sprintf("ManagedImageSharedImageGalleryId: %s\n", a.ManagedImageSharedImageGalleryId))
}
} else {
buf.WriteString(fmt.Sprintf("StorageAccountLocation: %s\n", a.StorageAccountLocation))
buf.WriteString(fmt.Sprintf("OSDiskUri: %s\n", a.OSDiskUri))
buf.WriteString(fmt.Sprintf("OSDiskUriReadOnlySas: %s\n", a.OSDiskUriReadOnlySas))
buf.WriteString(fmt.Sprintf("TemplateUri: %s\n", a.TemplateUri))
buf.WriteString(fmt.Sprintf("TemplateUriReadOnlySas: %s\n", a.TemplateUriReadOnlySas))
if a.AdditionalDisks != nil {
for i, additionaldisk := range *a.AdditionalDisks {
buf.WriteString(fmt.Sprintf("AdditionalDiskUri (datadisk-%d): %s\n", i+1, additionaldisk.AdditionalDiskUri))
buf.WriteString(fmt.Sprintf("AdditionalDiskUriReadOnlySas (datadisk-%d): %s\n", i+1, additionaldisk.AdditionalDiskUriReadOnlySas))
}
}
}
return buf.String()
}
func (*Artifact) Destroy() error {
return nil
}
func (a *Artifact) stateAtlasMetadata() interface{} {
metadata := make(map[string]string)
metadata["StorageAccountLocation"] = a.StorageAccountLocation
metadata["OSDiskUri"] = a.OSDiskUri
metadata["OSDiskUriReadOnlySas"] = a.OSDiskUriReadOnlySas
metadata["TemplateUri"] = a.TemplateUri
metadata["TemplateUriReadOnlySas"] = a.TemplateUriReadOnlySas
return metadata
}

View File

@ -0,0 +1,429 @@
package arm
import (
"fmt"
"strings"
"testing"
)
func getFakeSasUrl(name string) string {
return fmt.Sprintf("SAS-%s", name)
}
func generatedData() map[string]interface{} {
return make(map[string]interface{})
}
func TestArtifactIdVHD(t *testing.T) {
template := CaptureTemplate{
Resources: []CaptureResources{
{
Properties: CaptureProperties{
StorageProfile: CaptureStorageProfile{
OSDisk: CaptureDisk{
Image: CaptureUri{
Uri: "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd",
},
},
},
},
Location: "southcentralus",
},
},
}
artifact, err := NewArtifact(&template, getFakeSasUrl, "Linux", generatedData())
if err != nil {
t.Fatalf("err=%s", err)
}
expected := "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd"
result := artifact.Id()
if result != expected {
t.Fatalf("bad: %s", result)
}
}
func TestArtifactIDManagedImage(t *testing.T) {
artifact, err := NewManagedImageArtifact("Linux", "fakeResourceGroup", "fakeName", "fakeLocation", "fakeID", "fakeOsDiskSnapshotName", "fakeDataDiskSnapshotPrefix", generatedData())
if err != nil {
t.Fatalf("err=%s", err)
}
expected := `Azure.ResourceManagement.VMImage:
OSType: Linux
ManagedImageResourceGroupName: fakeResourceGroup
ManagedImageName: fakeName
ManagedImageId: fakeID
ManagedImageLocation: fakeLocation
ManagedImageOSDiskSnapshotName: fakeOsDiskSnapshotName
ManagedImageDataDiskSnapshotPrefix: fakeDataDiskSnapshotPrefix
`
result := artifact.String()
if result != expected {
t.Fatalf("bad: %s", result)
}
}
func TestArtifactIDManagedImageWithoutOSDiskSnapshotName(t *testing.T) {
artifact, err := NewManagedImageArtifact("Linux", "fakeResourceGroup", "fakeName", "fakeLocation", "fakeID", "", "fakeDataDiskSnapshotPrefix", generatedData())
if err != nil {
t.Fatalf("err=%s", err)
}
expected := `Azure.ResourceManagement.VMImage:
OSType: Linux
ManagedImageResourceGroupName: fakeResourceGroup
ManagedImageName: fakeName
ManagedImageId: fakeID
ManagedImageLocation: fakeLocation
ManagedImageDataDiskSnapshotPrefix: fakeDataDiskSnapshotPrefix
`
result := artifact.String()
if result != expected {
t.Fatalf("bad: %s", result)
}
}
func TestArtifactIDManagedImageWithoutDataDiskSnapshotPrefix(t *testing.T) {
artifact, err := NewManagedImageArtifact("Linux", "fakeResourceGroup", "fakeName", "fakeLocation", "fakeID", "fakeOsDiskSnapshotName", "", generatedData())
if err != nil {
t.Fatalf("err=%s", err)
}
expected := `Azure.ResourceManagement.VMImage:
OSType: Linux
ManagedImageResourceGroupName: fakeResourceGroup
ManagedImageName: fakeName
ManagedImageId: fakeID
ManagedImageLocation: fakeLocation
ManagedImageOSDiskSnapshotName: fakeOsDiskSnapshotName
`
result := artifact.String()
if result != expected {
t.Fatalf("bad: %s", result)
}
}
func TestArtifactIDManagedImageWithSharedImageGalleryId(t *testing.T) {
artifact, err := NewManagedImageArtifactWithSIGAsDestination("Linux", "fakeResourceGroup", "fakeName", "fakeLocation", "fakeID", "fakeOsDiskSnapshotName", "fakeDataDiskSnapshotPrefix", "fakeSharedImageGallery", generatedData())
if err != nil {
t.Fatalf("err=%s", err)
}
expected := `Azure.ResourceManagement.VMImage:
OSType: Linux
ManagedImageResourceGroupName: fakeResourceGroup
ManagedImageName: fakeName
ManagedImageId: fakeID
ManagedImageLocation: fakeLocation
ManagedImageOSDiskSnapshotName: fakeOsDiskSnapshotName
ManagedImageDataDiskSnapshotPrefix: fakeDataDiskSnapshotPrefix
ManagedImageSharedImageGalleryId: fakeSharedImageGallery
`
result := artifact.String()
if result != expected {
t.Fatalf("bad: %s", result)
}
}
func TestArtifactString(t *testing.T) {
template := CaptureTemplate{
Resources: []CaptureResources{
{
Properties: CaptureProperties{
StorageProfile: CaptureStorageProfile{
OSDisk: CaptureDisk{
Image: CaptureUri{
Uri: "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd",
},
},
},
},
Location: "southcentralus",
},
},
}
artifact, err := NewArtifact(&template, getFakeSasUrl, "Linux", generatedData())
if err != nil {
t.Fatalf("err=%s", err)
}
testSubject := artifact.String()
if !strings.Contains(testSubject, "OSDiskUri: https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd") {
t.Errorf("Expected String() output to contain OSDiskUri")
}
if !strings.Contains(testSubject, "OSDiskUriReadOnlySas: SAS-Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd") {
t.Errorf("Expected String() output to contain OSDiskUriReadOnlySas")
}
if !strings.Contains(testSubject, "TemplateUri: https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json") {
t.Errorf("Expected String() output to contain TemplateUri")
}
if !strings.Contains(testSubject, "TemplateUriReadOnlySas: SAS-Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json") {
t.Errorf("Expected String() output to contain TemplateUriReadOnlySas")
}
if !strings.Contains(testSubject, "StorageAccountLocation: southcentralus") {
t.Errorf("Expected String() output to contain StorageAccountLocation")
}
if !strings.Contains(testSubject, "OSType: Linux") {
t.Errorf("Expected String() output to contain OSType")
}
}
func TestAdditionalDiskArtifactString(t *testing.T) {
template := CaptureTemplate{
Resources: []CaptureResources{
{
Properties: CaptureProperties{
StorageProfile: CaptureStorageProfile{
OSDisk: CaptureDisk{
Image: CaptureUri{
Uri: "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd",
},
},
DataDisks: []CaptureDisk{
{
Image: CaptureUri{
Uri: "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd",
},
},
},
},
},
Location: "southcentralus",
},
},
}
artifact, err := NewArtifact(&template, getFakeSasUrl, "Linux", generatedData())
if err != nil {
t.Fatalf("err=%s", err)
}
testSubject := artifact.String()
if !strings.Contains(testSubject, "OSDiskUri: https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd") {
t.Errorf("Expected String() output to contain OSDiskUri")
}
if !strings.Contains(testSubject, "OSDiskUriReadOnlySas: SAS-Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd") {
t.Errorf("Expected String() output to contain OSDiskUriReadOnlySas")
}
if !strings.Contains(testSubject, "TemplateUri: https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json") {
t.Errorf("Expected String() output to contain TemplateUri")
}
if !strings.Contains(testSubject, "TemplateUriReadOnlySas: SAS-Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json") {
t.Errorf("Expected String() output to contain TemplateUriReadOnlySas")
}
if !strings.Contains(testSubject, "StorageAccountLocation: southcentralus") {
t.Errorf("Expected String() output to contain StorageAccountLocation")
}
if !strings.Contains(testSubject, "OSType: Linux") {
t.Errorf("Expected String() output to contain OSType")
}
if !strings.Contains(testSubject, "AdditionalDiskUri (datadisk-1): https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd") {
t.Errorf("Expected String() output to contain AdditionalDiskUri")
}
if !strings.Contains(testSubject, "AdditionalDiskUriReadOnlySas (datadisk-1): SAS-Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd") {
t.Errorf("Expected String() output to contain AdditionalDiskUriReadOnlySas")
}
}
func TestArtifactProperties(t *testing.T) {
template := CaptureTemplate{
Resources: []CaptureResources{
{
Properties: CaptureProperties{
StorageProfile: CaptureStorageProfile{
OSDisk: CaptureDisk{
Image: CaptureUri{
Uri: "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd",
},
},
},
},
Location: "southcentralus",
},
},
}
testSubject, err := NewArtifact(&template, getFakeSasUrl, "Linux", generatedData())
if err != nil {
t.Fatalf("err=%s", err)
}
if testSubject.OSDiskUri != "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd" {
t.Errorf("Expected template to be 'https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd', but got %s", testSubject.OSDiskUri)
}
if testSubject.OSDiskUriReadOnlySas != "SAS-Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd" {
t.Errorf("Expected template to be 'SAS-Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd', but got %s", testSubject.OSDiskUriReadOnlySas)
}
if testSubject.TemplateUri != "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json" {
t.Errorf("Expected template to be 'https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json', but got %s", testSubject.TemplateUri)
}
if testSubject.TemplateUriReadOnlySas != "SAS-Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json" {
t.Errorf("Expected template to be 'SAS-Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json', but got %s", testSubject.TemplateUriReadOnlySas)
}
if testSubject.StorageAccountLocation != "southcentralus" {
t.Errorf("Expected StorageAccountLocation to be 'southcentral', but got %s", testSubject.StorageAccountLocation)
}
if testSubject.OSType != "Linux" {
t.Errorf("Expected OSType to be 'Linux', but got %s", testSubject.OSType)
}
}
func TestAdditionalDiskArtifactProperties(t *testing.T) {
template := CaptureTemplate{
Resources: []CaptureResources{
{
Properties: CaptureProperties{
StorageProfile: CaptureStorageProfile{
OSDisk: CaptureDisk{
Image: CaptureUri{
Uri: "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd",
},
},
DataDisks: []CaptureDisk{
{
Image: CaptureUri{
Uri: "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd",
},
},
},
},
},
Location: "southcentralus",
},
},
}
testSubject, err := NewArtifact(&template, getFakeSasUrl, "Linux", generatedData())
if err != nil {
t.Fatalf("err=%s", err)
}
if testSubject.OSDiskUri != "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd" {
t.Errorf("Expected template to be 'https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd', but got %s", testSubject.OSDiskUri)
}
if testSubject.OSDiskUriReadOnlySas != "SAS-Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd" {
t.Errorf("Expected template to be 'SAS-Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd', but got %s", testSubject.OSDiskUriReadOnlySas)
}
if testSubject.TemplateUri != "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json" {
t.Errorf("Expected template to be 'https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json', but got %s", testSubject.TemplateUri)
}
if testSubject.TemplateUriReadOnlySas != "SAS-Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json" {
t.Errorf("Expected template to be 'SAS-Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json', but got %s", testSubject.TemplateUriReadOnlySas)
}
if testSubject.StorageAccountLocation != "southcentralus" {
t.Errorf("Expected StorageAccountLocation to be 'southcentral', but got %s", testSubject.StorageAccountLocation)
}
if testSubject.OSType != "Linux" {
t.Errorf("Expected OSType to be 'Linux', but got %s", testSubject.OSType)
}
if testSubject.AdditionalDisks == nil {
t.Errorf("Expected AdditionalDisks to be not nil")
}
if len(*testSubject.AdditionalDisks) != 1 {
t.Errorf("Expected AdditionalDisks to have one additional disk, but got %d", len(*testSubject.AdditionalDisks))
}
if (*testSubject.AdditionalDisks)[0].AdditionalDiskUri != "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd" {
t.Errorf("Expected additional disk uri to be 'https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd', but got %s", (*testSubject.AdditionalDisks)[0].AdditionalDiskUri)
}
if (*testSubject.AdditionalDisks)[0].AdditionalDiskUriReadOnlySas != "SAS-Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd" {
t.Errorf("Expected additional disk sas to be 'SAS-Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd', but got %s", (*testSubject.AdditionalDisks)[0].AdditionalDiskUriReadOnlySas)
}
}
func TestArtifactOverHyphenatedCaptureUri(t *testing.T) {
template := CaptureTemplate{
Resources: []CaptureResources{
{
Properties: CaptureProperties{
StorageProfile: CaptureStorageProfile{
OSDisk: CaptureDisk{
Image: CaptureUri{
Uri: "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/pac-ker-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd",
},
},
},
},
Location: "southcentralus",
},
},
}
testSubject, err := NewArtifact(&template, getFakeSasUrl, "Linux", generatedData())
if err != nil {
t.Fatalf("err=%s", err)
}
if testSubject.TemplateUri != "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/pac-ker-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json" {
t.Errorf("Expected template to be 'https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/pac-ker-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json', but got %s", testSubject.TemplateUri)
}
}
func TestArtifactRejectMalformedTemplates(t *testing.T) {
template := CaptureTemplate{}
_, err := NewArtifact(&template, getFakeSasUrl, "Linux", generatedData())
if err == nil {
t.Fatalf("Expected artifact creation to fail, but it succeeded.")
}
}
func TestArtifactRejectMalformedStorageUri(t *testing.T) {
template := CaptureTemplate{
Resources: []CaptureResources{
{
Properties: CaptureProperties{
StorageProfile: CaptureStorageProfile{
OSDisk: CaptureDisk{
Image: CaptureUri{
Uri: "bark",
},
},
},
},
},
},
}
_, err := NewArtifact(&template, getFakeSasUrl, "Linux", generatedData())
if err == nil {
t.Fatalf("Expected artifact creation to fail, but it succeeded.")
}
}
func TestArtifactState_StateData(t *testing.T) {
expectedData := "this is the data"
artifact := &Artifact{
StateData: map[string]interface{}{"state_data": expectedData},
}
// Valid state
result := artifact.State("state_data")
if result != expectedData {
t.Fatalf("Bad: State data was %s instead of %s", result, expectedData)
}
// Invalid state
result = artifact.State("invalid_key")
if result != nil {
t.Fatalf("Bad: State should be nil for invalid state data name")
}
// Nil StateData should not fail and should return nil
artifact = &Artifact{}
result = artifact.State("key")
if result != nil {
t.Fatalf("Bad: State should be nil for nil StateData")
}
}

View File

@ -0,0 +1,311 @@
package arm
import (
"context"
"encoding/json"
"fmt"
"math"
"net/http"
"net/url"
"os"
"strconv"
"time"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
newCompute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-03-01/compute"
"github.com/Azure/azure-sdk-for-go/services/keyvault/mgmt/2018-02-14/keyvault"
"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-01-01/network"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-02-01/resources"
armStorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2017-10-01/storage"
"github.com/Azure/azure-sdk-for-go/storage"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/hashicorp/packer-plugin-sdk/useragent"
"github.com/hashicorp/packer/builder/azure/common"
"github.com/hashicorp/packer/builder/azure/version"
)
const (
EnvPackerLogAzureMaxLen = "PACKER_LOG_AZURE_MAXLEN"
)
type AzureClient struct {
storage.BlobStorageClient
resources.DeploymentsClient
resources.DeploymentOperationsClient
resources.GroupsClient
network.PublicIPAddressesClient
network.InterfacesClient
network.SubnetsClient
network.VirtualNetworksClient
network.SecurityGroupsClient
compute.ImagesClient
compute.VirtualMachinesClient
common.VaultClient
armStorage.AccountsClient
compute.DisksClient
compute.SnapshotsClient
newCompute.GalleryImageVersionsClient
newCompute.GalleryImagesClient
InspectorMaxLength int
Template *CaptureTemplate
LastError azureErrorResponse
VaultClientDelete keyvault.VaultsClient
}
func getCaptureResponse(body string) *CaptureTemplate {
var operation CaptureOperation
err := json.Unmarshal([]byte(body), &operation)
if err != nil {
return nil
}
if operation.Properties != nil && operation.Properties.Output != nil {
return operation.Properties.Output
}
return nil
}
// HACK(chrboum): This method is a hack. It was written to work around this issue
// (https://github.com/Azure/azure-sdk-for-go/issues/307) and to an extent this
// issue (https://github.com/Azure/azure-rest-api-specs/issues/188).
//
// Capturing a VM is a long running operation that requires polling. There are
// couple different forms of polling, and the end result of a poll operation is
// discarded by the SDK. It is expected that any discarded data can be re-fetched,
// so discarding it has minimal impact. Unfortunately, there is no way to re-fetch
// the template returned by a capture call that I am aware of.
//
// If the second issue were fixed the VM ID would be included when GET'ing a VM. The
// VM ID could be used to locate the captured VHD, and captured template.
// Unfortunately, the VM ID is not included so this method cannot be used either.
//
// This code captures the template and saves it to the client (the AzureClient type).
// It expects that the capture API is called only once, or rather you only care that the
// last call's value is important because subsequent requests are not persisted. There
// is no care given to multiple threads writing this value because for our use case
// it does not matter.
func templateCapture(client *AzureClient) autorest.RespondDecorator {
return func(r autorest.Responder) autorest.Responder {
return autorest.ResponderFunc(func(resp *http.Response) error {
body, bodyString := handleBody(resp.Body, math.MaxInt64)
resp.Body = body
captureTemplate := getCaptureResponse(bodyString)
if captureTemplate != nil {
client.Template = captureTemplate
}
return r.Respond(resp)
})
}
}
func errorCapture(client *AzureClient) autorest.RespondDecorator {
return func(r autorest.Responder) autorest.Responder {
return autorest.ResponderFunc(func(resp *http.Response) error {
body, bodyString := handleBody(resp.Body, math.MaxInt64)
resp.Body = body
errorResponse := newAzureErrorResponse(bodyString)
if errorResponse != nil {
client.LastError = *errorResponse
}
return r.Respond(resp)
})
}
}
// WAITING(chrboum): I have logged https://github.com/Azure/azure-sdk-for-go/issues/311 to get this
// method included in the SDK. It has been accepted, and I'll cut over to the official way
// once it ships.
func byConcatDecorators(decorators ...autorest.RespondDecorator) autorest.RespondDecorator {
return func(r autorest.Responder) autorest.Responder {
return autorest.DecorateResponder(r, decorators...)
}
}
func NewAzureClient(subscriptionID, sigSubscriptionID, resourceGroupName, storageAccountName string,
cloud *azure.Environment, sharedGalleryTimeout time.Duration, pollingDuration time.Duration,
servicePrincipalToken, servicePrincipalTokenVault *adal.ServicePrincipalToken) (*AzureClient, error) {
var azureClient = &AzureClient{}
maxlen := getInspectorMaxLength()
azureClient.DeploymentsClient = resources.NewDeploymentsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.DeploymentsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.DeploymentsClient.RequestInspector = withInspection(maxlen)
azureClient.DeploymentsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.DeploymentsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.DeploymentsClient.UserAgent)
azureClient.DeploymentsClient.Client.PollingDuration = pollingDuration
azureClient.DeploymentOperationsClient = resources.NewDeploymentOperationsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.DeploymentOperationsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.DeploymentOperationsClient.RequestInspector = withInspection(maxlen)
azureClient.DeploymentOperationsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.DeploymentOperationsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.DeploymentOperationsClient.UserAgent)
azureClient.DeploymentOperationsClient.Client.PollingDuration = pollingDuration
azureClient.DisksClient = compute.NewDisksClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.DisksClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.DisksClient.RequestInspector = withInspection(maxlen)
azureClient.DisksClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.DisksClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.DisksClient.UserAgent)
azureClient.DisksClient.Client.PollingDuration = pollingDuration
azureClient.GroupsClient = resources.NewGroupsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.GroupsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.GroupsClient.RequestInspector = withInspection(maxlen)
azureClient.GroupsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.GroupsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.GroupsClient.UserAgent)
azureClient.GroupsClient.Client.PollingDuration = pollingDuration
azureClient.ImagesClient = compute.NewImagesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.ImagesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.ImagesClient.RequestInspector = withInspection(maxlen)
azureClient.ImagesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.ImagesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.ImagesClient.UserAgent)
azureClient.ImagesClient.Client.PollingDuration = pollingDuration
azureClient.InterfacesClient = network.NewInterfacesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.InterfacesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.InterfacesClient.RequestInspector = withInspection(maxlen)
azureClient.InterfacesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.InterfacesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.InterfacesClient.UserAgent)
azureClient.InterfacesClient.Client.PollingDuration = pollingDuration
azureClient.SubnetsClient = network.NewSubnetsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.SubnetsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.SubnetsClient.RequestInspector = withInspection(maxlen)
azureClient.SubnetsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.SubnetsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.SubnetsClient.UserAgent)
azureClient.SubnetsClient.Client.PollingDuration = pollingDuration
azureClient.VirtualNetworksClient = network.NewVirtualNetworksClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.VirtualNetworksClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.VirtualNetworksClient.RequestInspector = withInspection(maxlen)
azureClient.VirtualNetworksClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.VirtualNetworksClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.VirtualNetworksClient.UserAgent)
azureClient.VirtualNetworksClient.Client.PollingDuration = pollingDuration
azureClient.SecurityGroupsClient = network.NewSecurityGroupsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.SecurityGroupsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.SecurityGroupsClient.RequestInspector = withInspection(maxlen)
azureClient.SecurityGroupsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.SecurityGroupsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.SecurityGroupsClient.UserAgent)
azureClient.PublicIPAddressesClient = network.NewPublicIPAddressesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.PublicIPAddressesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.PublicIPAddressesClient.RequestInspector = withInspection(maxlen)
azureClient.PublicIPAddressesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.PublicIPAddressesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.PublicIPAddressesClient.UserAgent)
azureClient.PublicIPAddressesClient.Client.PollingDuration = pollingDuration
azureClient.VirtualMachinesClient = compute.NewVirtualMachinesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.VirtualMachinesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.VirtualMachinesClient.RequestInspector = withInspection(maxlen)
azureClient.VirtualMachinesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), templateCapture(azureClient), errorCapture(azureClient))
azureClient.VirtualMachinesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.VirtualMachinesClient.UserAgent)
azureClient.VirtualMachinesClient.Client.PollingDuration = pollingDuration
azureClient.SnapshotsClient = compute.NewSnapshotsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.SnapshotsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.SnapshotsClient.RequestInspector = withInspection(maxlen)
azureClient.SnapshotsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.SnapshotsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.SnapshotsClient.UserAgent)
azureClient.SnapshotsClient.Client.PollingDuration = pollingDuration
azureClient.AccountsClient = armStorage.NewAccountsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.AccountsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.AccountsClient.RequestInspector = withInspection(maxlen)
azureClient.AccountsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.AccountsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.AccountsClient.UserAgent)
azureClient.AccountsClient.Client.PollingDuration = pollingDuration
azureClient.GalleryImageVersionsClient = newCompute.NewGalleryImageVersionsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.GalleryImageVersionsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.GalleryImageVersionsClient.RequestInspector = withInspection(maxlen)
azureClient.GalleryImageVersionsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.GalleryImageVersionsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.GalleryImageVersionsClient.UserAgent)
azureClient.GalleryImageVersionsClient.Client.PollingDuration = sharedGalleryTimeout
if sigSubscriptionID != "" {
azureClient.GalleryImageVersionsClient.SubscriptionID = sigSubscriptionID
}
azureClient.GalleryImagesClient = newCompute.NewGalleryImagesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
azureClient.GalleryImagesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.GalleryImagesClient.RequestInspector = withInspection(maxlen)
azureClient.GalleryImagesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.GalleryImagesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.GalleryImagesClient.UserAgent)
azureClient.GalleryImagesClient.Client.PollingDuration = pollingDuration
if sigSubscriptionID != "" {
azureClient.GalleryImagesClient.SubscriptionID = sigSubscriptionID
}
keyVaultURL, err := url.Parse(cloud.KeyVaultEndpoint)
if err != nil {
return nil, err
}
azureClient.VaultClient = common.NewVaultClient(*keyVaultURL)
azureClient.VaultClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalTokenVault)
azureClient.VaultClient.RequestInspector = withInspection(maxlen)
azureClient.VaultClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.VaultClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.VaultClient.UserAgent)
azureClient.VaultClient.Client.PollingDuration = pollingDuration
// This client is different than the above because it manages the vault
// itself rather than the contents of the vault.
azureClient.VaultClientDelete = keyvault.NewVaultsClient(subscriptionID)
azureClient.VaultClientDelete.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
azureClient.VaultClientDelete.RequestInspector = withInspection(maxlen)
azureClient.VaultClientDelete.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
azureClient.VaultClientDelete.UserAgent = fmt.Sprintf("%s %s", useragent.String(version.AzurePluginVersion.FormattedVersion()), azureClient.VaultClientDelete.UserAgent)
azureClient.VaultClientDelete.Client.PollingDuration = pollingDuration
// If this is a managed disk build, this should be ignored.
if resourceGroupName != "" && storageAccountName != "" {
accountKeys, err := azureClient.AccountsClient.ListKeys(context.TODO(), resourceGroupName, storageAccountName)
if err != nil {
return nil, err
}
storageClient, err := storage.NewClient(
storageAccountName,
*(*accountKeys.Keys)[0].Value,
cloud.StorageEndpointSuffix,
storage.DefaultAPIVersion,
true /*useHttps*/)
if err != nil {
return nil, err
}
azureClient.BlobStorageClient = storageClient.GetBlobService()
}
return azureClient, nil
}
func getInspectorMaxLength() int64 {
value, ok := os.LookupEnv(EnvPackerLogAzureMaxLen)
if !ok {
return math.MaxInt64
}
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return 0
}
if i < 0 {
return 0
}
return i
}

View File

@ -0,0 +1,67 @@
package arm
import (
"bytes"
"encoding/json"
"fmt"
)
type azureErrorDetails struct {
Code string `json:"code"`
Message string `json:"message"`
Details []azureErrorDetails `json:"details"`
}
type azureErrorResponse struct {
ErrorDetails azureErrorDetails `json:"error"`
}
func newAzureErrorResponse(s string) *azureErrorResponse {
var errorResponse azureErrorResponse
err := json.Unmarshal([]byte(s), &errorResponse)
if err == nil {
return &errorResponse
}
return nil
}
func (e *azureErrorDetails) isEmpty() bool {
return e.Code == ""
}
func (e *azureErrorResponse) isEmpty() bool {
return e.ErrorDetails.isEmpty()
}
func (e *azureErrorResponse) Error() string {
var buf bytes.Buffer
//buf.WriteString("-=-=- ERROR -=-=-")
formatAzureErrorResponse(e.ErrorDetails, &buf, "")
//buf.WriteString("-=-=- ERROR -=-=-")
return buf.String()
}
// format a Azure Error Response by recursing through the JSON structure.
//
// Errors may contain nested errors, which are JSON documents that have been
// serialized and escaped. Keep following this nesting all the way down...
func formatAzureErrorResponse(error azureErrorDetails, buf *bytes.Buffer, indent string) {
if error.isEmpty() {
return
}
buf.WriteString(fmt.Sprintf("ERROR: %s-> %s : %s\n", indent, error.Code, error.Message))
for _, x := range error.Details {
newIndent := fmt.Sprintf("%s ", indent)
var aer azureErrorResponse
err := json.Unmarshal([]byte(x.Message), &aer)
if err == nil {
buf.WriteString(fmt.Sprintf("ERROR: %s-> %s\n", newIndent, x.Code))
formatAzureErrorResponse(aer.ErrorDetails, buf, newIndent)
} else {
buf.WriteString(fmt.Sprintf("ERROR: %s-> %s : %s\n", newIndent, x.Code, x.Message))
}
}
}

View File

@ -0,0 +1,4 @@
ERROR: -> DeploymentFailed : At least one resource deployment operation failed. Please list deployment operations for details. Please see https://aka.ms/arm-debug for usage details.
ERROR: -> BadRequest
ERROR: -> InvalidRequestFormat : Cannot parse the request.
ERROR: -> InvalidJson : Error converting value "playground" to type 'Microsoft.WindowsAzure.Networking.Nrp.Frontend.Contract.Csm.Public.IpAllocationMethod'. Path 'properties.publicIPAllocationMethod', line 1, position 130.

View File

@ -0,0 +1 @@
ERROR: -> ResourceNotFound : The Resource 'Microsoft.Compute/images/PackerUbuntuImage' under resource group 'packer-test00' was not found.

View File

@ -0,0 +1,77 @@
package arm
import (
"strings"
"testing"
approvaltests "github.com/approvals/go-approval-tests"
"github.com/hashicorp/packer-plugin-sdk/json"
)
const AzureErrorSimple = `{"error":{"code":"ResourceNotFound","message":"The Resource 'Microsoft.Compute/images/PackerUbuntuImage' under resource group 'packer-test00' was not found."}}`
const AzureErrorNested = `{"status":"Failed","error":{"code":"DeploymentFailed","message":"At least one resource deployment operation failed. Please list deployment operations for details. Please see https://aka.ms/arm-debug for usage details.","details":[{"code":"BadRequest","message":"{\r\n \"error\": {\r\n \"code\": \"InvalidRequestFormat\",\r\n \"message\": \"Cannot parse the request.\",\r\n \"details\": [\r\n {\r\n \"code\": \"InvalidJson\",\r\n \"message\": \"Error converting value \\\"playground\\\" to type 'Microsoft.WindowsAzure.Networking.Nrp.Frontend.Contract.Csm.Public.IpAllocationMethod'. Path 'properties.publicIPAllocationMethod', line 1, position 130.\"\r\n }\r\n ]\r\n }\r\n}"}]}}`
func TestAzureErrorSimpleShouldUnmarshal(t *testing.T) {
var azureErrorResponse azureErrorResponse
err := json.Unmarshal([]byte(AzureErrorSimple), &azureErrorResponse)
if err != nil {
t.Fatal(err)
}
if azureErrorResponse.ErrorDetails.Code != "ResourceNotFound" {
t.Errorf("Error.Code")
}
if azureErrorResponse.ErrorDetails.Message != "The Resource 'Microsoft.Compute/images/PackerUbuntuImage' under resource group 'packer-test00' was not found." {
t.Errorf("Error.Message")
}
}
func TestAzureErrorNestedShouldUnmarshal(t *testing.T) {
var azureError azureErrorResponse
err := json.Unmarshal([]byte(AzureErrorNested), &azureError)
if err != nil {
t.Fatal(err)
}
if azureError.ErrorDetails.Code != "DeploymentFailed" {
t.Errorf("Error.Code")
}
if !strings.HasPrefix(azureError.ErrorDetails.Message, "At least one resource deployment operation failed") {
t.Errorf("Error.Message")
}
}
func TestAzureErrorEmptyShouldFormat(t *testing.T) {
var aer azureErrorResponse
s := aer.Error()
if s != "" {
t.Fatalf("Expected \"\", but got %s", aer.Error())
}
}
func TestAzureErrorSimpleShouldFormat(t *testing.T) {
var azureErrorResponse azureErrorResponse
err := json.Unmarshal([]byte(AzureErrorSimple), &azureErrorResponse)
if err != nil {
t.Fatal(err)
}
err = approvaltests.VerifyString(t, azureErrorResponse.Error())
if err != nil {
t.Fatal(err)
}
}
func TestAzureErrorNestedShouldFormat(t *testing.T) {
var azureErrorResponse azureErrorResponse
err := json.Unmarshal([]byte(AzureErrorNested), &azureErrorResponse)
if err != nil {
t.Fatal(err)
}
err = approvaltests.VerifyString(t, azureErrorResponse.Error())
if err != nil {
t.Fatal(err)
}
}

View File

@ -0,0 +1,477 @@
package arm
import (
"context"
"errors"
"fmt"
"log"
"os"
"runtime"
"strings"
"time"
armstorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2017-10-01/storage"
"github.com/Azure/azure-sdk-for-go/storage"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/dgrijalva/jwt-go"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer-plugin-sdk/communicator"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
packerAzureCommon "github.com/hashicorp/packer/builder/azure/common"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/builder/azure/common/lin"
)
type Builder struct {
config Config
stateBag multistep.StateBag
runner multistep.Runner
}
const (
DefaultSasBlobContainer = "system/Microsoft.Compute"
DefaultSecretName = "packerKeyVaultSecret"
)
func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() }
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
warnings, errs := b.config.Prepare(raws...)
if errs != nil {
return nil, warnings, errs
}
b.stateBag = new(multistep.BasicStateBag)
b.configureStateBag(b.stateBag)
b.setTemplateParameters(b.stateBag)
b.setImageParameters(b.stateBag)
return nil, warnings, errs
}
func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (packersdk.Artifact, error) {
ui.Say("Running builder ...")
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// FillParameters function captures authType and sets defaults.
err := b.config.ClientConfig.FillParameters()
if err != nil {
return nil, err
}
//When running Packer on an Azure instance using Managed Identity, FillParameters will update SubscriptionID from the instance
// so lets make sure to update our state bag with the valid subscriptionID.
if b.config.isManagedImage() && b.config.SharedGalleryDestination.SigDestinationGalleryName != "" {
b.stateBag.Put(constants.ArmManagedImageSubscription, b.config.ClientConfig.SubscriptionID)
}
log.Print(":: Configuration")
packerAzureCommon.DumpConfig(&b.config, func(s string) { log.Print(s) })
b.stateBag.Put("hook", hook)
b.stateBag.Put(constants.Ui, ui)
spnCloud, spnKeyVault, err := b.getServicePrincipalTokens(ui.Say)
if err != nil {
return nil, err
}
ui.Message("Creating Azure Resource Manager (ARM) client ...")
azureClient, err := NewAzureClient(
b.config.ClientConfig.SubscriptionID,
b.config.SharedGalleryDestination.SigDestinationSubscription,
b.config.ResourceGroupName,
b.config.StorageAccount,
b.config.ClientConfig.CloudEnvironment(),
b.config.SharedGalleryTimeout,
b.config.PollingDurationTimeout,
spnCloud,
spnKeyVault)
if err != nil {
return nil, err
}
resolver := newResourceResolver(azureClient)
if err := resolver.Resolve(&b.config); err != nil {
return nil, err
}
if b.config.ClientConfig.ObjectID == "" {
b.config.ClientConfig.ObjectID = getObjectIdFromToken(ui, spnCloud)
} else {
ui.Message("You have provided Object_ID which is no longer needed, azure packer builder determines this dynamically from the authentication token")
}
if b.config.ClientConfig.ObjectID == "" && b.config.OSType != constants.Target_Linux {
return nil, fmt.Errorf("could not determine the ObjectID for the user, which is required for Windows builds")
}
if b.config.isManagedImage() {
_, err := azureClient.GroupsClient.Get(ctx, b.config.ManagedImageResourceGroupName)
if err != nil {
return nil, fmt.Errorf("Cannot locate the managed image resource group %s.", b.config.ManagedImageResourceGroupName)
}
// If a managed image already exists it cannot be overwritten.
_, err = azureClient.ImagesClient.Get(ctx, b.config.ManagedImageResourceGroupName, b.config.ManagedImageName, "")
if err == nil {
if b.config.PackerForce {
ui.Say(fmt.Sprintf("the managed image named %s already exists, but deleting it due to -force flag", b.config.ManagedImageName))
f, err := azureClient.ImagesClient.Delete(ctx, b.config.ManagedImageResourceGroupName, b.config.ManagedImageName)
if err == nil {
err = f.WaitForCompletionRef(ctx, azureClient.ImagesClient.Client)
}
if err != nil {
return nil, fmt.Errorf("failed to delete the managed image named %s : %s", b.config.ManagedImageName, azureClient.LastError.Error())
}
} else {
return nil, fmt.Errorf("the managed image named %s already exists in the resource group %s, use the -force option to automatically delete it.", b.config.ManagedImageName, b.config.ManagedImageResourceGroupName)
}
}
} else {
// User is not using Managed Images to build, warning message here that this path is being deprecated
ui.Error("Warning: You are using Azure Packer Builder to create VHDs which is being deprecated, consider using Managed Images. Learn more https://www.packer.io/docs/builders/azure/arm#azure-arm-builder-specific-options")
}
if b.config.BuildResourceGroupName != "" {
group, err := azureClient.GroupsClient.Get(ctx, b.config.BuildResourceGroupName)
if err != nil {
return nil, fmt.Errorf("Cannot locate the existing build resource resource group %s.", b.config.BuildResourceGroupName)
}
b.config.Location = *group.Location
}
b.config.validateLocationZoneResiliency(ui.Say)
if b.config.StorageAccount != "" {
account, err := b.getBlobAccount(ctx, azureClient, b.config.ResourceGroupName, b.config.StorageAccount)
if err != nil {
return nil, err
}
b.config.storageAccountBlobEndpoint = *account.AccountProperties.PrimaryEndpoints.Blob
if !equalLocation(*account.Location, b.config.Location) {
return nil, fmt.Errorf("The storage account is located in %s, but the build will take place in %s. The locations must be identical", *account.Location, b.config.Location)
}
}
endpointConnectType := PublicEndpoint
if b.isPublicPrivateNetworkCommunication() && b.isPrivateNetworkCommunication() {
endpointConnectType = PublicEndpointInPrivateNetwork
} else if b.isPrivateNetworkCommunication() {
endpointConnectType = PrivateEndpoint
}
b.setRuntimeParameters(b.stateBag)
b.setTemplateParameters(b.stateBag)
b.setImageParameters(b.stateBag)
deploymentName := b.stateBag.Get(constants.ArmDeploymentName).(string)
// For Managed Images, validate that Shared Gallery Image exists before publishing to SIG
if b.config.isManagedImage() && b.config.SharedGalleryDestination.SigDestinationGalleryName != "" {
_, err = azureClient.GalleryImagesClient.Get(ctx, b.config.SharedGalleryDestination.SigDestinationResourceGroup, b.config.SharedGalleryDestination.SigDestinationGalleryName, b.config.SharedGalleryDestination.SigDestinationImageName)
if err != nil {
return nil, fmt.Errorf("the Shared Gallery Image to which to publish the managed image version to does not exist in the resource group %s", b.config.SharedGalleryDestination.SigDestinationResourceGroup)
}
// SIG requires that replication regions include the region in which the Managed Image resides
managedImageLocation := normalizeAzureRegion(b.stateBag.Get(constants.ArmLocation).(string))
foundMandatoryReplicationRegion := false
var normalizedReplicationRegions []string
for _, region := range b.config.SharedGalleryDestination.SigDestinationReplicationRegions {
// change region to lower-case and strip spaces
normalizedRegion := normalizeAzureRegion(region)
normalizedReplicationRegions = append(normalizedReplicationRegions, normalizedRegion)
if strings.EqualFold(normalizedRegion, managedImageLocation) {
foundMandatoryReplicationRegion = true
continue
}
}
if foundMandatoryReplicationRegion == false {
b.config.SharedGalleryDestination.SigDestinationReplicationRegions = append(normalizedReplicationRegions, managedImageLocation)
}
b.stateBag.Put(constants.ArmManagedImageSharedGalleryReplicationRegions, b.config.SharedGalleryDestination.SigDestinationReplicationRegions)
}
var steps []multistep.Step
if b.config.OSType == constants.Target_Linux {
steps = []multistep.Step{
NewStepCreateResourceGroup(azureClient, ui),
NewStepValidateTemplate(azureClient, ui, &b.config, GetVirtualMachineDeployment),
NewStepDeployTemplate(azureClient, ui, &b.config, deploymentName, GetVirtualMachineDeployment),
NewStepGetIPAddress(azureClient, ui, endpointConnectType),
&communicator.StepConnectSSH{
Config: &b.config.Comm,
Host: lin.SSHHost,
SSHConfig: b.config.Comm.SSHConfigFunc(),
},
&commonsteps.StepProvision{},
&commonsteps.StepCleanupTempKeys{
Comm: &b.config.Comm,
},
NewStepGetOSDisk(azureClient, ui),
NewStepGetAdditionalDisks(azureClient, ui),
NewStepPowerOffCompute(azureClient, ui),
NewStepSnapshotOSDisk(azureClient, ui, &b.config),
NewStepSnapshotDataDisks(azureClient, ui, &b.config),
NewStepCaptureImage(azureClient, ui),
NewStepPublishToSharedImageGallery(azureClient, ui, &b.config),
}
} else if b.config.OSType == constants.Target_Windows {
steps = []multistep.Step{
NewStepCreateResourceGroup(azureClient, ui),
}
if b.config.BuildKeyVaultName == "" {
keyVaultDeploymentName := b.stateBag.Get(constants.ArmKeyVaultDeploymentName).(string)
steps = append(steps,
NewStepValidateTemplate(azureClient, ui, &b.config, GetKeyVaultDeployment),
NewStepDeployTemplate(azureClient, ui, &b.config, keyVaultDeploymentName, GetKeyVaultDeployment),
)
} else {
steps = append(steps, NewStepCertificateInKeyVault(&azureClient.VaultClient, ui, &b.config))
}
steps = append(steps,
NewStepGetCertificate(azureClient, ui),
NewStepSetCertificate(&b.config, ui),
NewStepValidateTemplate(azureClient, ui, &b.config, GetVirtualMachineDeployment),
NewStepDeployTemplate(azureClient, ui, &b.config, deploymentName, GetVirtualMachineDeployment),
NewStepGetIPAddress(azureClient, ui, endpointConnectType),
&communicator.StepConnectWinRM{
Config: &b.config.Comm,
Host: func(stateBag multistep.StateBag) (string, error) {
return stateBag.Get(constants.SSHHost).(string), nil
},
WinRMConfig: func(multistep.StateBag) (*communicator.WinRMConfig, error) {
return &communicator.WinRMConfig{
Username: b.config.UserName,
Password: b.config.Password,
}, nil
},
},
&commonsteps.StepProvision{},
NewStepGetOSDisk(azureClient, ui),
NewStepGetAdditionalDisks(azureClient, ui),
NewStepPowerOffCompute(azureClient, ui),
NewStepSnapshotOSDisk(azureClient, ui, &b.config),
NewStepSnapshotDataDisks(azureClient, ui, &b.config),
NewStepCaptureImage(azureClient, ui),
NewStepPublishToSharedImageGallery(azureClient, ui, &b.config),
)
} else {
return nil, fmt.Errorf("Builder does not support the os_type '%s'", b.config.OSType)
}
if b.config.PackerDebug {
ui.Message(fmt.Sprintf("temp admin user: '%s'", b.config.UserName))
ui.Message(fmt.Sprintf("temp admin password: '%s'", b.config.Password))
if len(b.config.Comm.SSHPrivateKey) != 0 {
debugKeyPath := fmt.Sprintf("%s-%s.pem", b.config.PackerBuildName, b.config.tmpComputeName)
ui.Message(fmt.Sprintf("temp ssh key: %s", debugKeyPath))
b.writeSSHPrivateKey(ui, debugKeyPath)
}
}
b.runner = commonsteps.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(ctx, b.stateBag)
// Report any errors.
if rawErr, ok := b.stateBag.GetOk(constants.Error); ok {
return nil, rawErr.(error)
}
// If we were interrupted or cancelled, then just exit.
if _, ok := b.stateBag.GetOk(multistep.StateCancelled); ok {
return nil, errors.New("Build was cancelled.")
}
if _, ok := b.stateBag.GetOk(multistep.StateHalted); ok {
return nil, errors.New("Build was halted.")
}
generatedData := map[string]interface{}{"generated_data": b.stateBag.Get("generated_data")}
if b.config.isManagedImage() {
managedImageID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/images/%s",
b.config.ClientConfig.SubscriptionID, b.config.ManagedImageResourceGroupName, b.config.ManagedImageName)
if b.config.SharedGalleryDestination.SigDestinationGalleryName != "" {
return NewManagedImageArtifactWithSIGAsDestination(b.config.OSType,
b.config.ManagedImageResourceGroupName,
b.config.ManagedImageName,
b.config.Location,
managedImageID,
b.config.ManagedImageOSDiskSnapshotName,
b.config.ManagedImageDataDiskSnapshotPrefix,
b.stateBag.Get(constants.ArmManagedImageSharedGalleryId).(string),
generatedData)
}
return NewManagedImageArtifact(b.config.OSType,
b.config.ManagedImageResourceGroupName,
b.config.ManagedImageName,
b.config.Location,
managedImageID,
b.config.ManagedImageOSDiskSnapshotName,
b.config.ManagedImageDataDiskSnapshotPrefix,
generatedData)
} else if template, ok := b.stateBag.GetOk(constants.ArmCaptureTemplate); ok {
return NewArtifact(
template.(*CaptureTemplate),
func(name string) string {
blob := azureClient.BlobStorageClient.GetContainerReference(DefaultSasBlobContainer).GetBlobReference(name)
options := storage.BlobSASOptions{}
options.BlobServiceSASPermissions.Read = true
options.Expiry = time.Now().AddDate(0, 1, 0).UTC() // one month
sasUrl, _ := blob.GetSASURI(options)
return sasUrl
},
b.config.OSType,
generatedData)
}
return &Artifact{
StateData: generatedData,
}, nil
}
func (b *Builder) writeSSHPrivateKey(ui packersdk.Ui, debugKeyPath string) {
f, err := os.Create(debugKeyPath)
if err != nil {
ui.Say(fmt.Sprintf("Error saving debug key: %s", err))
}
defer f.Close()
// Write the key out
if _, err := f.Write(b.config.Comm.SSHPrivateKey); err != nil {
ui.Say(fmt.Sprintf("Error saving debug key: %s", err))
return
}
// Chmod it so that it is SSH ready
if runtime.GOOS != "windows" {
if err := f.Chmod(0600); err != nil {
ui.Say(fmt.Sprintf("Error setting permissions of debug key: %s", err))
}
}
}
func (b *Builder) isPublicPrivateNetworkCommunication() bool {
return DefaultPrivateVirtualNetworkWithPublicIp != b.config.PrivateVirtualNetworkWithPublicIp
}
func (b *Builder) isPrivateNetworkCommunication() bool {
return b.config.VirtualNetworkName != ""
}
func equalLocation(location1, location2 string) bool {
return strings.EqualFold(canonicalizeLocation(location1), canonicalizeLocation(location2))
}
func canonicalizeLocation(location string) string {
return strings.Replace(location, " ", "", -1)
}
func (b *Builder) getBlobAccount(ctx context.Context, client *AzureClient, resourceGroupName string, storageAccountName string) (*armstorage.Account, error) {
account, err := client.AccountsClient.GetProperties(ctx, resourceGroupName, storageAccountName)
if err != nil {
return nil, err
}
return &account, err
}
func (b *Builder) configureStateBag(stateBag multistep.StateBag) {
stateBag.Put(constants.AuthorizedKey, b.config.sshAuthorizedKey)
stateBag.Put(constants.ArmTags, packerAzureCommon.MapToAzureTags(b.config.AzureTags))
stateBag.Put(constants.ArmComputeName, b.config.tmpComputeName)
stateBag.Put(constants.ArmDeploymentName, b.config.tmpDeploymentName)
if b.config.OSType == constants.Target_Windows && b.config.BuildKeyVaultName == "" {
stateBag.Put(constants.ArmKeyVaultDeploymentName, fmt.Sprintf("kv%s", b.config.tmpDeploymentName))
}
stateBag.Put(constants.ArmKeyVaultName, b.config.tmpKeyVaultName)
stateBag.Put(constants.ArmIsExistingKeyVault, false)
if b.config.BuildKeyVaultName != "" {
stateBag.Put(constants.ArmKeyVaultName, b.config.BuildKeyVaultName)
b.config.tmpKeyVaultName = b.config.BuildKeyVaultName
stateBag.Put(constants.ArmIsExistingKeyVault, true)
}
stateBag.Put(constants.ArmNicName, b.config.tmpNicName)
stateBag.Put(constants.ArmPublicIPAddressName, b.config.tmpPublicIPAddressName)
stateBag.Put(constants.ArmResourceGroupName, b.config.BuildResourceGroupName)
stateBag.Put(constants.ArmIsExistingResourceGroup, true)
if b.config.tmpResourceGroupName != "" {
stateBag.Put(constants.ArmResourceGroupName, b.config.tmpResourceGroupName)
stateBag.Put(constants.ArmIsExistingResourceGroup, false)
if b.config.BuildResourceGroupName != "" {
stateBag.Put(constants.ArmDoubleResourceGroupNameSet, true)
}
}
stateBag.Put(constants.ArmStorageAccountName, b.config.StorageAccount)
stateBag.Put(constants.ArmIsManagedImage, b.config.isManagedImage())
stateBag.Put(constants.ArmManagedImageResourceGroupName, b.config.ManagedImageResourceGroupName)
stateBag.Put(constants.ArmManagedImageName, b.config.ManagedImageName)
stateBag.Put(constants.ArmManagedImageOSDiskSnapshotName, b.config.ManagedImageOSDiskSnapshotName)
stateBag.Put(constants.ArmManagedImageDataDiskSnapshotPrefix, b.config.ManagedImageDataDiskSnapshotPrefix)
stateBag.Put(constants.ArmAsyncResourceGroupDelete, b.config.AsyncResourceGroupDelete)
if b.config.isManagedImage() && b.config.SharedGalleryDestination.SigDestinationGalleryName != "" {
stateBag.Put(constants.ArmManagedImageSigPublishResourceGroup, b.config.SharedGalleryDestination.SigDestinationResourceGroup)
stateBag.Put(constants.ArmManagedImageSharedGalleryName, b.config.SharedGalleryDestination.SigDestinationGalleryName)
stateBag.Put(constants.ArmManagedImageSharedGalleryImageName, b.config.SharedGalleryDestination.SigDestinationImageName)
stateBag.Put(constants.ArmManagedImageSharedGalleryImageVersion, b.config.SharedGalleryDestination.SigDestinationImageVersion)
stateBag.Put(constants.ArmManagedImageSubscription, b.config.ClientConfig.SubscriptionID)
stateBag.Put(constants.ArmManagedImageSharedGalleryImageVersionEndOfLifeDate, b.config.SharedGalleryImageVersionEndOfLifeDate)
stateBag.Put(constants.ArmManagedImageSharedGalleryImageVersionReplicaCount, b.config.SharedGalleryImageVersionReplicaCount)
stateBag.Put(constants.ArmManagedImageSharedGalleryImageVersionExcludeFromLatest, b.config.SharedGalleryImageVersionExcludeFromLatest)
}
}
// Parameters that are only known at runtime after querying Azure.
func (b *Builder) setRuntimeParameters(stateBag multistep.StateBag) {
stateBag.Put(constants.ArmLocation, b.config.Location)
}
func (b *Builder) setTemplateParameters(stateBag multistep.StateBag) {
stateBag.Put(constants.ArmVirtualMachineCaptureParameters, b.config.toVirtualMachineCaptureParameters())
}
func (b *Builder) setImageParameters(stateBag multistep.StateBag) {
stateBag.Put(constants.ArmImageParameters, b.config.toImageParameters())
}
func (b *Builder) getServicePrincipalTokens(say func(string)) (*adal.ServicePrincipalToken, *adal.ServicePrincipalToken, error) {
return b.config.ClientConfig.GetServicePrincipalTokens(say)
}
func getObjectIdFromToken(ui packersdk.Ui, token *adal.ServicePrincipalToken) string {
claims := jwt.MapClaims{}
var p jwt.Parser
var err error
_, _, err = p.ParseUnverified(token.OAuthToken(), claims)
if err != nil {
ui.Error(fmt.Sprintf("Failed to parse the token,Error: %s", err.Error()))
return ""
}
oid, _ := claims["oid"].(string)
return oid
}
func normalizeAzureRegion(name string) string {
return strings.ToLower(strings.Replace(name, " ", "", -1))
}

View File

@ -0,0 +1,513 @@
package arm
// these tests require the following variables to be set,
// although some test will only use a subset:
//
// * ARM_CLIENT_ID
// * ARM_CLIENT_SECRET
// * ARM_SUBSCRIPTION_ID
// * ARM_STORAGE_ACCOUNT
//
// The subscription in question should have a resource group
// called "packer-acceptance-test" in "South Central US" region. The
// storage account referred to in the above variable should
// be inside this resource group and in "South Central US" as well.
//
// In addition, the PACKER_ACC variable should also be set to
// a non-empty value to enable Packer acceptance tests and the
// options "-v -timeout 90m" should be provided to the test
// command, e.g.:
// go test -v -timeout 90m -run TestBuilderAcc_.*
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"strings"
"testing"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure/auth"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
builderT "github.com/hashicorp/packer/acctest"
)
const DeviceLoginAcceptanceTest = "DEVICELOGIN_TEST"
func TestBuilderAcc_ManagedDisk_Windows(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccManagedDiskWindows,
})
}
func TestBuilderAcc_ManagedDisk_Windows_Build_Resource_Group(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccManagedDiskWindowsBuildResourceGroup,
})
}
func TestBuilderAcc_ManagedDisk_Windows_Build_Resource_Group_Additional_Disk(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccManagedDiskWindowsBuildResourceGroupAdditionalDisk,
})
}
func TestBuilderAcc_ManagedDisk_Windows_DeviceLogin(t *testing.T) {
if os.Getenv(DeviceLoginAcceptanceTest) == "" {
t.Skip(fmt.Sprintf(
"Device Login Acceptance tests skipped unless env '%s' set, as its requires manual step during execution",
DeviceLoginAcceptanceTest))
return
}
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccManagedDiskWindowsDeviceLogin,
})
}
func TestBuilderAcc_ManagedDisk_Linux(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccManagedDiskLinux,
})
}
func TestBuilderAcc_ManagedDisk_Linux_DeviceLogin(t *testing.T) {
if os.Getenv(DeviceLoginAcceptanceTest) == "" {
t.Skip(fmt.Sprintf(
"Device Login Acceptance tests skipped unless env '%s' set, as its requires manual step during execution",
DeviceLoginAcceptanceTest))
return
}
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccManagedDiskLinuxDeviceLogin,
})
}
func TestBuilderAcc_ManagedDisk_Linux_AzureCLI(t *testing.T) {
if os.Getenv("AZURE_CLI_AUTH") == "" {
t.Skip("Azure CLI Acceptance tests skipped unless env 'AZURE_CLI_AUTH' is set, and an active `az login` session has been established")
return
}
var b Builder
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAuthPreCheck(t) },
Builder: &b,
Template: testBuilderAccManagedDiskLinuxAzureCLI,
Check: func([]packersdk.Artifact) error {
checkTemporaryGroupDeleted(t, &b)
return nil
},
})
}
func TestBuilderAcc_Blob_Windows(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccBlobWindows,
})
}
func TestBuilderAcc_Blob_Linux(t *testing.T) {
var b Builder
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAuthPreCheck(t) },
Builder: &b,
Template: testBuilderAccBlobLinux,
Check: func([]packersdk.Artifact) error {
checkUnmanagedVHDDeleted(t, &b)
return nil
},
})
}
func testAccPreCheck(*testing.T) {}
func testAuthPreCheck(t *testing.T) {
_, err := auth.NewAuthorizerFromEnvironment()
if err != nil {
t.Fatalf("failed to auth to azure: %s", err)
}
}
func checkTemporaryGroupDeleted(t *testing.T, b *Builder) {
ui := testUi()
spnCloud, spnKeyVault, err := b.getServicePrincipalTokens(ui.Say)
if err != nil {
t.Fatalf("failed getting azure tokens: %s", err)
}
ui.Message("Creating test Azure Resource Manager (ARM) client ...")
azureClient, err := NewAzureClient(
b.config.ClientConfig.SubscriptionID,
b.config.SharedGalleryDestination.SigDestinationSubscription,
b.config.ResourceGroupName,
b.config.StorageAccount,
b.config.ClientConfig.CloudEnvironment(),
b.config.SharedGalleryTimeout,
b.config.PollingDurationTimeout,
spnCloud,
spnKeyVault)
if err != nil {
t.Fatalf("failed to create azure client: %s", err)
}
// Validate resource group has been deleted
_, err = azureClient.GroupsClient.Get(context.Background(), b.config.tmpResourceGroupName)
if err == nil || !resourceNotFound(err) {
t.Fatalf("failed validating resource group deletion: %s", err)
}
}
func checkUnmanagedVHDDeleted(t *testing.T, b *Builder) {
ui := testUi()
spnCloud, spnKeyVault, err := b.getServicePrincipalTokens(ui.Say)
if err != nil {
t.Fatalf("failed getting azure tokens: %s", err)
}
azureClient, err := NewAzureClient(
b.config.ClientConfig.SubscriptionID,
b.config.SharedGalleryDestination.SigDestinationSubscription,
b.config.ResourceGroupName,
b.config.StorageAccount,
b.config.ClientConfig.CloudEnvironment(),
b.config.SharedGalleryTimeout,
b.config.PollingDurationTimeout,
spnCloud,
spnKeyVault)
if err != nil {
t.Fatalf("failed to create azure client: %s", err)
}
// validate temporary os blob was deleted
blob := azureClient.BlobStorageClient.GetContainerReference("images").GetBlobReference(b.config.tmpOSDiskName)
_, err = blob.BreakLease(nil)
if err != nil && !strings.Contains(err.Error(), "BlobNotFound") {
t.Fatalf("failed validating deletion of unmanaged vhd: %s", err)
}
// Validate resource group has been deleted
_, err = azureClient.GroupsClient.Get(context.Background(), b.config.tmpResourceGroupName)
if err == nil || !resourceNotFound(err) {
t.Fatalf("failed validating resource group deletion: %s", err)
}
}
func resourceNotFound(err error) bool {
derr := autorest.DetailedError{}
return errors.As(err, &derr) && derr.StatusCode == 404
}
func testUi() *packersdk.BasicUi {
return &packersdk.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
ErrorWriter: new(bytes.Buffer),
}
}
const testBuilderAccManagedDiskWindows = `
{
"variables": {
"client_id": "{{env ` + "`ARM_CLIENT_ID`" + `}}",
"client_secret": "{{env ` + "`ARM_CLIENT_SECRET`" + `}}",
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}"
},
"builders": [{
"type": "test",
"client_id": "{{user ` + "`client_id`" + `}}",
"client_secret": "{{user ` + "`client_secret`" + `}}",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskWindows-{{timestamp}}",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "2012-R2-Datacenter",
"communicator": "winrm",
"winrm_use_ssl": "true",
"winrm_insecure": "true",
"winrm_timeout": "3m",
"winrm_username": "packer",
"async_resourcegroup_delete": "true",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}]
}
`
const testBuilderAccManagedDiskWindowsBuildResourceGroup = `
{
"variables": {
"client_id": "{{env ` + "`ARM_CLIENT_ID`" + `}}",
"client_secret": "{{env ` + "`ARM_CLIENT_SECRET`" + `}}",
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}"
},
"builders": [{
"type": "test",
"client_id": "{{user ` + "`client_id`" + `}}",
"client_secret": "{{user ` + "`client_secret`" + `}}",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"build_resource_group_name" : "packer-acceptance-test",
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskWindowsBuildResourceGroup-{{timestamp}}",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "2012-R2-Datacenter",
"communicator": "winrm",
"winrm_use_ssl": "true",
"winrm_insecure": "true",
"winrm_timeout": "3m",
"winrm_username": "packer",
"async_resourcegroup_delete": "true",
"vm_size": "Standard_DS2_v2"
}]
}
`
const testBuilderAccManagedDiskWindowsBuildResourceGroupAdditionalDisk = `
{
"variables": {
"client_id": "{{env ` + "`ARM_CLIENT_ID`" + `}}",
"client_secret": "{{env ` + "`ARM_CLIENT_SECRET`" + `}}",
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}"
},
"builders": [{
"type": "test",
"client_id": "{{user ` + "`client_id`" + `}}",
"client_secret": "{{user ` + "`client_secret`" + `}}",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"build_resource_group_name" : "packer-acceptance-test",
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskWindowsBuildResourceGroupAdditionDisk-{{timestamp}}",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "2012-R2-Datacenter",
"communicator": "winrm",
"winrm_use_ssl": "true",
"winrm_insecure": "true",
"winrm_timeout": "3m",
"winrm_username": "packer",
"async_resourcegroup_delete": "true",
"vm_size": "Standard_DS2_v2",
"disk_additional_size": [10,15]
}]
}
`
const testBuilderAccManagedDiskWindowsDeviceLogin = `
{
"variables": {
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}"
},
"builders": [{
"type": "test",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskWindowsDeviceLogin-{{timestamp}}",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "2012-R2-Datacenter",
"communicator": "winrm",
"winrm_use_ssl": "true",
"winrm_insecure": "true",
"winrm_timeout": "3m",
"winrm_username": "packer",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}]
}
`
const testBuilderAccManagedDiskLinux = `
{
"variables": {
"client_id": "{{env ` + "`ARM_CLIENT_ID`" + `}}",
"client_secret": "{{env ` + "`ARM_CLIENT_SECRET`" + `}}",
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}"
},
"builders": [{
"type": "test",
"client_id": "{{user ` + "`client_id`" + `}}",
"client_secret": "{{user ` + "`client_secret`" + `}}",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskLinux-{{timestamp}}",
"os_type": "Linux",
"image_publisher": "Canonical",
"image_offer": "UbuntuServer",
"image_sku": "16.04-LTS",
"location": "South Central US",
"vm_size": "Standard_DS2_v2",
"azure_tags": {
"env": "testing",
"builder": "packer"
}
}]
}
`
const testBuilderAccManagedDiskLinuxDeviceLogin = `
{
"variables": {
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}"
},
"builders": [{
"type": "test",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskLinuxDeviceLogin-{{timestamp}}",
"os_type": "Linux",
"image_publisher": "Canonical",
"image_offer": "UbuntuServer",
"image_sku": "16.04-LTS",
"async_resourcegroup_delete": "true",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}]
}
`
const testBuilderAccBlobWindows = `
{
"variables": {
"client_id": "{{env ` + "`ARM_CLIENT_ID`" + `}}",
"client_secret": "{{env ` + "`ARM_CLIENT_SECRET`" + `}}",
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}",
"storage_account": "{{env ` + "`ARM_STORAGE_ACCOUNT`" + `}}"
},
"builders": [{
"type": "test",
"client_id": "{{user ` + "`client_id`" + `}}",
"client_secret": "{{user ` + "`client_secret`" + `}}",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"storage_account": "{{user ` + "`storage_account`" + `}}",
"resource_group_name": "packer-acceptance-test",
"capture_container_name": "test",
"capture_name_prefix": "testBuilderAccBlobWin",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "2012-R2-Datacenter",
"communicator": "winrm",
"winrm_use_ssl": "true",
"winrm_insecure": "true",
"winrm_timeout": "3m",
"winrm_username": "packer",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}]
}
`
const testBuilderAccBlobLinux = `
{
"variables": {
"client_id": "{{env ` + "`ARM_CLIENT_ID`" + `}}",
"client_secret": "{{env ` + "`ARM_CLIENT_SECRET`" + `}}",
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}",
"storage_account": "{{env ` + "`ARM_STORAGE_ACCOUNT`" + `}}"
},
"builders": [{
"type": "test",
"client_id": "{{user ` + "`client_id`" + `}}",
"client_secret": "{{user ` + "`client_secret`" + `}}",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"storage_account": "{{user ` + "`storage_account`" + `}}",
"resource_group_name": "packer-acceptance-test",
"capture_container_name": "test",
"capture_name_prefix": "testBuilderAccBlobLinux",
"os_type": "Linux",
"image_publisher": "Canonical",
"image_offer": "UbuntuServer",
"image_sku": "16.04-LTS",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}]
}
`
const testBuilderAccManagedDiskLinuxAzureCLI = `
{
"builders": [{
"type": "test",
"use_azure_cli_auth": true,
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskLinuxAzureCLI-{{timestamp}}",
"temp_resource_group_name": "packer-acceptance-test-managed-cli",
"os_type": "Linux",
"image_publisher": "Canonical",
"image_offer": "UbuntuServer",
"image_sku": "16.04-LTS",
"location": "South Central US",
"vm_size": "Standard_DS2_v2",
"azure_tags": {
"env": "testing",
"builder": "packer"
}
}]
}
`

View File

@ -0,0 +1,67 @@
package arm
import (
"testing"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStateBagShouldBePopulatedExpectedValues(t *testing.T) {
var testSubject Builder
_, _, err := testSubject.Prepare(getArmBuilderConfiguration(), getPackerConfiguration())
if err != nil {
t.Fatalf("failed to prepare: %s", err)
}
var expectedStateBagKeys = []string{
constants.AuthorizedKey,
constants.ArmTags,
constants.ArmComputeName,
constants.ArmDeploymentName,
constants.ArmNicName,
constants.ArmResourceGroupName,
constants.ArmStorageAccountName,
constants.ArmVirtualMachineCaptureParameters,
constants.ArmPublicIPAddressName,
constants.ArmAsyncResourceGroupDelete,
}
for _, v := range expectedStateBagKeys {
if _, ok := testSubject.stateBag.GetOk(v); ok == false {
t.Errorf("Expected the builder's state bag to contain '%s', but it did not.", v)
}
}
}
func TestStateBagShouldPoluateExpectedTags(t *testing.T) {
var testSubject Builder
expectedTags := map[string]string{
"env": "test",
"builder": "packer",
}
armConfig := getArmBuilderConfiguration()
armConfig["azure_tags"] = expectedTags
_, _, err := testSubject.Prepare(armConfig, getPackerConfiguration())
if err != nil {
t.Fatalf("failed to prepare: %s", err)
}
tags, ok := testSubject.stateBag.Get(constants.ArmTags).(map[string]*string)
if !ok {
t.Errorf("Expected the builder's state bag to contain tags of type %T, but didn't.", testSubject.config.AzureTags)
}
if len(tags) != len(expectedTags) {
t.Errorf("expect tags from state to be the same length as tags from config")
}
for k, v := range tags {
if expectedTags[k] != *v {
t.Errorf("expect tag value of %s to be %s, but got %s", k, expectedTags[k], *v)
}
}
}

View File

@ -0,0 +1,84 @@
package arm
type CaptureTemplateParameter struct {
Type string `json:"type"`
DefaultValue string `json:"defaultValue,omitempty"`
}
type CaptureHardwareProfile struct {
VMSize string `json:"vmSize"`
}
type CaptureUri struct {
Uri string `json:"uri"`
}
type CaptureDisk struct {
OSType string `json:"osType"`
Name string `json:"name"`
Image CaptureUri `json:"image"`
Vhd CaptureUri `json:"vhd"`
CreateOption string `json:"createOption"`
Caching string `json:"caching"`
}
type CaptureStorageProfile struct {
OSDisk CaptureDisk `json:"osDisk"`
DataDisks []CaptureDisk `json:"dataDisks"`
}
type CaptureOSProfile struct {
ComputerName string `json:"computerName"`
AdminUsername string `json:"adminUsername"`
AdminPassword string `json:"adminPassword"`
}
type CaptureNetworkInterface struct {
Id string `json:"id"`
}
type CaptureNetworkProfile struct {
NetworkInterfaces []CaptureNetworkInterface `json:"networkInterfaces"`
}
type CaptureBootDiagnostics struct {
Enabled bool `json:"enabled"`
}
type CaptureDiagnosticProfile struct {
BootDiagnostics CaptureBootDiagnostics `json:"bootDiagnostics"`
}
type CaptureProperties struct {
HardwareProfile CaptureHardwareProfile `json:"hardwareProfile"`
StorageProfile CaptureStorageProfile `json:"storageProfile"`
OSProfile CaptureOSProfile `json:"osProfile"`
NetworkProfile CaptureNetworkProfile `json:"networkProfile"`
DiagnosticsProfile CaptureDiagnosticProfile `json:"diagnosticsProfile"`
ProvisioningState int `json:"provisioningState"`
}
type CaptureResources struct {
ApiVersion string `json:"apiVersion"`
Name string `json:"name"`
Type string `json:"type"`
Location string `json:"location"`
Properties CaptureProperties `json:"properties"`
}
type CaptureTemplate struct {
Schema string `json:"$schema"`
ContentVersion string `json:"contentVersion"`
Parameters map[string]CaptureTemplateParameter `json:"parameters"`
Resources []CaptureResources `json:"resources"`
}
type CaptureOperationProperties struct {
Output *CaptureTemplate `json:"output"`
}
type CaptureOperation struct {
OperationId string `json:"operationId"`
Status string `json:"status"`
Properties *CaptureOperationProperties `json:"properties"`
}

View File

@ -0,0 +1,272 @@
package arm
import (
"encoding/json"
"testing"
)
var captureTemplate01 = `{
"operationId": "ac1c7c38-a591-41b3-89bd-ea39fceace1b",
"status": "Succeeded",
"startTime": "2016-04-04T21:07:25.2900874+00:00",
"endTime": "2016-04-04T21:07:26.4776321+00:00",
"properties": {
"output": {
"$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/VM_IP.json",
"contentVersion": "1.0.0.0",
"parameters": {
"vmName": {
"type": "string"
},
"vmSize": {
"type": "string",
"defaultValue": "Standard_A2"
},
"adminUserName": {
"type": "string"
},
"adminPassword": {
"type": "securestring"
},
"networkInterfaceId": {
"type": "string"
}
},
"resources": [
{
"apiVersion": "2015-06-15",
"properties": {
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"storageProfile": {
"osDisk": {
"osType": "Linux",
"name": "packer-osDisk.32118633-6dc9-449f-83b6-a7d2983bec14.vhd",
"createOption": "FromImage",
"image": {
"uri": "http://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.32118633-6dc9-449f-83b6-a7d2983bec14.vhd"
},
"vhd": {
"uri": "http://storage.blob.core.windows.net/vmcontainerce1a1b75-f480-47cb-8e6e-55142e4a5f68/osDisk.ce1a1b75-f480-47cb-8e6e-55142e4a5f68.vhd"
},
"caching": "ReadWrite"
},
"dataDisks": [
{
"lun": 0,
"name": "packer-datadisk-0.32118633-6dc9-449f-83b6-a7d2983bec14.vhd",
"createOption": "Empty",
"image": {
"uri": "http://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-0.32118633-6dc9-449f-83b6-a7d2983bec14.vhd"
},
"vhd": {
"uri": "http://storage.blob.core.windows.net/vmcontainerce1a1b75-f480-47cb-8e6e-55142e4a5f68/datadisk-0.ce1a1b75-f480-47cb-8e6e-55142e4a5f68.vhd"
},
"caching": "ReadWrite"
},
{
"lun": 1,
"name": "packer-datadisk-1.32118633-6dc9-449f-83b6-a7d2983bec14.vhd",
"createOption": "Empty",
"image": {
"uri": "http://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.32118633-6dc9-449f-83b6-a7d2983bec14.vhd"
},
"vhd": {
"uri": "http://storage.blob.core.windows.net/vmcontainerce1a1b75-f480-47cb-8e6e-55142e4a5f68/datadisk-1.ce1a1b75-f480-47cb-8e6e-55142e4a5f68.vhd"
},
"caching": "ReadWrite"
}
]
},
"osProfile": {
"computerName": "[parameters('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[parameters('networkInterfaceId')]"
}
]
},
"diagnosticsProfile": {
"bootDiagnostics": {
"enabled": false
}
},
"provisioningState": 0
},
"name": "[parameters('vmName')]",
"type": "Microsoft.Compute/virtualMachines",
"location": "southcentralus"
}
]
}
}
}`
var captureTemplate02 = `{
"operationId": "ac1c7c38-a591-41b3-89bd-ea39fceace1b",
"status": "Succeeded",
"startTime": "2016-04-04T21:07:25.2900874+00:00",
"endTime": "2016-04-04T21:07:26.4776321+00:00"
}`
func TestCaptureParseJson(t *testing.T) {
var operation CaptureOperation
err := json.Unmarshal([]byte(captureTemplate01), &operation)
if err != nil {
t.Fatalf("failed to the sample capture operation: %s", err)
}
testSubject := operation.Properties.Output
if testSubject.Schema != "http://schema.management.azure.com/schemas/2014-04-01-preview/VM_IP.json" {
t.Errorf("Schema's value was unexpected: %s", testSubject.Schema)
}
if testSubject.ContentVersion != "1.0.0.0" {
t.Errorf("ContentVersion's value was unexpected: %s", testSubject.ContentVersion)
}
// == Parameters ====================================
if len(testSubject.Parameters) != 5 {
t.Fatalf("expected parameters to have 5 keys, but got %d", len(testSubject.Parameters))
}
if _, ok := testSubject.Parameters["vmName"]; !ok {
t.Errorf("Parameters['vmName'] was an expected parameters, but it did not exist")
}
if testSubject.Parameters["vmName"].Type != "string" {
t.Errorf("Parameters['vmName'].Type == 'string', but got '%s'", testSubject.Parameters["vmName"].Type)
}
if _, ok := testSubject.Parameters["vmSize"]; !ok {
t.Errorf("Parameters['vmSize'] was an expected parameters, but it did not exist")
}
if testSubject.Parameters["vmSize"].Type != "string" {
t.Errorf("Parameters['vmSize'].Type == 'string', but got '%s'", testSubject.Parameters["vmSize"])
}
if testSubject.Parameters["vmSize"].DefaultValue != "Standard_A2" {
t.Errorf("Parameters['vmSize'].DefaultValue == 'string', but got '%s'", testSubject.Parameters["vmSize"].DefaultValue)
}
// == Resources =====================================
if len(testSubject.Resources) != 1 {
t.Fatalf("expected resources to have length 1, but got %d", len(testSubject.Resources))
}
if testSubject.Resources[0].Name != "[parameters('vmName')]" {
t.Errorf("Resources[0].Name's value was unexpected: %s", testSubject.Resources[0].Name)
}
if testSubject.Resources[0].Type != "Microsoft.Compute/virtualMachines" {
t.Errorf("Resources[0].Type's value was unexpected: %s", testSubject.Resources[0].Type)
}
if testSubject.Resources[0].Location != "southcentralus" {
t.Errorf("Resources[0].Location's value was unexpected: %s", testSubject.Resources[0].Location)
}
// == Resources/Properties =====================================
if testSubject.Resources[0].Properties.ProvisioningState != 0 {
t.Errorf("Resources[0].Properties.ProvisioningState's value was unexpected: %d", testSubject.Resources[0].Properties.ProvisioningState)
}
// == Resources/Properties/HardwareProfile ======================
hardwareProfile := testSubject.Resources[0].Properties.HardwareProfile
if hardwareProfile.VMSize != "[parameters('vmSize')]" {
t.Errorf("Resources[0].Properties.HardwareProfile.VMSize's value was unexpected: %s", hardwareProfile.VMSize)
}
// == Resources/Properties/StorageProfile/OSDisk ================
osDisk := testSubject.Resources[0].Properties.StorageProfile.OSDisk
if osDisk.OSType != "Linux" {
t.Errorf("Resources[0].Properties.StorageProfile.OSDisk.OSDisk's value was unexpected: %s", osDisk.OSType)
}
if osDisk.Name != "packer-osDisk.32118633-6dc9-449f-83b6-a7d2983bec14.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.OSDisk.Name's value was unexpected: %s", osDisk.Name)
}
if osDisk.CreateOption != "FromImage" {
t.Errorf("Resources[0].Properties.StorageProfile.OSDisk.CreateOption's value was unexpected: %s", osDisk.CreateOption)
}
if osDisk.Image.Uri != "http://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.32118633-6dc9-449f-83b6-a7d2983bec14.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.OSDisk.Image.Uri's value was unexpected: %s", osDisk.Image.Uri)
}
if osDisk.Vhd.Uri != "http://storage.blob.core.windows.net/vmcontainerce1a1b75-f480-47cb-8e6e-55142e4a5f68/osDisk.ce1a1b75-f480-47cb-8e6e-55142e4a5f68.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.OSDisk.Vhd.Uri's value was unexpected: %s", osDisk.Vhd.Uri)
}
if osDisk.Caching != "ReadWrite" {
t.Errorf("Resources[0].Properties.StorageProfile.OSDisk.Caching's value was unexpected: %s", osDisk.Caching)
}
// == Resources/Properties/StorageProfile/DataDisks ================
dataDisks := testSubject.Resources[0].Properties.StorageProfile.DataDisks
if len(dataDisks) != 2 {
t.Errorf("Resources[0].Properties.StorageProfile.DataDisks, 2 disks expected but was: %d", len(dataDisks))
}
if dataDisks[0].Name != "packer-datadisk-0.32118633-6dc9-449f-83b6-a7d2983bec14.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[0].Name's value was unexpected: %s", dataDisks[0].Name)
}
if dataDisks[0].CreateOption != "Empty" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[0].CreateOption's value was unexpected: %s", dataDisks[0].CreateOption)
}
if dataDisks[0].Image.Uri != "http://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-0.32118633-6dc9-449f-83b6-a7d2983bec14.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[0].Image.Uri's value was unexpected: %s", dataDisks[0].Image.Uri)
}
if dataDisks[0].Vhd.Uri != "http://storage.blob.core.windows.net/vmcontainerce1a1b75-f480-47cb-8e6e-55142e4a5f68/datadisk-0.ce1a1b75-f480-47cb-8e6e-55142e4a5f68.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[0].Vhd.Uri's value was unexpected: %s", dataDisks[0].Vhd.Uri)
}
if dataDisks[0].Caching != "ReadWrite" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[0].Caching's value was unexpected: %s", dataDisks[0].Caching)
}
if dataDisks[1].Name != "packer-datadisk-1.32118633-6dc9-449f-83b6-a7d2983bec14.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[1].Name's value was unexpected: %s", dataDisks[1].Name)
}
if dataDisks[1].CreateOption != "Empty" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[1].CreateOption's value was unexpected: %s", dataDisks[1].CreateOption)
}
if dataDisks[1].Image.Uri != "http://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.32118633-6dc9-449f-83b6-a7d2983bec14.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[1].Image.Uri's value was unexpected: %s", dataDisks[1].Image.Uri)
}
if dataDisks[1].Vhd.Uri != "http://storage.blob.core.windows.net/vmcontainerce1a1b75-f480-47cb-8e6e-55142e4a5f68/datadisk-1.ce1a1b75-f480-47cb-8e6e-55142e4a5f68.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[1].Vhd.Uri's value was unexpected: %s", dataDisks[1].Vhd.Uri)
}
if dataDisks[1].Caching != "ReadWrite" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[1].Caching's value was unexpected: %s", dataDisks[1].Caching)
}
// == Resources/Properties/OSProfile ============================
osProfile := testSubject.Resources[0].Properties.OSProfile
if osProfile.AdminPassword != "[parameters('adminPassword')]" {
t.Errorf("Resources[0].Properties.OSProfile.AdminPassword's value was unexpected: %s", osProfile.AdminPassword)
}
if osProfile.AdminUsername != "[parameters('adminUsername')]" {
t.Errorf("Resources[0].Properties.OSProfile.AdminUsername's value was unexpected: %s", osProfile.AdminUsername)
}
if osProfile.ComputerName != "[parameters('vmName')]" {
t.Errorf("Resources[0].Properties.OSProfile.ComputerName's value was unexpected: %s", osProfile.ComputerName)
}
// == Resources/Properties/NetworkProfile =======================
networkProfile := testSubject.Resources[0].Properties.NetworkProfile
if len(networkProfile.NetworkInterfaces) != 1 {
t.Errorf("Count of Resources[0].Properties.NetworkProfile.NetworkInterfaces was expected to be 1, but go %d", len(networkProfile.NetworkInterfaces))
}
if networkProfile.NetworkInterfaces[0].Id != "[parameters('networkInterfaceId')]" {
t.Errorf("Resources[0].Properties.NetworkProfile.NetworkInterfaces[0].Id's value was unexpected: %s", networkProfile.NetworkInterfaces[0].Id)
}
// == Resources/Properties/DiagnosticsProfile ===================
diagnosticsProfile := testSubject.Resources[0].Properties.DiagnosticsProfile
if diagnosticsProfile.BootDiagnostics.Enabled != false {
t.Errorf("Resources[0].Properties.DiagnosticsProfile.BootDiagnostics.Enabled's value was unexpected: %t", diagnosticsProfile.BootDiagnostics.Enabled)
}
}
func TestCaptureEmptyOperationJson(t *testing.T) {
var operation CaptureOperation
err := json.Unmarshal([]byte(captureTemplate02), &operation)
if err != nil {
t.Fatalf("failed to the sample capture operation: %s", err)
}
if operation.Properties != nil {
t.Errorf("JSON contained no properties, but value was not nil: %+v", operation.Properties)
}
}

1229
builder/azure/arm/config.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,357 @@
// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT.
package arm
import (
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer-plugin-sdk/template/config"
"github.com/zclconf/go-cty/cty"
)
// FlatConfig is an auto-generated flat version of Config.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatConfig struct {
PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"`
PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"`
PackerCoreVersion *string `mapstructure:"packer_core_version" cty:"packer_core_version" hcl:"packer_core_version"`
PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"`
PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"`
PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"`
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
CloudEnvironmentName *string `mapstructure:"cloud_environment_name" required:"false" cty:"cloud_environment_name" hcl:"cloud_environment_name"`
ClientID *string `mapstructure:"client_id" cty:"client_id" hcl:"client_id"`
ClientSecret *string `mapstructure:"client_secret" cty:"client_secret" hcl:"client_secret"`
ClientCertPath *string `mapstructure:"client_cert_path" cty:"client_cert_path" hcl:"client_cert_path"`
ClientCertExpireTimeout *string `mapstructure:"client_cert_token_timeout" required:"false" cty:"client_cert_token_timeout" hcl:"client_cert_token_timeout"`
ClientJWT *string `mapstructure:"client_jwt" cty:"client_jwt" hcl:"client_jwt"`
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"`
UseAzureCLIAuth *bool `mapstructure:"use_azure_cli_auth" required:"false" cty:"use_azure_cli_auth" hcl:"use_azure_cli_auth"`
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"`
SharedGalleryDestination *FlatSharedImageGalleryDestination `mapstructure:"shared_image_gallery_destination" cty:"shared_image_gallery_destination" hcl:"shared_image_gallery_destination"`
SharedGalleryTimeout *string `mapstructure:"shared_image_gallery_timeout" cty:"shared_image_gallery_timeout" hcl:"shared_image_gallery_timeout"`
SharedGalleryImageVersionEndOfLifeDate *string `mapstructure:"shared_gallery_image_version_end_of_life_date" required:"false" cty:"shared_gallery_image_version_end_of_life_date" hcl:"shared_gallery_image_version_end_of_life_date"`
SharedGalleryImageVersionReplicaCount *int32 `mapstructure:"shared_image_gallery_replica_count" required:"false" cty:"shared_image_gallery_replica_count" hcl:"shared_image_gallery_replica_count"`
SharedGalleryImageVersionExcludeFromLatest *bool `mapstructure:"shared_gallery_image_version_exclude_from_latest" required:"false" cty:"shared_gallery_image_version_exclude_from_latest" hcl:"shared_gallery_image_version_exclude_from_latest"`
ImagePublisher *string `mapstructure:"image_publisher" required:"true" cty:"image_publisher" hcl:"image_publisher"`
ImageOffer *string `mapstructure:"image_offer" required:"true" cty:"image_offer" hcl:"image_offer"`
ImageSku *string `mapstructure:"image_sku" required:"true" cty:"image_sku" hcl:"image_sku"`
ImageVersion *string `mapstructure:"image_version" required:"false" cty:"image_version" hcl:"image_version"`
ImageUrl *string `mapstructure:"image_url" required:"true" cty:"image_url" hcl:"image_url"`
CustomManagedImageName *string `mapstructure:"custom_managed_image_name" required:"true" cty:"custom_managed_image_name" hcl:"custom_managed_image_name"`
CustomManagedImageResourceGroupName *string `mapstructure:"custom_managed_image_resource_group_name" required:"true" cty:"custom_managed_image_resource_group_name" hcl:"custom_managed_image_resource_group_name"`
Location *string `mapstructure:"location" cty:"location" hcl:"location"`
VMSize *string `mapstructure:"vm_size" required:"false" cty:"vm_size" hcl:"vm_size"`
ManagedImageResourceGroupName *string `mapstructure:"managed_image_resource_group_name" cty:"managed_image_resource_group_name" hcl:"managed_image_resource_group_name"`
ManagedImageName *string `mapstructure:"managed_image_name" cty:"managed_image_name" hcl:"managed_image_name"`
ManagedImageStorageAccountType *string `mapstructure:"managed_image_storage_account_type" required:"false" cty:"managed_image_storage_account_type" hcl:"managed_image_storage_account_type"`
ManagedImageOSDiskSnapshotName *string `mapstructure:"managed_image_os_disk_snapshot_name" required:"false" cty:"managed_image_os_disk_snapshot_name" hcl:"managed_image_os_disk_snapshot_name"`
ManagedImageDataDiskSnapshotPrefix *string `mapstructure:"managed_image_data_disk_snapshot_prefix" required:"false" cty:"managed_image_data_disk_snapshot_prefix" hcl:"managed_image_data_disk_snapshot_prefix"`
ManagedImageZoneResilient *bool `mapstructure:"managed_image_zone_resilient" required:"false" cty:"managed_image_zone_resilient" hcl:"managed_image_zone_resilient"`
AzureTags map[string]string `mapstructure:"azure_tags" required:"false" cty:"azure_tags" hcl:"azure_tags"`
AzureTag []config.FlatNameValue `mapstructure:"azure_tag" required:"false" cty:"azure_tag" hcl:"azure_tag"`
ResourceGroupName *string `mapstructure:"resource_group_name" cty:"resource_group_name" hcl:"resource_group_name"`
StorageAccount *string `mapstructure:"storage_account" cty:"storage_account" hcl:"storage_account"`
TempComputeName *string `mapstructure:"temp_compute_name" required:"false" cty:"temp_compute_name" hcl:"temp_compute_name"`
TempNicName *string `mapstructure:"temp_nic_name" required:"false" cty:"temp_nic_name" hcl:"temp_nic_name"`
TempResourceGroupName *string `mapstructure:"temp_resource_group_name" cty:"temp_resource_group_name" hcl:"temp_resource_group_name"`
BuildResourceGroupName *string `mapstructure:"build_resource_group_name" cty:"build_resource_group_name" hcl:"build_resource_group_name"`
BuildKeyVaultName *string `mapstructure:"build_key_vault_name" cty:"build_key_vault_name" hcl:"build_key_vault_name"`
BuildKeyVaultSKU *string `mapstructure:"build_key_vault_sku" cty:"build_key_vault_sku" hcl:"build_key_vault_sku"`
PrivateVirtualNetworkWithPublicIp *bool `mapstructure:"private_virtual_network_with_public_ip" required:"false" cty:"private_virtual_network_with_public_ip" hcl:"private_virtual_network_with_public_ip"`
VirtualNetworkName *string `mapstructure:"virtual_network_name" required:"false" cty:"virtual_network_name" hcl:"virtual_network_name"`
VirtualNetworkSubnetName *string `mapstructure:"virtual_network_subnet_name" required:"false" cty:"virtual_network_subnet_name" hcl:"virtual_network_subnet_name"`
VirtualNetworkResourceGroupName *string `mapstructure:"virtual_network_resource_group_name" required:"false" cty:"virtual_network_resource_group_name" hcl:"virtual_network_resource_group_name"`
CustomDataFile *string `mapstructure:"custom_data_file" required:"false" cty:"custom_data_file" hcl:"custom_data_file"`
PlanInfo *FlatPlanInformation `mapstructure:"plan_info" required:"false" cty:"plan_info" hcl:"plan_info"`
PollingDurationTimeout *string `mapstructure:"polling_duration_timeout" required:"false" cty:"polling_duration_timeout" hcl:"polling_duration_timeout"`
OSType *string `mapstructure:"os_type" required:"false" cty:"os_type" hcl:"os_type"`
TempOSDiskName *string `mapstructure:"temp_os_disk_name" required:"false" cty:"temp_os_disk_name" hcl:"temp_os_disk_name"`
OSDiskSizeGB *int32 `mapstructure:"os_disk_size_gb" required:"false" cty:"os_disk_size_gb" hcl:"os_disk_size_gb"`
AdditionalDiskSize []int32 `mapstructure:"disk_additional_size" required:"false" cty:"disk_additional_size" hcl:"disk_additional_size"`
DiskCachingType *string `mapstructure:"disk_caching_type" required:"false" cty:"disk_caching_type" hcl:"disk_caching_type"`
AllowedInboundIpAddresses []string `mapstructure:"allowed_inbound_ip_addresses" cty:"allowed_inbound_ip_addresses" hcl:"allowed_inbound_ip_addresses"`
BootDiagSTGAccount *string `mapstructure:"boot_diag_storage_account" required:"false" cty:"boot_diag_storage_account" hcl:"boot_diag_storage_account"`
CustomResourcePrefix *string `mapstructure:"custom_resource_build_prefix" required:"false" cty:"custom_resource_build_prefix" hcl:"custom_resource_build_prefix"`
Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"`
PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"`
SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"`
SSHPort *int `mapstructure:"ssh_port" cty:"ssh_port" hcl:"ssh_port"`
SSHUsername *string `mapstructure:"ssh_username" cty:"ssh_username" hcl:"ssh_username"`
SSHPassword *string `mapstructure:"ssh_password" cty:"ssh_password" hcl:"ssh_password"`
SSHKeyPairName *string `mapstructure:"ssh_keypair_name" undocumented:"true" cty:"ssh_keypair_name" hcl:"ssh_keypair_name"`
SSHTemporaryKeyPairName *string `mapstructure:"temporary_key_pair_name" undocumented:"true" cty:"temporary_key_pair_name" hcl:"temporary_key_pair_name"`
SSHTemporaryKeyPairType *string `mapstructure:"temporary_key_pair_type" cty:"temporary_key_pair_type" hcl:"temporary_key_pair_type"`
SSHTemporaryKeyPairBits *int `mapstructure:"temporary_key_pair_bits" cty:"temporary_key_pair_bits" hcl:"temporary_key_pair_bits"`
SSHCiphers []string `mapstructure:"ssh_ciphers" cty:"ssh_ciphers" hcl:"ssh_ciphers"`
SSHClearAuthorizedKeys *bool `mapstructure:"ssh_clear_authorized_keys" cty:"ssh_clear_authorized_keys" hcl:"ssh_clear_authorized_keys"`
SSHKEXAlgos []string `mapstructure:"ssh_key_exchange_algorithms" cty:"ssh_key_exchange_algorithms" hcl:"ssh_key_exchange_algorithms"`
SSHPrivateKeyFile *string `mapstructure:"ssh_private_key_file" undocumented:"true" cty:"ssh_private_key_file" hcl:"ssh_private_key_file"`
SSHCertificateFile *string `mapstructure:"ssh_certificate_file" cty:"ssh_certificate_file" hcl:"ssh_certificate_file"`
SSHPty *bool `mapstructure:"ssh_pty" cty:"ssh_pty" hcl:"ssh_pty"`
SSHTimeout *string `mapstructure:"ssh_timeout" cty:"ssh_timeout" hcl:"ssh_timeout"`
SSHWaitTimeout *string `mapstructure:"ssh_wait_timeout" undocumented:"true" cty:"ssh_wait_timeout" hcl:"ssh_wait_timeout"`
SSHAgentAuth *bool `mapstructure:"ssh_agent_auth" undocumented:"true" cty:"ssh_agent_auth" hcl:"ssh_agent_auth"`
SSHDisableAgentForwarding *bool `mapstructure:"ssh_disable_agent_forwarding" cty:"ssh_disable_agent_forwarding" hcl:"ssh_disable_agent_forwarding"`
SSHHandshakeAttempts *int `mapstructure:"ssh_handshake_attempts" cty:"ssh_handshake_attempts" hcl:"ssh_handshake_attempts"`
SSHBastionHost *string `mapstructure:"ssh_bastion_host" cty:"ssh_bastion_host" hcl:"ssh_bastion_host"`
SSHBastionPort *int `mapstructure:"ssh_bastion_port" cty:"ssh_bastion_port" hcl:"ssh_bastion_port"`
SSHBastionAgentAuth *bool `mapstructure:"ssh_bastion_agent_auth" cty:"ssh_bastion_agent_auth" hcl:"ssh_bastion_agent_auth"`
SSHBastionUsername *string `mapstructure:"ssh_bastion_username" cty:"ssh_bastion_username" hcl:"ssh_bastion_username"`
SSHBastionPassword *string `mapstructure:"ssh_bastion_password" cty:"ssh_bastion_password" hcl:"ssh_bastion_password"`
SSHBastionInteractive *bool `mapstructure:"ssh_bastion_interactive" cty:"ssh_bastion_interactive" hcl:"ssh_bastion_interactive"`
SSHBastionPrivateKeyFile *string `mapstructure:"ssh_bastion_private_key_file" cty:"ssh_bastion_private_key_file" hcl:"ssh_bastion_private_key_file"`
SSHBastionCertificateFile *string `mapstructure:"ssh_bastion_certificate_file" cty:"ssh_bastion_certificate_file" hcl:"ssh_bastion_certificate_file"`
SSHFileTransferMethod *string `mapstructure:"ssh_file_transfer_method" cty:"ssh_file_transfer_method" hcl:"ssh_file_transfer_method"`
SSHProxyHost *string `mapstructure:"ssh_proxy_host" cty:"ssh_proxy_host" hcl:"ssh_proxy_host"`
SSHProxyPort *int `mapstructure:"ssh_proxy_port" cty:"ssh_proxy_port" hcl:"ssh_proxy_port"`
SSHProxyUsername *string `mapstructure:"ssh_proxy_username" cty:"ssh_proxy_username" hcl:"ssh_proxy_username"`
SSHProxyPassword *string `mapstructure:"ssh_proxy_password" cty:"ssh_proxy_password" hcl:"ssh_proxy_password"`
SSHKeepAliveInterval *string `mapstructure:"ssh_keep_alive_interval" cty:"ssh_keep_alive_interval" hcl:"ssh_keep_alive_interval"`
SSHReadWriteTimeout *string `mapstructure:"ssh_read_write_timeout" cty:"ssh_read_write_timeout" hcl:"ssh_read_write_timeout"`
SSHRemoteTunnels []string `mapstructure:"ssh_remote_tunnels" cty:"ssh_remote_tunnels" hcl:"ssh_remote_tunnels"`
SSHLocalTunnels []string `mapstructure:"ssh_local_tunnels" cty:"ssh_local_tunnels" hcl:"ssh_local_tunnels"`
SSHPublicKey []byte `mapstructure:"ssh_public_key" undocumented:"true" cty:"ssh_public_key" hcl:"ssh_public_key"`
SSHPrivateKey []byte `mapstructure:"ssh_private_key" undocumented:"true" cty:"ssh_private_key" hcl:"ssh_private_key"`
WinRMUser *string `mapstructure:"winrm_username" cty:"winrm_username" hcl:"winrm_username"`
WinRMPassword *string `mapstructure:"winrm_password" cty:"winrm_password" hcl:"winrm_password"`
WinRMHost *string `mapstructure:"winrm_host" cty:"winrm_host" hcl:"winrm_host"`
WinRMNoProxy *bool `mapstructure:"winrm_no_proxy" cty:"winrm_no_proxy" hcl:"winrm_no_proxy"`
WinRMPort *int `mapstructure:"winrm_port" cty:"winrm_port" hcl:"winrm_port"`
WinRMTimeout *string `mapstructure:"winrm_timeout" cty:"winrm_timeout" hcl:"winrm_timeout"`
WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl" hcl:"winrm_use_ssl"`
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
AsyncResourceGroupDelete *bool `mapstructure:"async_resourcegroup_delete" required:"false" cty:"async_resourcegroup_delete" hcl:"async_resourcegroup_delete"`
}
// FlatMapstructure returns a new FlatConfig.
// FlatConfig is an auto-generated flat version of Config.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatConfig)
}
// HCL2Spec returns the hcl spec of a Config.
// This spec is used by HCL to read the fields of Config.
// 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_core_version": &hcldec.AttrSpec{Name: "packer_core_version", 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_cert_token_timeout": &hcldec.AttrSpec{Name: "client_cert_token_timeout", 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},
"use_azure_cli_auth": &hcldec.AttrSpec{Name: "use_azure_cli_auth", Type: cty.Bool, 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((*config.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_nic_name": &hcldec.AttrSpec{Name: "temp_nic_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},
"temp_os_disk_name": &hcldec.AttrSpec{Name: "temp_os_disk_name", 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},
"temporary_key_pair_type": &hcldec.AttrSpec{Name: "temporary_key_pair_type", Type: cty.String, Required: false},
"temporary_key_pair_bits": &hcldec.AttrSpec{Name: "temporary_key_pair_bits", Type: cty.Number, Required: false},
"ssh_ciphers": &hcldec.AttrSpec{Name: "ssh_ciphers", Type: cty.List(cty.String), Required: false},
"ssh_clear_authorized_keys": &hcldec.AttrSpec{Name: "ssh_clear_authorized_keys", Type: cty.Bool, Required: false},
"ssh_key_exchange_algorithms": &hcldec.AttrSpec{Name: "ssh_key_exchange_algorithms", Type: cty.List(cty.String), Required: false},
"ssh_private_key_file": &hcldec.AttrSpec{Name: "ssh_private_key_file", Type: cty.String, Required: false},
"ssh_certificate_file": &hcldec.AttrSpec{Name: "ssh_certificate_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_bastion_certificate_file": &hcldec.AttrSpec{Name: "ssh_bastion_certificate_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_no_proxy": &hcldec.AttrSpec{Name: "winrm_no_proxy", Type: cty.Bool, 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
}
// FlatPlanInformation is an auto-generated flat version of PlanInformation.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatPlanInformation struct {
PlanName *string `mapstructure:"plan_name" cty:"plan_name" hcl:"plan_name"`
PlanProduct *string `mapstructure:"plan_product" cty:"plan_product" hcl:"plan_product"`
PlanPublisher *string `mapstructure:"plan_publisher" cty:"plan_publisher" hcl:"plan_publisher"`
PlanPromotionCode *string `mapstructure:"plan_promotion_code" cty:"plan_promotion_code" hcl:"plan_promotion_code"`
}
// FlatMapstructure returns a new FlatPlanInformation.
// FlatPlanInformation is an auto-generated flat version of PlanInformation.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*PlanInformation) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatPlanInformation)
}
// HCL2Spec returns the hcl spec of a PlanInformation.
// This spec is used by HCL to read the fields of PlanInformation.
// The decoded values from this spec will then be applied to a FlatPlanInformation.
func (*FlatPlanInformation) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"plan_name": &hcldec.AttrSpec{Name: "plan_name", Type: cty.String, Required: false},
"plan_product": &hcldec.AttrSpec{Name: "plan_product", Type: cty.String, Required: false},
"plan_publisher": &hcldec.AttrSpec{Name: "plan_publisher", Type: cty.String, Required: false},
"plan_promotion_code": &hcldec.AttrSpec{Name: "plan_promotion_code", Type: cty.String, Required: false},
}
return s
}
// FlatSharedImageGallery is an auto-generated flat version of SharedImageGallery.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatSharedImageGallery struct {
Subscription *string `mapstructure:"subscription" cty:"subscription" hcl:"subscription"`
ResourceGroup *string `mapstructure:"resource_group" cty:"resource_group" hcl:"resource_group"`
GalleryName *string `mapstructure:"gallery_name" cty:"gallery_name" hcl:"gallery_name"`
ImageName *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"`
ImageVersion *string `mapstructure:"image_version" required:"false" cty:"image_version" hcl:"image_version"`
}
// FlatMapstructure returns a new FlatSharedImageGallery.
// FlatSharedImageGallery is an auto-generated flat version of SharedImageGallery.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*SharedImageGallery) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatSharedImageGallery)
}
// HCL2Spec returns the hcl spec of a SharedImageGallery.
// This spec is used by HCL to read the fields of SharedImageGallery.
// The decoded values from this spec will then be applied to a FlatSharedImageGallery.
func (*FlatSharedImageGallery) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"subscription": &hcldec.AttrSpec{Name: "subscription", Type: cty.String, Required: false},
"resource_group": &hcldec.AttrSpec{Name: "resource_group", Type: cty.String, Required: false},
"gallery_name": &hcldec.AttrSpec{Name: "gallery_name", Type: cty.String, Required: false},
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
"image_version": &hcldec.AttrSpec{Name: "image_version", Type: cty.String, Required: false},
}
return s
}
// FlatSharedImageGalleryDestination is an auto-generated flat version of SharedImageGalleryDestination.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatSharedImageGalleryDestination struct {
SigDestinationSubscription *string `mapstructure:"subscription" cty:"subscription" hcl:"subscription"`
SigDestinationResourceGroup *string `mapstructure:"resource_group" cty:"resource_group" hcl:"resource_group"`
SigDestinationGalleryName *string `mapstructure:"gallery_name" cty:"gallery_name" hcl:"gallery_name"`
SigDestinationImageName *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"`
SigDestinationImageVersion *string `mapstructure:"image_version" cty:"image_version" hcl:"image_version"`
SigDestinationReplicationRegions []string `mapstructure:"replication_regions" cty:"replication_regions" hcl:"replication_regions"`
}
// FlatMapstructure returns a new FlatSharedImageGalleryDestination.
// FlatSharedImageGalleryDestination is an auto-generated flat version of SharedImageGalleryDestination.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*SharedImageGalleryDestination) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatSharedImageGalleryDestination)
}
// HCL2Spec returns the hcl spec of a SharedImageGalleryDestination.
// This spec is used by HCL to read the fields of SharedImageGalleryDestination.
// The decoded values from this spec will then be applied to a FlatSharedImageGalleryDestination.
func (*FlatSharedImageGalleryDestination) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"subscription": &hcldec.AttrSpec{Name: "subscription", Type: cty.String, Required: false},
"resource_group": &hcldec.AttrSpec{Name: "resource_group", Type: cty.String, Required: false},
"gallery_name": &hcldec.AttrSpec{Name: "gallery_name", Type: cty.String, Required: false},
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
"image_version": &hcldec.AttrSpec{Name: "image_version", Type: cty.String, Required: false},
"replication_regions": &hcldec.AttrSpec{Name: "replication_regions", Type: cty.List(cty.String), Required: false},
}
return s
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
package arm
import (
"bytes"
"io/ioutil"
"log"
"net/http"
"io"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/hashicorp/packer/builder/azure/common/logutil"
)
func chop(data []byte, maxlen int64) string {
s := string(data)
if int64(len(s)) > maxlen {
s = s[:maxlen] + "..."
}
return s
}
func handleBody(body io.ReadCloser, maxlen int64) (io.ReadCloser, string) {
if body == nil {
return nil, ""
}
defer body.Close()
b, err := ioutil.ReadAll(body)
if err != nil {
return nil, ""
}
return ioutil.NopCloser(bytes.NewReader(b)), chop(b, maxlen)
}
func withInspection(maxlen int64) autorest.PrepareDecorator {
return func(p autorest.Preparer) autorest.Preparer {
return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
body, bodyString := handleBody(r.Body, maxlen)
r.Body = body
log.Print("Azure request", logutil.Fields{
"method": r.Method,
"request": r.URL.String(),
"body": bodyString,
})
return p.Prepare(r)
})
}
}
func byInspecting(maxlen int64) autorest.RespondDecorator {
return func(r autorest.Responder) autorest.Responder {
return autorest.ResponderFunc(func(resp *http.Response) error {
body, bodyString := handleBody(resp.Body, maxlen)
resp.Body = body
log.Print("Azure response", logutil.Fields{
"status": resp.Status,
"method": resp.Request.Method,
"request": resp.Request.URL.String(),
"x-ms-request-id": azure.ExtractRequestID(resp),
"body": bodyString,
})
return r.Respond(resp)
})
}
}

View File

@ -0,0 +1,59 @@
package arm
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"time"
"golang.org/x/crypto/ssh"
)
const (
KeySize = 2048
)
type OpenSshKeyPair struct {
privateKey *rsa.PrivateKey
publicKey ssh.PublicKey
}
func NewOpenSshKeyPair() (*OpenSshKeyPair, error) {
return NewOpenSshKeyPairWithSize(KeySize)
}
func NewOpenSshKeyPairWithSize(keySize int) (*OpenSshKeyPair, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, keySize)
if err != nil {
return nil, err
}
publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, err
}
return &OpenSshKeyPair{
privateKey: privateKey,
publicKey: publicKey,
}, nil
}
func (s *OpenSshKeyPair) AuthorizedKey() string {
return fmt.Sprintf("%s %s packer Azure Deployment%s",
s.publicKey.Type(),
base64.StdEncoding.EncodeToString(s.publicKey.Marshal()),
time.Now().Format(time.RFC3339))
}
func (s *OpenSshKeyPair) PrivateKey() []byte {
privateKey := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(s.privateKey),
})
return privateKey
}

View File

@ -0,0 +1,37 @@
package arm
import (
"testing"
"golang.org/x/crypto/ssh"
)
func TestFart(t *testing.T) {
}
func TestAuthorizedKeyShouldParse(t *testing.T) {
testSubject, err := NewOpenSshKeyPairWithSize(512)
if err != nil {
t.Fatalf("Failed to create a new OpenSSH key pair, err=%s.", err)
}
authorizedKey := testSubject.AuthorizedKey()
_, _, _, _, err = ssh.ParseAuthorizedKey([]byte(authorizedKey))
if err != nil {
t.Fatalf("Failed to parse the authorized key, err=%s", err)
}
}
func TestPrivateKeyShouldParse(t *testing.T) {
testSubject, err := NewOpenSshKeyPairWithSize(512)
if err != nil {
t.Fatalf("Failed to create a new OpenSSH key pair, err=%s.", err)
}
_, err = ssh.ParsePrivateKey([]byte(testSubject.PrivateKey()))
if err != nil {
t.Fatalf("Failed to parse the private key, err=%s\n", err)
}
}

View File

@ -0,0 +1,141 @@
package arm
// Code to resolve resources that are required by the API. These resources
// can most likely be resolved without asking the user, thereby reducing the
// amount of configuration they need to provide.
//
// Resource resolver differs from config retriever because resource resolver
// requires a client to communicate with the Azure API. A config retriever is
// used to determine values without use of a client.
import (
"context"
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
)
type resourceResolver struct {
client *AzureClient
findVirtualNetworkResourceGroup func(*AzureClient, string) (string, error)
findVirtualNetworkSubnet func(*AzureClient, string, string) (string, error)
}
func newResourceResolver(client *AzureClient) *resourceResolver {
return &resourceResolver{
client: client,
findVirtualNetworkResourceGroup: findVirtualNetworkResourceGroup,
findVirtualNetworkSubnet: findVirtualNetworkSubnet,
}
}
func (s *resourceResolver) Resolve(c *Config) error {
if s.shouldResolveResourceGroup(c) {
resourceGroupName, err := s.findVirtualNetworkResourceGroup(s.client, c.VirtualNetworkName)
if err != nil {
return err
}
subnetName, err := s.findVirtualNetworkSubnet(s.client, resourceGroupName, c.VirtualNetworkName)
if err != nil {
return err
}
c.VirtualNetworkResourceGroupName = resourceGroupName
c.VirtualNetworkSubnetName = subnetName
}
if s.shouldResolveManagedImageName(c) {
image, err := findManagedImageByName(s.client, c.CustomManagedImageName, c.CustomManagedImageResourceGroupName)
if err != nil {
return err
}
c.customManagedImageID = *image.ID
}
return nil
}
func (s *resourceResolver) shouldResolveResourceGroup(c *Config) bool {
return c.VirtualNetworkName != "" && c.VirtualNetworkResourceGroupName == ""
}
func (s *resourceResolver) shouldResolveManagedImageName(c *Config) bool {
return c.CustomManagedImageName != ""
}
func getResourceGroupNameFromId(id string) string {
// "/subscriptions/3f499422-dd76-4114-8859-86d526c9deb6/resourceGroups/packer-Resource-Group-yylnwsl30j/providers/...
xs := strings.Split(id, "/")
return xs[4]
}
func findManagedImageByName(client *AzureClient, name, resourceGroupName string) (*compute.Image, error) {
images, err := client.ImagesClient.ListByResourceGroupComplete(context.TODO(), resourceGroupName)
if err != nil {
return nil, err
}
for images.NotDone() {
image := images.Value()
if strings.EqualFold(name, *image.Name) {
return &image, nil
}
if err = images.Next(); err != nil {
return nil, err
}
}
return nil, fmt.Errorf("Cannot find an image named '%s' in the resource group '%s'", name, resourceGroupName)
}
func findVirtualNetworkResourceGroup(client *AzureClient, name string) (string, error) {
virtualNetworks, err := client.VirtualNetworksClient.ListAllComplete(context.TODO())
if err != nil {
return "", err
}
resourceGroupNames := make([]string, 0)
for virtualNetworks.NotDone() {
virtualNetwork := virtualNetworks.Value()
if strings.EqualFold(name, *virtualNetwork.Name) {
rgn := getResourceGroupNameFromId(*virtualNetwork.ID)
resourceGroupNames = append(resourceGroupNames, rgn)
}
if err = virtualNetworks.Next(); err != nil {
return "", err
}
}
if len(resourceGroupNames) == 0 {
return "", fmt.Errorf("Cannot find a resource group with a virtual network called %q", name)
}
if len(resourceGroupNames) > 1 {
return "", fmt.Errorf("Found multiple resource groups with a virtual network called %q, please use virtual_network_subnet_name and virtual_network_resource_group_name to disambiguate", name)
}
return resourceGroupNames[0], nil
}
func findVirtualNetworkSubnet(client *AzureClient, resourceGroupName string, name string) (string, error) {
subnets, err := client.SubnetsClient.List(context.TODO(), resourceGroupName, name)
if err != nil {
return "", err
}
subnetList := subnets.Values() // only first page of subnets, but only interested in ==0 or >1
if len(subnetList) == 0 {
return "", fmt.Errorf("Cannot find a subnet in the resource group %q associated with the virtual network called %q", resourceGroupName, name)
}
if len(subnetList) > 1 {
return "", fmt.Errorf("Found multiple subnets in the resource group %q associated with the virtual network called %q, please use virtual_network_subnet_name and virtual_network_resource_group_name to disambiguate", resourceGroupName, name)
}
subnet := subnetList[0]
return *subnet.Name, nil
}

View File

@ -0,0 +1,79 @@
package arm
import (
"testing"
)
func TestResourceResolverIgnoresEmptyVirtualNetworkName(t *testing.T) {
var c Config
c.Prepare(getArmBuilderConfiguration(), getPackerConfiguration())
if c.VirtualNetworkName != "" {
t.Fatalf("Expected VirtualNetworkName to be empty by default")
}
sut := newTestResourceResolver()
sut.findVirtualNetworkResourceGroup = nil // assert that this is not even called
sut.Resolve(&c)
if c.VirtualNetworkName != "" {
t.Fatalf("Expected VirtualNetworkName to be empty")
}
if c.VirtualNetworkResourceGroupName != "" {
t.Fatalf("Expected VirtualNetworkResourceGroupName to be empty")
}
}
// If the user fully specified the virtual network name and resource group then
// there is no need to do a lookup.
func TestResourceResolverIgnoresSetVirtualNetwork(t *testing.T) {
var c Config
c.Prepare(getArmBuilderConfiguration(), getPackerConfiguration())
c.VirtualNetworkName = "--virtual-network-name--"
c.VirtualNetworkResourceGroupName = "--virtual-network-resource-group-name--"
c.VirtualNetworkSubnetName = "--virtual-network-subnet-name--"
sut := newTestResourceResolver()
sut.findVirtualNetworkResourceGroup = nil // assert that this is not even called
sut.findVirtualNetworkSubnet = nil // assert that this is not even called
sut.Resolve(&c)
if c.VirtualNetworkName != "--virtual-network-name--" {
t.Fatalf("Expected VirtualNetworkName to be --virtual-network-name--")
}
if c.VirtualNetworkResourceGroupName != "--virtual-network-resource-group-name--" {
t.Fatalf("Expected VirtualNetworkResourceGroupName to be --virtual-network-resource-group-name--")
}
if c.VirtualNetworkSubnetName != "--virtual-network-subnet-name--" {
t.Fatalf("Expected VirtualNetworkSubnetName to be --virtual-network-subnet-name--")
}
}
// If the user set virtual network name then the code should resolve virtual network
// resource group name.
func TestResourceResolverSetVirtualNetworkResourceGroupName(t *testing.T) {
var c Config
c.Prepare(getArmBuilderConfiguration(), getPackerConfiguration())
c.VirtualNetworkName = "--virtual-network-name--"
sut := newTestResourceResolver()
sut.Resolve(&c)
if c.VirtualNetworkResourceGroupName != "findVirtualNetworkResourceGroup is mocked" {
t.Fatalf("Expected VirtualNetworkResourceGroupName to be 'findVirtualNetworkResourceGroup is mocked'")
}
if c.VirtualNetworkSubnetName != "findVirtualNetworkSubnet is mocked" {
t.Fatalf("Expected findVirtualNetworkSubnet to be 'findVirtualNetworkSubnet is mocked'")
}
}
func newTestResourceResolver() resourceResolver {
return resourceResolver{
client: nil,
findVirtualNetworkResourceGroup: func(*AzureClient, string) (string, error) {
return "findVirtualNetworkResourceGroup is mocked", nil
},
findVirtualNetworkSubnet: func(*AzureClient, string, string) (string, error) {
return "findVirtualNetworkSubnet is mocked", nil
},
}
}

20
builder/azure/arm/step.go Normal file
View File

@ -0,0 +1,20 @@
package arm
import (
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func processStepResult(
err error, sayError func(error), state multistep.StateBag) multistep.StepAction {
if err != nil {
state.Put(constants.Error, err)
sayError(err)
return multistep.ActionHalt
}
return multistep.ActionContinue
}

View File

@ -0,0 +1,120 @@
package arm
import (
"context"
"fmt"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
type StepCaptureImage struct {
client *AzureClient
generalizeVM func(resourceGroupName, computeName string) error
captureVhd func(ctx context.Context, resourceGroupName string, computeName string, parameters *compute.VirtualMachineCaptureParameters) error
captureManagedImage func(ctx context.Context, resourceGroupName string, computeName string, parameters *compute.Image) error
get func(client *AzureClient) *CaptureTemplate
say func(message string)
error func(e error)
}
func NewStepCaptureImage(client *AzureClient, ui packersdk.Ui) *StepCaptureImage {
var step = &StepCaptureImage{
client: client,
get: func(client *AzureClient) *CaptureTemplate {
return client.Template
},
say: func(message string) {
ui.Say(message)
},
error: func(e error) {
ui.Error(e.Error())
},
}
step.generalizeVM = step.generalize
step.captureVhd = step.captureImage
step.captureManagedImage = step.captureImageFromVM
return step
}
func (s *StepCaptureImage) generalize(resourceGroupName string, computeName string) error {
_, err := s.client.Generalize(context.TODO(), resourceGroupName, computeName)
if err != nil {
s.say(s.client.LastError.Error())
}
return err
}
func (s *StepCaptureImage) captureImageFromVM(ctx context.Context, resourceGroupName string, imageName string, image *compute.Image) error {
f, err := s.client.ImagesClient.CreateOrUpdate(ctx, resourceGroupName, imageName, *image)
if err != nil {
s.say(s.client.LastError.Error())
}
return f.WaitForCompletionRef(ctx, s.client.ImagesClient.Client)
}
func (s *StepCaptureImage) captureImage(ctx context.Context, resourceGroupName string, computeName string, parameters *compute.VirtualMachineCaptureParameters) error {
f, err := s.client.VirtualMachinesClient.Capture(ctx, resourceGroupName, computeName, *parameters)
if err != nil {
s.say(s.client.LastError.Error())
}
return f.WaitForCompletionRef(ctx, s.client.VirtualMachinesClient.Client)
}
func (s *StepCaptureImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Capturing image ...")
var computeName = state.Get(constants.ArmComputeName).(string)
var location = state.Get(constants.ArmLocation).(string)
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
var vmCaptureParameters = state.Get(constants.ArmVirtualMachineCaptureParameters).(*compute.VirtualMachineCaptureParameters)
var imageParameters = state.Get(constants.ArmImageParameters).(*compute.Image)
var isManagedImage = state.Get(constants.ArmIsManagedImage).(bool)
var targetManagedImageResourceGroupName = state.Get(constants.ArmManagedImageResourceGroupName).(string)
var targetManagedImageName = state.Get(constants.ArmManagedImageName).(string)
var targetManagedImageLocation = state.Get(constants.ArmLocation).(string)
s.say(fmt.Sprintf(" -> Compute ResourceGroupName : '%s'", resourceGroupName))
s.say(fmt.Sprintf(" -> Compute Name : '%s'", computeName))
s.say(fmt.Sprintf(" -> Compute Location : '%s'", location))
err := s.generalizeVM(resourceGroupName, computeName)
if err == nil {
if isManagedImage {
s.say(fmt.Sprintf(" -> Image ResourceGroupName : '%s'", targetManagedImageResourceGroupName))
s.say(fmt.Sprintf(" -> Image Name : '%s'", targetManagedImageName))
s.say(fmt.Sprintf(" -> Image Location : '%s'", targetManagedImageLocation))
err = s.captureManagedImage(ctx, targetManagedImageResourceGroupName, targetManagedImageName, imageParameters)
} else {
err = s.captureVhd(ctx, resourceGroupName, computeName, vmCaptureParameters)
}
}
if err != nil {
state.Put(constants.Error, err)
s.error(err)
return multistep.ActionHalt
}
// HACK(chrboum): I do not like this. The capture method should be returning this value
// instead having to pass in another lambda.
//
// Having to resort to capturing the template via an inspector is hack, and once I can
// resolve that I can cleanup this code too. See the comments in azure_client.go for more
// details.
// [paulmey]: autorest.Future now has access to the last http.Response, but I'm not sure if
// the body is still accessible.
template := s.get(s.client)
state.Put(constants.ArmCaptureTemplate, template)
return multistep.ActionContinue
}
func (*StepCaptureImage) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,137 @@
package arm
import (
"context"
"fmt"
"testing"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStepCaptureImageShouldFailIfCaptureFails(t *testing.T) {
var testSubject = &StepCaptureImage{
captureVhd: func(context.Context, string, string, *compute.VirtualMachineCaptureParameters) error {
return fmt.Errorf("!! Unit Test FAIL !!")
},
generalizeVM: func(string, string) error {
return nil
},
get: func(client *AzureClient) *CaptureTemplate {
return nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepCaptureImage()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepCaptureImageShouldPassIfCapturePasses(t *testing.T) {
var testSubject = &StepCaptureImage{
captureVhd: func(context.Context, string, string, *compute.VirtualMachineCaptureParameters) error { return nil },
generalizeVM: func(string, string) error {
return nil
},
get: func(client *AzureClient) *CaptureTemplate {
return nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepCaptureImage()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepCaptureImageShouldTakeStepArgumentsFromStateBag(t *testing.T) {
cancelCh := make(chan<- struct{})
defer close(cancelCh)
var actualResourceGroupName string
var actualComputeName string
var actualVirtualMachineCaptureParameters *compute.VirtualMachineCaptureParameters
actualCaptureTemplate := &CaptureTemplate{
Schema: "!! Unit Test !!",
}
var testSubject = &StepCaptureImage{
captureVhd: func(ctx context.Context, resourceGroupName string, computeName string, parameters *compute.VirtualMachineCaptureParameters) error {
actualResourceGroupName = resourceGroupName
actualComputeName = computeName
actualVirtualMachineCaptureParameters = parameters
return nil
},
generalizeVM: func(string, string) error {
return nil
},
get: func(client *AzureClient) *CaptureTemplate {
return actualCaptureTemplate
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepCaptureImage()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
var expectedComputeName = stateBag.Get(constants.ArmComputeName).(string)
var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string)
var expectedVirtualMachineCaptureParameters = stateBag.Get(constants.ArmVirtualMachineCaptureParameters).(*compute.VirtualMachineCaptureParameters)
var expectedCaptureTemplate = stateBag.Get(constants.ArmCaptureTemplate).(*CaptureTemplate)
if actualComputeName != expectedComputeName {
t.Fatal("Expected StepCaptureImage to source 'constants.ArmComputeName' from the state bag, but it did not.")
}
if actualResourceGroupName != expectedResourceGroupName {
t.Fatal("Expected StepCaptureImage to source 'constants.ArmResourceGroupName' from the state bag, but it did not.")
}
if actualVirtualMachineCaptureParameters != expectedVirtualMachineCaptureParameters {
t.Fatal("Expected StepCaptureImage to source 'constants.ArmVirtualMachineCaptureParameters' from the state bag, but it did not.")
}
if actualCaptureTemplate != expectedCaptureTemplate {
t.Fatal("Expected StepCaptureImage to source 'constants.ArmCaptureTemplate' from the state bag, but it did not.")
}
}
func createTestStateBagStepCaptureImage() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmLocation, "localhost")
stateBag.Put(constants.ArmComputeName, "Unit Test: ComputeName")
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
stateBag.Put(constants.ArmVirtualMachineCaptureParameters, &compute.VirtualMachineCaptureParameters{})
stateBag.Put(constants.ArmIsManagedImage, false)
stateBag.Put(constants.ArmManagedImageResourceGroupName, "")
stateBag.Put(constants.ArmManagedImageName, "")
stateBag.Put(constants.ArmImageParameters, &compute.Image{})
return stateBag
}

View File

@ -0,0 +1,45 @@
package arm
import (
"context"
"fmt"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
type StepCertificateInKeyVault struct {
config *Config
client common.AZVaultClientIface
say func(message string)
error func(e error)
}
func NewStepCertificateInKeyVault(cli common.AZVaultClientIface, ui packersdk.Ui, config *Config) *StepCertificateInKeyVault {
var step = &StepCertificateInKeyVault{
client: cli,
config: config,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
}
return step
}
func (s *StepCertificateInKeyVault) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Setting the certificate in the KeyVault...")
var keyVaultName = state.Get(constants.ArmKeyVaultName).(string)
err := s.client.SetSecret(keyVaultName, DefaultSecretName, s.config.winrmCertificate)
if err != nil {
s.error(fmt.Errorf("Error setting winrm cert in custom keyvault: %s", err))
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (*StepCertificateInKeyVault) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,66 @@
package arm
import (
"bytes"
"context"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
azcommon "github.com/hashicorp/packer/builder/azure/common"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestNewStepCertificateInKeyVault(t *testing.T) {
cli := azcommon.MockAZVaultClient{}
ui := &packersdk.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
state := new(multistep.BasicStateBag)
state.Put(constants.ArmKeyVaultName, "testKeyVaultName")
config := &Config{
winrmCertificate: "testCertificateString",
}
certKVStep := NewStepCertificateInKeyVault(&cli, ui, config)
stepAction := certKVStep.Run(context.TODO(), state)
if stepAction == multistep.ActionHalt {
t.Fatalf("step should have succeeded.")
}
if !cli.SetSecretCalled {
t.Fatalf("Step should have called SetSecret on Azure client.")
}
if cli.SetSecretCert != "testCertificateString" {
t.Fatalf("Step should have read cert from winRMCertificate field on config.")
}
if cli.SetSecretVaultName != "testKeyVaultName" {
t.Fatalf("step should have read keyvault name from state.")
}
}
func TestNewStepCertificateInKeyVault_error(t *testing.T) {
// Tell mock to return an error
cli := azcommon.MockAZVaultClient{}
cli.IsError = true
ui := &packersdk.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
state := new(multistep.BasicStateBag)
state.Put(constants.ArmKeyVaultName, "testKeyVaultName")
config := &Config{
winrmCertificate: "testCertificateString",
}
certKVStep := NewStepCertificateInKeyVault(&cli, ui, config)
stepAction := certKVStep.Run(context.TODO(), state)
if stepAction != multistep.ActionHalt {
t.Fatalf("step should have failed.")
}
}

View File

@ -0,0 +1,146 @@
package arm
import (
"context"
"errors"
"fmt"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-02-01/resources"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
type StepCreateResourceGroup struct {
client *AzureClient
create func(ctx context.Context, resourceGroupName string, location string, tags map[string]*string) error
say func(message string)
error func(e error)
exists func(ctx context.Context, resourceGroupName string) (bool, error)
}
func NewStepCreateResourceGroup(client *AzureClient, ui packersdk.Ui) *StepCreateResourceGroup {
var step = &StepCreateResourceGroup{
client: client,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
}
step.create = step.createResourceGroup
step.exists = step.doesResourceGroupExist
return step
}
func (s *StepCreateResourceGroup) createResourceGroup(ctx context.Context, resourceGroupName string, location string, tags map[string]*string) error {
_, err := s.client.GroupsClient.CreateOrUpdate(ctx, resourceGroupName, resources.Group{
Location: &location,
Tags: tags,
})
if err != nil {
s.say(s.client.LastError.Error())
}
return err
}
func (s *StepCreateResourceGroup) doesResourceGroupExist(ctx context.Context, resourceGroupName string) (bool, error) {
exists, err := s.client.GroupsClient.CheckExistence(ctx, resourceGroupName)
if err != nil {
s.say(s.client.LastError.Error())
}
return exists.Response.StatusCode != 404, err
}
func (s *StepCreateResourceGroup) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
var doubleResource, ok = state.GetOk(constants.ArmDoubleResourceGroupNameSet)
if ok && doubleResource.(bool) {
err := errors.New("You have filled in both temp_resource_group_name and build_resource_group_name. Please choose one.")
return processStepResult(err, s.error, state)
}
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
var location = state.Get(constants.ArmLocation).(string)
tags, ok := state.Get(constants.ArmTags).(map[string]*string)
if !ok {
err := fmt.Errorf("failed to extract tags from state bag")
state.Put(constants.Error, err)
s.error(err)
return multistep.ActionHalt
}
exists, err := s.exists(ctx, resourceGroupName)
if err != nil {
return processStepResult(err, s.error, state)
}
configThinksExists := state.Get(constants.ArmIsExistingResourceGroup).(bool)
if exists != configThinksExists {
if configThinksExists {
err = errors.New("The resource group you want to use does not exist yet. Please use temp_resource_group_name to create a temporary resource group.")
} else {
err = errors.New("A resource group with that name already exists. Please use build_resource_group_name to use an existing resource group.")
}
return processStepResult(err, s.error, state)
}
// If the resource group exists, we may not have permissions to update it so we don't.
if !exists {
s.say("Creating resource group ...")
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(ctx, resourceGroupName, location, tags)
if err == nil {
state.Put(constants.ArmIsResourceGroupCreated, true)
}
} else {
s.say("Using existing resource group ...")
s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName))
s.say(fmt.Sprintf(" -> Location : '%s'", location))
state.Put(constants.ArmIsResourceGroupCreated, true)
}
return processStepResult(err, s.error, state)
}
func (s *StepCreateResourceGroup) Cleanup(state multistep.StateBag) {
isCreated, ok := state.GetOk(constants.ArmIsResourceGroupCreated)
if !ok || !isCreated.(bool) {
return
}
ui := state.Get("ui").(packersdk.Ui)
if state.Get(constants.ArmIsExistingResourceGroup).(bool) {
ui.Say("\nThe resource group was not created by Packer, not deleting ...")
return
}
ctx := context.TODO()
resourceGroupName := state.Get(constants.ArmResourceGroupName).(string)
if exists, err := s.exists(ctx, resourceGroupName); !exists || err != nil {
return
}
ui.Say("\nCleanup requested, deleting resource group ...")
f, err := s.client.GroupsClient.Delete(ctx, resourceGroupName)
if err == nil {
if state.Get(constants.ArmAsyncResourceGroupDelete).(bool) {
s.say(fmt.Sprintf("\n Not waiting for Resource Group delete as requested by user. Resource Group Name is %s", resourceGroupName))
} else {
err = f.WaitForCompletionRef(ctx, s.client.GroupsClient.Client)
}
}
if err != nil {
ui.Error(fmt.Sprintf("Error deleting resource group. Please delete it manually.\n\n"+
"Name: %s\n"+
"Error: %s", resourceGroupName, err))
return
}
if !state.Get(constants.ArmAsyncResourceGroupDelete).(bool) {
ui.Say("Resource group has been deleted.")
}
}

View File

@ -0,0 +1,251 @@
package arm
import (
"context"
"errors"
"fmt"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStepCreateResourceGroupShouldFailIfBothGroupNames(t *testing.T) {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmDoubleResourceGroupNameSet, true)
value := "Unit Test: Tags"
tags := map[string]*string{
"tag01": &value,
}
stateBag.Put(constants.ArmTags, tags)
var testSubject = &StepCreateResourceGroup{
create: func(context.Context, string, string, map[string]*string) error { return nil },
say: func(message string) {},
error: func(e error) {},
exists: func(context.Context, string) (bool, error) { return false, nil },
}
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepCreateResourceGroupShouldFailIfCreateFails(t *testing.T) {
var testSubject = &StepCreateResourceGroup{
create: func(context.Context, string, string, map[string]*string) error {
return fmt.Errorf("!! Unit Test FAIL !!")
},
say: func(message string) {},
error: func(e error) {},
exists: func(context.Context, string) (bool, error) { return false, nil },
}
stateBag := createTestStateBagStepCreateResourceGroup()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepCreateResourceGroupShouldFailIfExistsFails(t *testing.T) {
var testSubject = &StepCreateResourceGroup{
create: func(context.Context, string, string, map[string]*string) error { return nil },
say: func(message string) {},
error: func(e error) {},
exists: func(context.Context, string) (bool, error) { return false, errors.New("FAIL") },
}
stateBag := createTestStateBagStepCreateResourceGroup()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepCreateResourceGroupShouldPassIfCreatePasses(t *testing.T) {
var testSubject = &StepCreateResourceGroup{
create: func(context.Context, string, string, map[string]*string) error { return nil },
say: func(message string) {},
error: func(e error) {},
exists: func(context.Context, string) (bool, error) { return false, nil },
}
stateBag := createTestStateBagStepCreateResourceGroup()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepCreateResourceGroupShouldTakeStepArgumentsFromStateBag(t *testing.T) {
var actualResourceGroupName string
var actualLocation string
var actualTags map[string]*string
var testSubject = &StepCreateResourceGroup{
create: func(ctx context.Context, resourceGroupName string, location string, tags map[string]*string) error {
actualResourceGroupName = resourceGroupName
actualLocation = location
actualTags = tags
return nil
},
say: func(message string) {},
error: func(e error) {},
exists: func(context.Context, string) (bool, error) { return false, nil },
}
stateBag := createTestStateBagStepCreateResourceGroup()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
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.")
}
if actualLocation != expectedLocation {
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.")
}
}
func TestStepCreateResourceGroupMarkShouldFailIfTryingExistingButDoesntExist(t *testing.T) {
var testSubject = &StepCreateResourceGroup{
create: func(context.Context, string, string, map[string]*string) error {
return fmt.Errorf("!! Unit Test FAIL !!")
},
say: func(message string) {},
error: func(e error) {},
exists: func(context.Context, string) (bool, error) { return false, nil },
}
stateBag := createTestExistingStateBagStepCreateResourceGroup()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepCreateResourceGroupMarkShouldFailIfTryingTempButExist(t *testing.T) {
var testSubject = &StepCreateResourceGroup{
create: func(context.Context, string, string, map[string]*string) error {
return fmt.Errorf("!! Unit Test FAIL !!")
},
say: func(message string) {},
error: func(e error) {},
exists: func(context.Context, string) (bool, error) { return true, nil },
}
stateBag := createTestStateBagStepCreateResourceGroup()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func createTestStateBagStepCreateResourceGroup() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmLocation, "Unit Test: Location")
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
stateBag.Put(constants.ArmIsExistingResourceGroup, false)
value := "Unit Test: Tags"
tags := map[string]*string{
"tag01": &value,
}
stateBag.Put(constants.ArmTags, tags)
return stateBag
}
func createTestExistingStateBagStepCreateResourceGroup() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmLocation, "Unit Test: Location")
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
stateBag.Put(constants.ArmIsExistingResourceGroup, true)
value := "Unit Test: Tags"
tags := map[string]*string{
"tag01": &value,
}
stateBag.Put(constants.ArmTags, tags)
return stateBag
}
func TestStepCreateResourceGroupShouldFailIfTagsFailCast(t *testing.T) {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmLocation, "Unit Test: Location")
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
stateBag.Put(constants.ArmIsExistingResourceGroup, true)
value := "Unit Test: Tags"
tags := map[string]string{
"tag01": value,
}
stateBag.Put(constants.ArmTags, tags)
var testSubject = &StepCreateResourceGroup{
create: func(context.Context, string, string, map[string]*string) error { return nil },
say: func(message string) {},
error: func(e error) {},
exists: func(context.Context, string) (bool, error) { return false, nil },
}
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}

View File

@ -0,0 +1,118 @@
package arm
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepDeleteAdditionalDisk struct {
client *AzureClient
delete func(string, string) error
deleteManaged func(context.Context, string, string) error
say func(message string)
error func(e error)
}
func NewStepDeleteAdditionalDisks(client *AzureClient, ui packersdk.Ui) *StepDeleteAdditionalDisk {
var step = &StepDeleteAdditionalDisk{
client: client,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
}
step.delete = step.deleteBlob
step.deleteManaged = step.deleteManagedDisk
return step
}
func (s *StepDeleteAdditionalDisk) deleteBlob(storageContainerName string, blobName string) error {
blob := s.client.BlobStorageClient.GetContainerReference(storageContainerName).GetBlobReference(blobName)
_, err := blob.BreakLease(nil)
if err != nil && !strings.Contains(err.Error(), "LeaseNotPresentWithLeaseOperation") {
s.say(s.client.LastError.Error())
return err
}
err = blob.Delete(nil)
if err != nil {
s.say(s.client.LastError.Error())
}
return err
}
func (s *StepDeleteAdditionalDisk) deleteManagedDisk(ctx context.Context, resourceGroupName string, diskName string) error {
xs := strings.Split(diskName, "/")
diskName = xs[len(xs)-1]
f, err := s.client.DisksClient.Delete(ctx, resourceGroupName, diskName)
if err == nil {
err = f.WaitForCompletionRef(ctx, s.client.DisksClient.Client)
}
return err
}
func (s *StepDeleteAdditionalDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Deleting the temporary Additional disk ...")
var dataDisks []string
if disks := state.Get(constants.ArmAdditionalDiskVhds); disks != nil {
dataDisks = disks.([]string)
}
var isManagedDisk = state.Get(constants.ArmIsManagedImage).(bool)
var isExistingResourceGroup = state.Get(constants.ArmIsExistingResourceGroup).(bool)
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
if dataDisks == nil {
s.say(fmt.Sprintf(" -> No Additional Disks specified"))
return multistep.ActionContinue
}
if isManagedDisk && !isExistingResourceGroup {
s.say(fmt.Sprintf(" -> Additional Disk : skipping, managed disk was used..."))
return multistep.ActionContinue
}
for i, additionaldisk := range dataDisks {
s.say(fmt.Sprintf(" -> Additional Disk %d: '%s'", i+1, additionaldisk))
var err error
if isManagedDisk {
err = s.deleteManaged(ctx, resourceGroupName, additionaldisk)
if err != nil {
s.say("Failed to delete the managed Additional Disk!")
return processStepResult(err, s.error, state)
}
continue
}
u, err := url.Parse(additionaldisk)
if err != nil {
s.say("Failed to parse the Additional Disk's VHD URI!")
return processStepResult(err, s.error, state)
}
xs := strings.Split(u.Path, "/")
if len(xs) < 3 {
err = errors.New("Failed to parse Additional Disk's VHD URI!")
} else {
var storageAccountName = xs[1]
var blobName = strings.Join(xs[2:], "/")
err = s.delete(storageAccountName, blobName)
}
if err != nil {
return processStepResult(err, s.error, state)
}
}
return multistep.ActionContinue
}
func (*StepDeleteAdditionalDisk) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,227 @@
package arm
import (
"context"
"errors"
"fmt"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStepDeleteAdditionalDiskShouldFailIfGetFails(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return fmt.Errorf("!! Unit Test FAIL !!") },
deleteManaged: func(context.Context, string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
}
stateBag := DeleteTestStateBagStepDeleteAdditionalDisk([]string{"http://storage.blob.core.windows.net/images/pkrvm_os.vhd"})
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepDeleteAdditionalDiskShouldPassIfGetPasses(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
}
stateBag := DeleteTestStateBagStepDeleteAdditionalDisk([]string{"http://storage.blob.core.windows.net/images/pkrvm_os.vhd"})
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepDeleteAdditionalDiskShouldTakeStepArgumentsFromStateBag(t *testing.T) {
var actualStorageContainerName string
var actualBlobName string
var testSubject = &StepDeleteAdditionalDisk{
delete: func(storageContainerName string, blobName string) error {
actualStorageContainerName = storageContainerName
actualBlobName = blobName
return nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := DeleteTestStateBagStepDeleteAdditionalDisk([]string{"http://storage.blob.core.windows.net/images/pkrvm_os.vhd"})
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if actualStorageContainerName != "images" {
t.Fatalf("Expected the storage container name to be 'images', but found '%s'.", actualStorageContainerName)
}
if actualBlobName != "pkrvm_os.vhd" {
t.Fatalf("Expected the blob name to be 'pkrvm_os.vhd', but found '%s'.", actualBlobName)
}
}
func TestStepDeleteAdditionalDiskShouldHandleComplexStorageContainerNames(t *testing.T) {
var actualStorageContainerName string
var actualBlobName string
var testSubject = &StepDeleteAdditionalDisk{
delete: func(storageContainerName string, blobName string) error {
actualStorageContainerName = storageContainerName
actualBlobName = blobName
return nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := DeleteTestStateBagStepDeleteAdditionalDisk([]string{"http://storage.blob.core.windows.net/abc/def/pkrvm_os.vhd"})
testSubject.Run(context.Background(), stateBag)
if actualStorageContainerName != "abc" {
t.Fatalf("Expected the storage container name to be 'abc/def', but found '%s'.", actualStorageContainerName)
}
if actualBlobName != "def/pkrvm_os.vhd" {
t.Fatalf("Expected the blob name to be 'pkrvm_os.vhd', but found '%s'.", actualBlobName)
}
}
func TestStepDeleteAdditionalDiskShouldFailIfVHDNameCannotBeURLParsed(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
deleteManaged: func(context.Context, string, string) error { return nil },
}
// Invalid URL per https://golang.org/src/net/url/url_test.go
stateBag := DeleteTestStateBagStepDeleteAdditionalDisk([]string{"http://[fe80::1%en0]/"})
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%v'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to not stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepDeleteAdditionalDiskShouldFailIfVHDNameIsTooShort(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
deleteManaged: func(context.Context, string, string) error { return nil },
}
stateBag := DeleteTestStateBagStepDeleteAdditionalDisk([]string{"storage.blob.core.windows.net/abc"})
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to not stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepDeleteAdditionalDiskShouldPassIfManagedDiskInTempResourceGroup(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
}
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmAdditionalDiskVhds, []string{"subscriptions/123-456-789/resourceGroups/existingresourcegroup/providers/Microsoft.Compute/disks/osdisk"})
stateBag.Put(constants.ArmIsManagedImage, true)
stateBag.Put(constants.ArmIsExistingResourceGroup, false)
stateBag.Put(constants.ArmResourceGroupName, "testgroup")
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepDeleteAdditionalDiskShouldFailIfManagedDiskInExistingResourceGroupFailsToDelete(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
deleteManaged: func(context.Context, string, string) error { return errors.New("UNIT TEST FAIL!") },
}
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmAdditionalDiskVhds, []string{"subscriptions/123-456-789/resourceGroups/existingresourcegroup/providers/Microsoft.Compute/disks/osdisk"})
stateBag.Put(constants.ArmIsManagedImage, true)
stateBag.Put(constants.ArmIsExistingResourceGroup, true)
stateBag.Put(constants.ArmResourceGroupName, "testgroup")
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to not stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepDeleteAdditionalDiskShouldFailIfManagedDiskInExistingResourceGroupIsDeleted(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
deleteManaged: func(context.Context, string, string) error { return nil },
}
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmAdditionalDiskVhds, []string{"subscriptions/123-456-789/resourceGroups/existingresourcegroup/providers/Microsoft.Compute/disks/osdisk"})
stateBag.Put(constants.ArmIsManagedImage, true)
stateBag.Put(constants.ArmIsExistingResourceGroup, true)
stateBag.Put(constants.ArmResourceGroupName, "testgroup")
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func DeleteTestStateBagStepDeleteAdditionalDisk(osDiskVhds []string) multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmAdditionalDiskVhds, osDiskVhds)
stateBag.Put(constants.ArmIsManagedImage, false)
stateBag.Put(constants.ArmIsExistingResourceGroup, false)
stateBag.Put(constants.ArmResourceGroupName, "testgroup")
return stateBag
}

View File

@ -0,0 +1,305 @@
package arm
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"sync"
"time"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/retry"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
type StepDeployTemplate struct {
client *AzureClient
deploy func(ctx context.Context, resourceGroupName string, deploymentName string) error
delete func(ctx context.Context, deploymentName, resourceGroupName string) error
disk func(ctx context.Context, resourceGroupName string, computeName string) (string, string, error)
deleteDisk func(ctx context.Context, imageType string, imageName string, resourceGroupName string) error
deleteDeployment func(ctx context.Context, state multistep.StateBag) error
say func(message string)
error func(e error)
config *Config
factory templateFactoryFunc
name string
}
func NewStepDeployTemplate(client *AzureClient, ui packersdk.Ui, config *Config, deploymentName string, factory templateFactoryFunc) *StepDeployTemplate {
var step = &StepDeployTemplate{
client: client,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
config: config,
factory: factory,
name: deploymentName,
}
step.deploy = step.deployTemplate
step.delete = step.deleteDeploymentResources
step.disk = step.getImageDetails
step.deleteDisk = step.deleteImage
step.deleteDeployment = step.deleteDeploymentObject
return step
}
func (s *StepDeployTemplate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Deploying deployment template ...")
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName))
s.say(fmt.Sprintf(" -> DeploymentName : '%s'", s.name))
return processStepResult(
s.deploy(ctx, resourceGroupName, s.name),
s.error, state)
}
func (s *StepDeployTemplate) Cleanup(state multistep.StateBag) {
defer func() {
err := s.deleteDeployment(context.Background(), state)
if err != nil {
s.say(err.Error())
}
}()
ui := state.Get("ui").(packersdk.Ui)
ui.Say("\nDeleting individual resources ...")
deploymentName := s.name
resourceGroupName := state.Get(constants.ArmResourceGroupName).(string)
// Get image disk details before deleting the image; otherwise we won't be able to
// delete the disk as the image request will return a 404
computeName := state.Get(constants.ArmComputeName).(string)
imageType, imageName, err := s.disk(context.TODO(), resourceGroupName, computeName)
if err != nil && !strings.Contains(err.Error(), "ResourceNotFound") {
ui.Error(fmt.Sprintf("Could not retrieve OS Image details: %s", err))
}
err = s.delete(context.TODO(), deploymentName, resourceGroupName)
if err != nil {
s.reportIfError(err, resourceGroupName)
}
// The disk was not found on the VM, this is an error.
if imageType == "" && imageName == "" {
ui.Error(fmt.Sprintf("Failed to find temporary OS disk on VM. Please delete manually.\n\n"+
"VM Name: %s\n"+
"Error: %s", computeName, err))
return
}
ui.Say(fmt.Sprintf(" Deleting -> %s : '%s'", imageType, imageName))
err = s.deleteDisk(context.TODO(), imageType, imageName, resourceGroupName)
if err != nil {
ui.Error(fmt.Sprintf("Error deleting resource. Please delete manually.\n\n"+
"Name: %s\n"+
"Error: %s", imageName, err))
}
}
func (s *StepDeployTemplate) deployTemplate(ctx context.Context, resourceGroupName string, deploymentName string) error {
deployment, err := s.factory(s.config)
if err != nil {
return err
}
f, err := s.client.DeploymentsClient.CreateOrUpdate(ctx, resourceGroupName, deploymentName, *deployment)
if err != nil {
s.say(s.client.LastError.Error())
return err
}
err = f.WaitForCompletionRef(ctx, s.client.DeploymentsClient.Client)
if err == nil {
s.say(s.client.LastError.Error())
}
return err
}
func (s *StepDeployTemplate) deleteDeploymentObject(ctx context.Context, state multistep.StateBag) error {
deploymentName := s.name
resourceGroupName := state.Get(constants.ArmResourceGroupName).(string)
ui := state.Get("ui").(packersdk.Ui)
ui.Say(fmt.Sprintf("Removing the created Deployment object: '%s'", deploymentName))
f, err := s.client.DeploymentsClient.Delete(ctx, resourceGroupName, deploymentName)
if err != nil {
return err
}
return f.WaitForCompletionRef(ctx, s.client.DeploymentsClient.Client)
}
func (s *StepDeployTemplate) getImageDetails(ctx context.Context, resourceGroupName string, computeName string) (string, string, error) {
//We can't depend on constants.ArmOSDiskVhd being set
var imageName, imageType string
vm, err := s.client.VirtualMachinesClient.Get(ctx, resourceGroupName, computeName, "")
if err != nil {
return imageName, imageType, err
}
if vm.StorageProfile.OsDisk.Vhd != nil {
imageType = "image"
imageName = *vm.StorageProfile.OsDisk.Vhd.URI
return imageType, imageName, nil
}
imageType = "Microsoft.Compute/disks"
imageName = *vm.StorageProfile.OsDisk.ManagedDisk.ID
return imageType, imageName, nil
}
//TODO(paulmey): move to helpers file
func deleteResource(ctx context.Context, client *AzureClient, resourceType string, resourceName string, resourceGroupName string) error {
switch resourceType {
case "Microsoft.Compute/virtualMachines":
f, err := client.VirtualMachinesClient.Delete(ctx, resourceGroupName, resourceName)
if err == nil {
err = f.WaitForCompletionRef(ctx, client.VirtualMachinesClient.Client)
}
return err
case "Microsoft.KeyVault/vaults":
_, err := client.VaultClientDelete.Delete(ctx, resourceGroupName, resourceName)
return err
case "Microsoft.Network/networkInterfaces":
f, err := client.InterfacesClient.Delete(ctx, resourceGroupName, resourceName)
if err == nil {
err = f.WaitForCompletionRef(ctx, client.InterfacesClient.Client)
}
return err
case "Microsoft.Network/virtualNetworks":
f, err := client.VirtualNetworksClient.Delete(ctx, resourceGroupName, resourceName)
if err == nil {
err = f.WaitForCompletionRef(ctx, client.VirtualNetworksClient.Client)
}
return err
case "Microsoft.Network/networkSecurityGroups":
f, err := client.SecurityGroupsClient.Delete(ctx, resourceGroupName, resourceName)
if err == nil {
err = f.WaitForCompletionRef(ctx, client.SecurityGroupsClient.Client)
}
return err
case "Microsoft.Network/publicIPAddresses":
f, err := client.PublicIPAddressesClient.Delete(ctx, resourceGroupName, resourceName)
if err == nil {
err = f.WaitForCompletionRef(ctx, client.PublicIPAddressesClient.Client)
}
return err
}
return nil
}
func (s *StepDeployTemplate) deleteImage(ctx context.Context, imageType string, imageName string, resourceGroupName string) error {
// Managed disk
if imageType == "Microsoft.Compute/disks" {
xs := strings.Split(imageName, "/")
diskName := xs[len(xs)-1]
f, err := s.client.DisksClient.Delete(ctx, resourceGroupName, diskName)
if err == nil {
err = f.WaitForCompletionRef(ctx, s.client.DisksClient.Client)
}
return err
}
// VHD image
u, err := url.Parse(imageName)
if err != nil {
return err
}
xs := strings.Split(u.Path, "/")
if len(xs) < 3 {
return errors.New("Unable to parse path of image " + imageName)
}
var storageAccountName = xs[1]
var blobName = strings.Join(xs[2:], "/")
blob := s.client.BlobStorageClient.GetContainerReference(storageAccountName).GetBlobReference(blobName)
_, err = blob.BreakLease(nil)
if err != nil && !strings.Contains(err.Error(), "LeaseNotPresentWithLeaseOperation") {
s.say(s.client.LastError.Error())
return err
}
return blob.Delete(nil)
}
func (s *StepDeployTemplate) deleteDeploymentResources(ctx context.Context, deploymentName, resourceGroupName string) error {
var maxResources int32 = 50
deploymentOperations, err := s.client.DeploymentOperationsClient.ListComplete(ctx, resourceGroupName, deploymentName, &maxResources)
if err != nil {
s.reportIfError(err, resourceGroupName)
return err
}
resources := map[string]string{}
for deploymentOperations.NotDone() {
deploymentOperation := deploymentOperations.Value()
// Sometimes an empty operation is added to the list by Azure
if deploymentOperation.Properties.TargetResource == nil {
_ = deploymentOperations.Next()
continue
}
resourceName := *deploymentOperation.Properties.TargetResource.ResourceName
resourceType := *deploymentOperation.Properties.TargetResource.ResourceType
s.say(fmt.Sprintf("Adding to deletion queue -> %s : '%s'", resourceType, resourceName))
resources[resourceType] = resourceName
if err = deploymentOperations.Next(); err != nil {
return err
}
}
var wg sync.WaitGroup
wg.Add(len(resources))
for resourceType, resourceName := range resources {
go func(resourceType, resourceName string) {
defer wg.Done()
retryConfig := retry.Config{
Tries: 10,
RetryDelay: (&retry.Backoff{InitialBackoff: 10 * time.Second, MaxBackoff: 600 * time.Second, Multiplier: 2}).Linear,
}
err = retryConfig.Run(ctx, func(ctx context.Context) error {
s.say(fmt.Sprintf("Attempting deletion -> %s : '%s'", resourceType, resourceName))
err := deleteResource(ctx, s.client,
resourceType,
resourceName,
resourceGroupName)
if err != nil {
s.say(fmt.Sprintf("Error deleting resource. Will retry.\n"+
"Name: %s\n"+
"Error: %s\n", resourceName, err.Error()))
}
return err
})
if err != nil {
s.reportIfError(err, resourceName)
}
}(resourceType, resourceName)
}
s.say("Waiting for deletion of all resources...")
wg.Wait()
return nil
}
func (s *StepDeployTemplate) reportIfError(err error, resourceName string) {
if err != nil {
s.say(fmt.Sprintf("Error deleting resource. Please delete manually.\n\n"+
"Name: %s\n"+
"Error: %s", resourceName, err.Error()))
s.error(err)
}
}

View File

@ -0,0 +1,205 @@
package arm
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStepDeployTemplateShouldFailIfDeployFails(t *testing.T) {
var testSubject = &StepDeployTemplate{
deploy: func(context.Context, string, string) error {
return fmt.Errorf("!! Unit Test FAIL !!")
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepDeployTemplate()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepDeployTemplateShouldPassIfDeployPasses(t *testing.T) {
var testSubject = &StepDeployTemplate{
deploy: func(context.Context, string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepDeployTemplate()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepDeployTemplateShouldTakeStepArgumentsFromStateBag(t *testing.T) {
var actualResourceGroupName string
var actualDeploymentName string
var testSubject = &StepDeployTemplate{
deploy: func(ctx context.Context, resourceGroupName string, deploymentName string) error {
actualResourceGroupName = resourceGroupName
actualDeploymentName = deploymentName
return nil
},
say: func(message string) {},
error: func(e error) {},
name: "--deployment-name--",
}
stateBag := createTestStateBagStepValidateTemplate()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string)
if actualDeploymentName != "--deployment-name--" {
t.Fatal("Expected StepValidateTemplate to source 'constants.ArmDeploymentName' from the state bag, but it did not.")
}
if actualResourceGroupName != expectedResourceGroupName {
t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.")
}
}
func TestStepDeployTemplateDeleteImageShouldFailWhenImageUrlCannotBeParsed(t *testing.T) {
var testSubject = &StepDeployTemplate{
say: func(message string) {},
error: func(e error) {},
name: "--deployment-name--",
}
// Invalid URL per https://golang.org/src/net/url/url_test.go
err := testSubject.deleteImage(context.TODO(), "image", "http://[fe80::1%en0]/", "Unit Test: ResourceGroupName")
if err == nil {
t.Fatal("Expected a failure because of the failed image name")
}
}
func TestStepDeployTemplateDeleteImageShouldFailWithInvalidImage(t *testing.T) {
var testSubject = &StepDeployTemplate{
say: func(message string) {},
error: func(e error) {},
name: "--deployment-name--",
}
err := testSubject.deleteImage(context.TODO(), "image", "storage.blob.core.windows.net/abc", "Unit Test: ResourceGroupName")
if err == nil {
t.Fatal("Expected a failure because of the failed image name")
}
}
func TestStepDeployTemplateCleanupShouldDeleteManagedOSImageInExistingResourceGroup(t *testing.T) {
var deleteDiskCounter = 0
var testSubject = createTestStepDeployTemplateDeleteOSImage(&deleteDiskCounter)
stateBag := createTestStateBagStepDeployTemplate()
stateBag.Put(constants.ArmIsManagedImage, true)
stateBag.Put(constants.ArmIsExistingResourceGroup, true)
stateBag.Put(constants.ArmIsResourceGroupCreated, true)
stateBag.Put("ui", packersdk.TestUi(t))
testSubject.Cleanup(stateBag)
if deleteDiskCounter != 1 {
t.Fatalf("Expected DeployTemplate Cleanup to invoke deleteDisk 1 time, but invoked %d times", deleteDiskCounter)
}
}
func TestStepDeployTemplateCleanupShouldDeleteManagedOSImageInTemporaryResourceGroup(t *testing.T) {
var deleteDiskCounter = 0
var testSubject = createTestStepDeployTemplateDeleteOSImage(&deleteDiskCounter)
stateBag := createTestStateBagStepDeployTemplate()
stateBag.Put(constants.ArmIsManagedImage, true)
stateBag.Put(constants.ArmIsExistingResourceGroup, false)
stateBag.Put(constants.ArmIsResourceGroupCreated, true)
stateBag.Put("ui", packersdk.TestUi(t))
testSubject.Cleanup(stateBag)
if deleteDiskCounter != 1 {
t.Fatalf("Expected DeployTemplate Cleanup to invoke deleteDisk 1 times, but invoked %d times", deleteDiskCounter)
}
}
func TestStepDeployTemplateCleanupShouldDeleteVHDOSImageInExistingResourceGroup(t *testing.T) {
var deleteDiskCounter = 0
var testSubject = createTestStepDeployTemplateDeleteOSImage(&deleteDiskCounter)
stateBag := createTestStateBagStepDeployTemplate()
stateBag.Put(constants.ArmIsManagedImage, false)
stateBag.Put(constants.ArmIsExistingResourceGroup, true)
stateBag.Put(constants.ArmIsResourceGroupCreated, true)
stateBag.Put("ui", packersdk.TestUi(t))
testSubject.Cleanup(stateBag)
if deleteDiskCounter != 1 {
t.Fatalf("Expected DeployTemplate Cleanup to invoke deleteDisk 1 time, but invoked %d times", deleteDiskCounter)
}
}
func TestStepDeployTemplateCleanupShouldVHDOSImageInTemporaryResourceGroup(t *testing.T) {
var deleteDiskCounter = 0
var testSubject = createTestStepDeployTemplateDeleteOSImage(&deleteDiskCounter)
stateBag := createTestStateBagStepDeployTemplate()
stateBag.Put(constants.ArmIsManagedImage, false)
stateBag.Put(constants.ArmIsExistingResourceGroup, false)
stateBag.Put(constants.ArmIsResourceGroupCreated, true)
stateBag.Put("ui", packersdk.TestUi(t))
testSubject.Cleanup(stateBag)
if deleteDiskCounter != 1 {
t.Fatalf("Expected DeployTemplate Cleanup to invoke deleteDisk 1 times, but invoked %d times", deleteDiskCounter)
}
}
func createTestStateBagStepDeployTemplate() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmDeploymentName, "Unit Test: DeploymentName")
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
stateBag.Put(constants.ArmComputeName, "Unit Test: ComputeName")
return stateBag
}
func createTestStepDeployTemplateDeleteOSImage(deleteDiskCounter *int) *StepDeployTemplate {
return &StepDeployTemplate{
deploy: func(context.Context, string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
deleteDisk: func(ctx context.Context, imageType string, imageName string, resourceGroupName string) error {
*deleteDiskCounter++
return nil
},
disk: func(ctx context.Context, resourceGroupName, computeName string) (string, string, error) {
return "Microsoft.Compute/disks", "", nil
},
delete: func(ctx context.Context, deploymentName, resourceGroupName string) error {
return nil
},
deleteDeployment: func(ctx context.Context, state multistep.StateBag) error {
return nil
},
}
}

View File

@ -0,0 +1,78 @@
package arm
import (
"context"
"fmt"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepGetDataDisk struct {
client *AzureClient
query func(ctx context.Context, resourceGroupName string, computeName string) (compute.VirtualMachine, error)
say func(message string)
error func(e error)
}
func NewStepGetAdditionalDisks(client *AzureClient, ui packersdk.Ui) *StepGetDataDisk {
var step = &StepGetDataDisk{
client: client,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
}
step.query = step.queryCompute
return step
}
func (s *StepGetDataDisk) queryCompute(ctx context.Context, resourceGroupName string, computeName string) (compute.VirtualMachine, error) {
vm, err := s.client.VirtualMachinesClient.Get(ctx, resourceGroupName, computeName, "")
if err != nil {
s.say(s.client.LastError.Error())
}
return vm, err
}
func (s *StepGetDataDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Querying the machine's additional disks properties ...")
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
var computeName = state.Get(constants.ArmComputeName).(string)
s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName))
s.say(fmt.Sprintf(" -> ComputeName : '%s'", computeName))
vm, err := s.query(ctx, resourceGroupName, computeName)
if err != nil {
state.Put(constants.Error, err)
s.error(err)
return multistep.ActionHalt
}
if vm.StorageProfile.DataDisks != nil {
var vhdUri string
additional_disks := make([]string, len(*vm.StorageProfile.DataDisks))
for i, additionaldisk := range *vm.StorageProfile.DataDisks {
if additionaldisk.Vhd != nil {
vhdUri = *additionaldisk.Vhd.URI
s.say(fmt.Sprintf(" -> Additional Disk %d : '%s'", i+1, vhdUri))
} else {
vhdUri = *additionaldisk.ManagedDisk.ID
s.say(fmt.Sprintf(" -> Managed Additional Disk %d : '%s'", i+1, vhdUri))
}
additional_disks[i] = vhdUri
}
state.Put(constants.ArmAdditionalDiskVhds, additional_disks)
}
return multistep.ActionContinue
}
func (*StepGetDataDisk) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,131 @@
package arm
import (
"context"
"fmt"
"testing"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer-plugin-sdk/multistep"
)
func TestStepGetAdditionalDiskShouldFailIfGetFails(t *testing.T) {
var testSubject = &StepGetDataDisk{
query: func(context.Context, string, string) (compute.VirtualMachine, error) {
return createVirtualMachineWithDataDisksFromUri("test.vhd"), fmt.Errorf("!! Unit Test FAIL !!")
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepGetAdditionalDisks()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepGetAdditionalDiskShouldPassIfGetPasses(t *testing.T) {
var testSubject = &StepGetDataDisk{
query: func(context.Context, string, string) (compute.VirtualMachine, error) {
return createVirtualMachineWithDataDisksFromUri("test.vhd"), nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepGetAdditionalDisks()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepGetAdditionalDiskShouldTakeValidateArgumentsFromStateBag(t *testing.T) {
var actualResourceGroupName string
var actualComputeName string
var testSubject = &StepGetDataDisk{
query: func(ctx context.Context, resourceGroupName string, computeName string) (compute.VirtualMachine, error) {
actualResourceGroupName = resourceGroupName
actualComputeName = computeName
return createVirtualMachineWithDataDisksFromUri("test.vhd"), nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepGetAdditionalDisks()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
var expectedComputeName = stateBag.Get(constants.ArmComputeName).(string)
var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string)
if actualComputeName != expectedComputeName {
t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.")
}
if actualResourceGroupName != expectedResourceGroupName {
t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.")
}
expectedAdditionalDiskVhds, ok := stateBag.GetOk(constants.ArmAdditionalDiskVhds)
if !ok {
t.Fatalf("Expected the state bag to have a value for '%s', but it did not.", constants.ArmAdditionalDiskVhds)
}
expectedAdditionalDiskVhd := expectedAdditionalDiskVhds.([]string)
if expectedAdditionalDiskVhd[0] != "test.vhd" {
t.Fatalf("Expected the value of stateBag[%s] to be 'test.vhd', but got '%s'.", constants.ArmAdditionalDiskVhds, expectedAdditionalDiskVhd[0])
}
}
func createTestStateBagStepGetAdditionalDisks() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmComputeName, "Unit Test: ComputeName")
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
return stateBag
}
func createVirtualMachineWithDataDisksFromUri(vhdUri string) compute.VirtualMachine {
vm := compute.VirtualMachine{
VirtualMachineProperties: &compute.VirtualMachineProperties{
StorageProfile: &compute.StorageProfile{
OsDisk: &compute.OSDisk{
Vhd: &compute.VirtualHardDisk{
URI: &vhdUri,
},
},
DataDisks: &[]compute.DataDisk{
{
Vhd: &compute.VirtualHardDisk{
URI: &vhdUri,
},
},
},
},
},
}
return vm
}

View File

@ -0,0 +1,77 @@
package arm
import (
"context"
"fmt"
"time"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
type StepGetCertificate struct {
client *AzureClient
get func(keyVaultName string, secretName string) (string, error)
say func(message string)
error func(e error)
pause func()
}
func NewStepGetCertificate(client *AzureClient, ui packersdk.Ui) *StepGetCertificate {
var step = &StepGetCertificate{
client: client,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
pause: func() { time.Sleep(30 * time.Second) },
}
step.get = step.getCertificateUrl
return step
}
func (s *StepGetCertificate) getCertificateUrl(keyVaultName string, secretName string) (string, error) {
secret, err := s.client.GetSecret(keyVaultName, secretName)
if err != nil {
s.say(s.client.LastError.Error())
return "", err
}
return *secret.ID, err
}
func (s *StepGetCertificate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Getting the certificate's URL ...")
var keyVaultName = state.Get(constants.ArmKeyVaultName).(string)
s.say(fmt.Sprintf(" -> Key Vault Name : '%s'", keyVaultName))
s.say(fmt.Sprintf(" -> Key Vault Secret Name : '%s'", DefaultSecretName))
var err error
var url string
for i := 0; i < 5; i++ {
url, err = s.get(keyVaultName, DefaultSecretName)
if err == nil {
break
}
s.say(fmt.Sprintf(" ...failed to get certificate URL, retry(%d)", i))
s.pause()
}
if err != nil {
state.Put(constants.Error, err)
s.error(err)
return multistep.ActionHalt
}
s.say(fmt.Sprintf(" -> Certificate URL : '%s'", url))
state.Put(constants.ArmCertificateUrl, url)
return multistep.ActionContinue
}
func (*StepGetCertificate) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,99 @@
package arm
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStepGetCertificateShouldFailIfGetFails(t *testing.T) {
var testSubject = &StepGetCertificate{
get: func(string, string) (string, error) { return "", fmt.Errorf("!! Unit Test FAIL !!") },
say: func(message string) {},
error: func(e error) {},
pause: func() {},
}
stateBag := createTestStateBagStepGetCertificate()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepGetCertificateShouldPassIfGetPasses(t *testing.T) {
var testSubject = &StepGetCertificate{
get: func(string, string) (string, error) { return "", nil },
say: func(message string) {},
error: func(e error) {},
pause: func() {},
}
stateBag := createTestStateBagStepGetCertificate()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepGetCertificateShouldTakeStepArgumentsFromStateBag(t *testing.T) {
var actualKeyVaultName string
var actualSecretName string
var testSubject = &StepGetCertificate{
get: func(keyVaultName string, secretName string) (string, error) {
actualKeyVaultName = keyVaultName
actualSecretName = secretName
return "http://key.vault/1", nil
},
say: func(message string) {},
error: func(e error) {},
pause: func() {},
}
stateBag := createTestStateBagStepGetCertificate()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
var expectedKeyVaultName = stateBag.Get(constants.ArmKeyVaultName).(string)
if actualKeyVaultName != expectedKeyVaultName {
t.Fatal("Expected StepGetCertificate to source 'constants.ArmKeyVaultName' from the state bag, but it did not.")
}
if actualSecretName != DefaultSecretName {
t.Fatal("Expected StepGetCertificate to use default value for secret, but it did not.")
}
expectedCertificateUrl, ok := stateBag.GetOk(constants.ArmCertificateUrl)
if !ok {
t.Fatalf("Expected the state bag to have a value for '%s', but it did not.", constants.ArmCertificateUrl)
}
if expectedCertificateUrl != "http://key.vault/1" {
t.Fatalf("Expected the value of stateBag[%s] to be 'http://key.vault/1', but got '%s'.", constants.ArmCertificateUrl, expectedCertificateUrl)
}
}
func createTestStateBagStepGetCertificate() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmKeyVaultName, "Unit Test: KeyVaultName")
return stateBag
}

View File

@ -0,0 +1,107 @@
package arm
import (
"context"
"fmt"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
type EndpointType int
const (
PublicEndpoint EndpointType = iota
PrivateEndpoint
PublicEndpointInPrivateNetwork
)
var (
EndpointCommunicationText = map[EndpointType]string{
PublicEndpoint: "PublicEndpoint",
PrivateEndpoint: "PrivateEndpoint",
PublicEndpointInPrivateNetwork: "PublicEndpointInPrivateNetwork",
}
)
type StepGetIPAddress struct {
client *AzureClient
endpoint EndpointType
get func(ctx context.Context, resourceGroupName string, ipAddressName string, interfaceName string) (string, error)
say func(message string)
error func(e error)
}
func NewStepGetIPAddress(client *AzureClient, ui packersdk.Ui, endpoint EndpointType) *StepGetIPAddress {
var step = &StepGetIPAddress{
client: client,
endpoint: endpoint,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
}
switch endpoint {
case PrivateEndpoint:
step.get = step.getPrivateIP
case PublicEndpoint:
step.get = step.getPublicIP
case PublicEndpointInPrivateNetwork:
step.get = step.getPublicIPInPrivateNetwork
}
return step
}
func (s *StepGetIPAddress) getPrivateIP(ctx context.Context, resourceGroupName string, ipAddressName string, interfaceName string) (string, error) {
resp, err := s.client.InterfacesClient.Get(ctx, resourceGroupName, interfaceName, "")
if err != nil {
s.say(s.client.LastError.Error())
return "", err
}
return *(*resp.IPConfigurations)[0].PrivateIPAddress, nil
}
func (s *StepGetIPAddress) getPublicIP(ctx context.Context, resourceGroupName string, ipAddressName string, interfaceName string) (string, error) {
resp, err := s.client.PublicIPAddressesClient.Get(ctx, resourceGroupName, ipAddressName, "")
if err != nil {
return "", err
}
return *resp.IPAddress, nil
}
func (s *StepGetIPAddress) getPublicIPInPrivateNetwork(ctx context.Context, resourceGroupName string, ipAddressName string, interfaceName string) (string, error) {
s.getPrivateIP(ctx, resourceGroupName, ipAddressName, interfaceName)
return s.getPublicIP(ctx, resourceGroupName, ipAddressName, interfaceName)
}
func (s *StepGetIPAddress) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Getting the VM's IP address ...")
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
var ipAddressName = state.Get(constants.ArmPublicIPAddressName).(string)
var nicName = state.Get(constants.ArmNicName).(string)
s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName))
s.say(fmt.Sprintf(" -> PublicIPAddressName : '%s'", ipAddressName))
s.say(fmt.Sprintf(" -> NicName : '%s'", nicName))
s.say(fmt.Sprintf(" -> Network Connection : '%s'", EndpointCommunicationText[s.endpoint]))
address, err := s.get(ctx, resourceGroupName, ipAddressName, nicName)
if err != nil {
state.Put(constants.Error, err)
s.error(err)
return multistep.ActionHalt
}
state.Put(constants.SSHHost, address)
s.say(fmt.Sprintf(" -> IP Address : '%s'", address))
return multistep.ActionContinue
}
func (*StepGetIPAddress) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,124 @@
package arm
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStepGetIPAddressShouldFailIfGetFails(t *testing.T) {
endpoints := []EndpointType{PublicEndpoint, PublicEndpointInPrivateNetwork}
for _, endpoint := range endpoints {
var testSubject = &StepGetIPAddress{
get: func(context.Context, string, string, string) (string, error) {
return "", fmt.Errorf("!! Unit Test FAIL !!")
},
endpoint: endpoint,
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepGetIPAddress()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
}
func TestStepGetIPAddressShouldPassIfGetPasses(t *testing.T) {
endpoints := []EndpointType{PublicEndpoint, PublicEndpointInPrivateNetwork}
for _, endpoint := range endpoints {
var testSubject = &StepGetIPAddress{
get: func(context.Context, string, string, string) (string, error) { return "", nil },
endpoint: endpoint,
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepGetIPAddress()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
}
func TestStepGetIPAddressShouldTakeStepArgumentsFromStateBag(t *testing.T) {
var actualResourceGroupName string
var actualIPAddressName string
var actualNicName string
endpoints := []EndpointType{PublicEndpoint, PublicEndpointInPrivateNetwork}
for _, endpoint := range endpoints {
var testSubject = &StepGetIPAddress{
get: func(ctx context.Context, resourceGroupName string, ipAddressName string, nicName string) (string, error) {
actualResourceGroupName = resourceGroupName
actualIPAddressName = ipAddressName
actualNicName = nicName
return "127.0.0.1", nil
},
endpoint: endpoint,
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepGetIPAddress()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string)
var expectedIPAddressName = stateBag.Get(constants.ArmPublicIPAddressName).(string)
var expectedNicName = stateBag.Get(constants.ArmNicName).(string)
if actualIPAddressName != expectedIPAddressName {
t.Fatal("Expected StepGetIPAddress to source 'constants.ArmIPAddressName' from the state bag, but it did not.")
}
if actualResourceGroupName != expectedResourceGroupName {
t.Fatal("Expected StepGetIPAddress to source 'constants.ArmResourceGroupName' from the state bag, but it did not.")
}
if actualNicName != expectedNicName {
t.Fatalf("Expected StepGetIPAddress to source 'constants.ArmNetworkInterfaceName' from the state bag, but it did not.")
}
expectedIPAddress, ok := stateBag.GetOk(constants.SSHHost)
if !ok {
t.Fatalf("Expected the state bag to have a value for '%s', but it did not.", constants.SSHHost)
}
if expectedIPAddress != "127.0.0.1" {
t.Fatalf("Expected the value of stateBag[%s] to be '127.0.0.1', but got '%s'.", constants.SSHHost, expectedIPAddress)
}
}
}
func createTestStateBagStepGetIPAddress() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmPublicIPAddressName, "Unit Test: PublicIPAddressName")
stateBag.Put(constants.ArmNicName, "Unit Test: NicName")
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
return stateBag
}

View File

@ -0,0 +1,72 @@
package arm
import (
"context"
"fmt"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepGetOSDisk struct {
client *AzureClient
query func(ctx context.Context, resourceGroupName string, computeName string) (compute.VirtualMachine, error)
say func(message string)
error func(e error)
}
func NewStepGetOSDisk(client *AzureClient, ui packersdk.Ui) *StepGetOSDisk {
var step = &StepGetOSDisk{
client: client,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
}
step.query = step.queryCompute
return step
}
func (s *StepGetOSDisk) queryCompute(ctx context.Context, resourceGroupName string, computeName string) (compute.VirtualMachine, error) {
vm, err := s.client.VirtualMachinesClient.Get(ctx, resourceGroupName, computeName, "")
if err != nil {
s.say(s.client.LastError.Error())
}
return vm, err
}
func (s *StepGetOSDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Querying the machine's properties ...")
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
var computeName = state.Get(constants.ArmComputeName).(string)
s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName))
s.say(fmt.Sprintf(" -> ComputeName : '%s'", computeName))
vm, err := s.query(ctx, resourceGroupName, computeName)
if err != nil {
state.Put(constants.Error, err)
s.error(err)
return multistep.ActionHalt
}
var vhdUri string
if vm.StorageProfile.OsDisk.Vhd != nil {
vhdUri = *vm.StorageProfile.OsDisk.Vhd.URI
s.say(fmt.Sprintf(" -> OS Disk : '%s'", vhdUri))
} else {
vhdUri = *vm.StorageProfile.OsDisk.ManagedDisk.ID
s.say(fmt.Sprintf(" -> Managed OS Disk : '%s'", vhdUri))
}
state.Put(constants.ArmOSDiskVhd, vhdUri)
return multistep.ActionContinue
}
func (*StepGetOSDisk) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,123 @@
package arm
import (
"context"
"fmt"
"testing"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer-plugin-sdk/multistep"
)
func TestStepGetOSDiskShouldFailIfGetFails(t *testing.T) {
var testSubject = &StepGetOSDisk{
query: func(context.Context, string, string) (compute.VirtualMachine, error) {
return createVirtualMachineFromUri("test.vhd"), fmt.Errorf("!! Unit Test FAIL !!")
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepGetOSDisk()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepGetOSDiskShouldPassIfGetPasses(t *testing.T) {
var testSubject = &StepGetOSDisk{
query: func(context.Context, string, string) (compute.VirtualMachine, error) {
return createVirtualMachineFromUri("test.vhd"), nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepGetOSDisk()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepGetOSDiskShouldTakeValidateArgumentsFromStateBag(t *testing.T) {
var actualResourceGroupName string
var actualComputeName string
var testSubject = &StepGetOSDisk{
query: func(ctx context.Context, resourceGroupName string, computeName string) (compute.VirtualMachine, error) {
actualResourceGroupName = resourceGroupName
actualComputeName = computeName
return createVirtualMachineFromUri("test.vhd"), nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepGetOSDisk()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
var expectedComputeName = stateBag.Get(constants.ArmComputeName).(string)
var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string)
if actualComputeName != expectedComputeName {
t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.")
}
if actualResourceGroupName != expectedResourceGroupName {
t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.")
}
expectedOSDiskVhd, ok := stateBag.GetOk(constants.ArmOSDiskVhd)
if !ok {
t.Fatalf("Expected the state bag to have a value for '%s', but it did not.", constants.ArmOSDiskVhd)
}
if expectedOSDiskVhd != "test.vhd" {
t.Fatalf("Expected the value of stateBag[%s] to be 'test.vhd', but got '%s'.", constants.ArmOSDiskVhd, expectedOSDiskVhd)
}
}
func createTestStateBagStepGetOSDisk() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmComputeName, "Unit Test: ComputeName")
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
return stateBag
}
func createVirtualMachineFromUri(vhdUri string) compute.VirtualMachine {
vm := compute.VirtualMachine{
VirtualMachineProperties: &compute.VirtualMachineProperties{
StorageProfile: &compute.StorageProfile{
OsDisk: &compute.OSDisk{
Vhd: &compute.VirtualHardDisk{
URI: &vhdUri,
},
},
},
},
}
return vm
}

View File

@ -0,0 +1,56 @@
package arm
import (
"context"
"fmt"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
type StepPowerOffCompute struct {
client *AzureClient
powerOff func(ctx context.Context, resourceGroupName string, computeName string) error
say func(message string)
error func(e error)
}
func NewStepPowerOffCompute(client *AzureClient, ui packersdk.Ui) *StepPowerOffCompute {
var step = &StepPowerOffCompute{
client: client,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
}
step.powerOff = step.powerOffCompute
return step
}
func (s *StepPowerOffCompute) powerOffCompute(ctx context.Context, resourceGroupName string, computeName string) error {
f, err := s.client.VirtualMachinesClient.Deallocate(ctx, resourceGroupName, computeName)
if err == nil {
err = f.WaitForCompletionRef(ctx, s.client.VirtualMachinesClient.Client)
}
if err != nil {
s.say(s.client.LastError.Error())
}
return err
}
func (s *StepPowerOffCompute) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Powering off machine ...")
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
var computeName = state.Get(constants.ArmComputeName).(string)
s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName))
s.say(fmt.Sprintf(" -> ComputeName : '%s'", computeName))
err := s.powerOff(ctx, resourceGroupName, computeName)
return processStepResult(err, s.error, state)
}
func (*StepPowerOffCompute) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,91 @@
package arm
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStepPowerOffComputeShouldFailIfPowerOffFails(t *testing.T) {
var testSubject = &StepPowerOffCompute{
powerOff: func(context.Context, string, string) error { return fmt.Errorf("!! Unit Test FAIL !!") },
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepPowerOffCompute()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepPowerOffComputeShouldPassIfPowerOffPasses(t *testing.T) {
var testSubject = &StepPowerOffCompute{
powerOff: func(context.Context, string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepPowerOffCompute()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepPowerOffComputeShouldTakeStepArgumentsFromStateBag(t *testing.T) {
var actualResourceGroupName string
var actualComputeName string
var testSubject = &StepPowerOffCompute{
powerOff: func(ctx context.Context, resourceGroupName string, computeName string) error {
actualResourceGroupName = resourceGroupName
actualComputeName = computeName
return nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepPowerOffCompute()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
var expectedComputeName = stateBag.Get(constants.ArmComputeName).(string)
var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string)
if actualComputeName != expectedComputeName {
t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.")
}
if actualResourceGroupName != expectedResourceGroupName {
t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.")
}
}
func createTestStateBagStepPowerOffCompute() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmComputeName, "Unit Test: ComputeName")
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
return stateBag
}

View File

@ -0,0 +1,158 @@
package arm
import (
"context"
"fmt"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-03-01/compute"
"github.com/Azure/go-autorest/autorest/date"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
type StepPublishToSharedImageGallery struct {
client *AzureClient
publish func(ctx context.Context, mdiID, miSigPubRg, miSIGalleryName, miSGImageName, miSGImageVersion string, miSigReplicationRegions []string, miSGImageVersionEndOfLifeDate string, miSGImageVersionExcludeFromLatest bool, miSigReplicaCount int32, location string, tags map[string]*string) (string, error)
say func(message string)
error func(e error)
toSIG func() bool
}
func NewStepPublishToSharedImageGallery(client *AzureClient, ui packersdk.Ui, config *Config) *StepPublishToSharedImageGallery {
var step = &StepPublishToSharedImageGallery{
client: client,
say: func(message string) {
ui.Say(message)
},
error: func(e error) {
ui.Error(e.Error())
},
toSIG: func() bool {
return config.isManagedImage() && config.SharedGalleryDestination.SigDestinationGalleryName != ""
},
}
step.publish = step.publishToSig
return step
}
func (s *StepPublishToSharedImageGallery) publishToSig(ctx context.Context, mdiID string, miSigPubRg string, miSIGalleryName string, miSGImageName string, miSGImageVersion string, miSigReplicationRegions []string, miSGImageVersionEndOfLifeDate string, miSGImageVersionExcludeFromLatest bool, miSigReplicaCount int32, location string, tags map[string]*string) (string, error) {
replicationRegions := make([]compute.TargetRegion, len(miSigReplicationRegions))
for i, v := range miSigReplicationRegions {
regionName := v
replicationRegions[i] = compute.TargetRegion{Name: &regionName}
}
var endOfLifeDate *date.Time
if miSGImageVersionEndOfLifeDate != "" {
parseDate, err := date.ParseTime("2006-01-02T15:04:05.99Z", miSGImageVersionEndOfLifeDate)
if err != nil {
s.say(fmt.Sprintf("Error parsing date from shared_gallery_image_version_end_of_life_date: %s", err))
return "", err
}
endOfLifeDate = &date.Time{Time: parseDate}
} else {
endOfLifeDate = (*date.Time)(nil)
}
galleryImageVersion := compute.GalleryImageVersion{
Location: &location,
Tags: tags,
GalleryImageVersionProperties: &compute.GalleryImageVersionProperties{
PublishingProfile: &compute.GalleryImageVersionPublishingProfile{
Source: &compute.GalleryArtifactSource{
ManagedImage: &compute.ManagedArtifact{
ID: &mdiID,
},
},
TargetRegions: &replicationRegions,
EndOfLifeDate: endOfLifeDate,
ExcludeFromLatest: &miSGImageVersionExcludeFromLatest,
ReplicaCount: &miSigReplicaCount,
},
},
}
f, err := s.client.GalleryImageVersionsClient.CreateOrUpdate(ctx, miSigPubRg, miSIGalleryName, miSGImageName, miSGImageVersion, galleryImageVersion)
if err != nil {
s.say(s.client.LastError.Error())
return "", err
}
err = f.WaitForCompletionRef(ctx, s.client.GalleryImageVersionsClient.Client)
if err != nil {
s.say(s.client.LastError.Error())
return "", err
}
createdSGImageVersion, err := f.Result(s.client.GalleryImageVersionsClient)
if err != nil {
s.say(s.client.LastError.Error())
return "", err
}
s.say(fmt.Sprintf(" -> Shared Gallery Image Version ID : '%s'", *(createdSGImageVersion.ID)))
return *(createdSGImageVersion.ID), nil
}
func (s *StepPublishToSharedImageGallery) Run(ctx context.Context, stateBag multistep.StateBag) multistep.StepAction {
if !s.toSIG() {
return multistep.ActionContinue
}
s.say("Publishing to Shared Image Gallery ...")
location := stateBag.Get(constants.ArmLocation).(string)
miSigPubRg := stateBag.Get(constants.ArmManagedImageSigPublishResourceGroup).(string)
miSIGalleryName := stateBag.Get(constants.ArmManagedImageSharedGalleryName).(string)
miSGImageName := stateBag.Get(constants.ArmManagedImageSharedGalleryImageName).(string)
miSGImageVersion := stateBag.Get(constants.ArmManagedImageSharedGalleryImageVersion).(string)
miSigReplicationRegions := stateBag.Get(constants.ArmManagedImageSharedGalleryReplicationRegions).([]string)
tags := stateBag.Get(constants.ArmTags).(map[string]*string)
targetManagedImageResourceGroupName := stateBag.Get(constants.ArmManagedImageResourceGroupName).(string)
targetManagedImageName := stateBag.Get(constants.ArmManagedImageName).(string)
managedImageSubscription := stateBag.Get(constants.ArmManagedImageSubscription).(string)
mdiID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/images/%s", managedImageSubscription, targetManagedImageResourceGroupName, targetManagedImageName)
miSGImageVersionEndOfLifeDate, _ := stateBag.Get(constants.ArmManagedImageSharedGalleryImageVersionEndOfLifeDate).(string)
miSGImageVersionExcludeFromLatest, _ := stateBag.Get(constants.ArmManagedImageSharedGalleryImageVersionExcludeFromLatest).(bool)
miSigReplicaCount, _ := stateBag.Get(constants.ArmManagedImageSharedGalleryImageVersionReplicaCount).(int32)
// Replica count must be between 1 and 10 inclusive.
if miSigReplicaCount <= 0 {
miSigReplicaCount = constants.SharedImageGalleryImageVersionDefaultMinReplicaCount
} else if miSigReplicaCount > 10 {
miSigReplicaCount = constants.SharedImageGalleryImageVersionDefaultMaxReplicaCount
}
s.say(fmt.Sprintf(" -> MDI ID used for SIG publish : '%s'", mdiID))
s.say(fmt.Sprintf(" -> SIG publish resource group : '%s'", miSigPubRg))
s.say(fmt.Sprintf(" -> SIG gallery name : '%s'", miSIGalleryName))
s.say(fmt.Sprintf(" -> SIG image name : '%s'", miSGImageName))
s.say(fmt.Sprintf(" -> SIG image version : '%s'", miSGImageVersion))
s.say(fmt.Sprintf(" -> SIG replication regions : '%v'", miSigReplicationRegions))
s.say(fmt.Sprintf(" -> SIG image version endoflife date : '%s'", miSGImageVersionEndOfLifeDate))
s.say(fmt.Sprintf(" -> SIG image version exclude from latest : '%t'", miSGImageVersionExcludeFromLatest))
s.say(fmt.Sprintf(" -> SIG replica count [1, 10] : '%d'", miSigReplicaCount))
createdGalleryImageVersionID, err := s.publish(ctx, mdiID, miSigPubRg, miSIGalleryName, miSGImageName, miSGImageVersion, miSigReplicationRegions, miSGImageVersionEndOfLifeDate, miSGImageVersionExcludeFromLatest, miSigReplicaCount, location, tags)
if err != nil {
stateBag.Put(constants.Error, err)
s.error(err)
return multistep.ActionHalt
}
stateBag.Put(constants.ArmManagedImageSharedGalleryId, createdGalleryImageVersionID)
return multistep.ActionContinue
}
func (*StepPublishToSharedImageGallery) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,85 @@
package arm
import (
"context"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStepPublishToSharedImageGalleryShouldNotPublishForVhd(t *testing.T) {
var testSubject = &StepPublishToSharedImageGallery{
publish: func(context.Context, string, string, string, string, string, []string, string, bool, int32, string, map[string]*string) (string, error) {
return "test", nil
},
say: func(message string) {},
error: func(e error) {},
toSIG: func() bool { return false },
}
stateBag := createTestStateBagStepPublishToSharedImageGalleryForVhd()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepPublishToSharedImageGalleryShouldPublishForManagedImageWithSig(t *testing.T) {
var testSubject = &StepPublishToSharedImageGallery{
publish: func(context.Context, string, string, string, string, string, []string, string, bool, int32, string, map[string]*string) (string, error) {
return "", nil
},
say: func(message string) {},
error: func(e error) {},
toSIG: func() bool { return true },
}
stateBag := createTestStateBagStepPublishToSharedImageGallery()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func createTestStateBagStepPublishToSharedImageGallery() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmManagedImageSigPublishResourceGroup, "Unit Test: ManagedImageSigPublishResourceGroup")
stateBag.Put(constants.ArmManagedImageSharedGalleryName, "Unit Test: ManagedImageSharedGalleryName")
stateBag.Put(constants.ArmManagedImageSharedGalleryImageName, "Unit Test: ManagedImageSharedGalleryImageName")
stateBag.Put(constants.ArmManagedImageSharedGalleryImageVersion, "Unit Test: ManagedImageSharedGalleryImageVersion")
stateBag.Put(constants.ArmLocation, "Unit Test: Location")
value := "Unit Test: Tags"
tags := map[string]*string{
"tag01": &value,
}
stateBag.Put(constants.ArmTags, tags)
stateBag.Put(constants.ArmManagedImageSharedGalleryReplicationRegions, []string{"ManagedImageSharedGalleryReplicationRegionA", "ManagedImageSharedGalleryReplicationRegionB"})
stateBag.Put(constants.ArmManagedImageResourceGroupName, "Unit Test: ManagedImageResourceGroupName")
stateBag.Put(constants.ArmManagedImageName, "Unit Test: ManagedImageName")
stateBag.Put(constants.ArmManagedImageSubscription, "Unit Test: ManagedImageSubscription")
return stateBag
}
func createTestStateBagStepPublishToSharedImageGalleryForVhd() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmLocation, "Unit Test: Location")
value := "Unit Test: Tags"
tags := map[string]*string{
"tag01": &value,
}
stateBag.Put(constants.ArmTags, tags)
return stateBag
}

View File

@ -0,0 +1,37 @@
package arm
import (
"context"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
type StepSetCertificate struct {
config *Config
say func(message string)
error func(e error)
}
func NewStepSetCertificate(config *Config, ui packersdk.Ui) *StepSetCertificate {
var step = &StepSetCertificate{
config: config,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
}
return step
}
func (s *StepSetCertificate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Setting the certificate's URL ...")
var winRMCertificateUrl = state.Get(constants.ArmCertificateUrl).(string)
s.config.tmpWinRMCertificateUrl = winRMCertificateUrl
return multistep.ActionContinue
}
func (*StepSetCertificate) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,54 @@
package arm
import (
"context"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStepSetCertificateShouldPassIfGetPasses(t *testing.T) {
var testSubject = &StepSetCertificate{
config: new(Config),
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepSetCertificate()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepSetCertificateShouldTakeStepArgumentsFromStateBag(t *testing.T) {
config := new(Config)
var testSubject = &StepSetCertificate{
config: config,
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepSetCertificate()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if config.tmpWinRMCertificateUrl != stateBag.Get(constants.ArmCertificateUrl) {
t.Fatalf("Expected config.tmpWinRMCertificateUrl to be %s, but got %s'", stateBag.Get(constants.ArmCertificateUrl), config.tmpWinRMCertificateUrl)
}
}
func createTestStateBagStepSetCertificate() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmCertificateUrl, "Unit Test: Certificate URL")
return stateBag
}

View File

@ -0,0 +1,108 @@
package arm
import (
"context"
"fmt"
"strconv"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
"github.com/Azure/go-autorest/autorest/to"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
type StepSnapshotDataDisks struct {
client *AzureClient
create func(ctx context.Context, resourceGroupName string, srcUriVhd string, location string, tags map[string]*string, dstSnapshotName string) error
say func(message string)
error func(e error)
enable func() bool
}
func NewStepSnapshotDataDisks(client *AzureClient, ui packersdk.Ui, config *Config) *StepSnapshotDataDisks {
var step = &StepSnapshotDataDisks{
client: client,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
enable: func() bool { return config.isManagedImage() && config.ManagedImageDataDiskSnapshotPrefix != "" },
}
step.create = step.createDataDiskSnapshot
return step
}
func (s *StepSnapshotDataDisks) createDataDiskSnapshot(ctx context.Context, resourceGroupName string, srcUriVhd string, location string, tags map[string]*string, dstSnapshotName string) error {
srcVhdToSnapshot := compute.Snapshot{
DiskProperties: &compute.DiskProperties{
CreationData: &compute.CreationData{
CreateOption: compute.Copy,
SourceResourceID: to.StringPtr(srcUriVhd),
},
},
Location: to.StringPtr(location),
Tags: tags,
}
f, err := s.client.SnapshotsClient.CreateOrUpdate(ctx, resourceGroupName, dstSnapshotName, srcVhdToSnapshot)
if err != nil {
s.say(s.client.LastError.Error())
return err
}
err = f.WaitForCompletionRef(ctx, s.client.SnapshotsClient.Client)
if err != nil {
s.say(s.client.LastError.Error())
return err
}
createdSnapshot, err := f.Result(s.client.SnapshotsClient)
if err != nil {
s.say(s.client.LastError.Error())
return err
}
s.say(fmt.Sprintf(" -> Snapshot ID : '%s'", *(createdSnapshot.ID)))
return nil
}
func (s *StepSnapshotDataDisks) Run(ctx context.Context, stateBag multistep.StateBag) multistep.StepAction {
if !s.enable() {
return multistep.ActionContinue
}
var resourceGroupName = stateBag.Get(constants.ArmManagedImageResourceGroupName).(string)
var location = stateBag.Get(constants.ArmLocation).(string)
var tags = stateBag.Get(constants.ArmTags).(map[string]*string)
var additionalDisks = stateBag.Get(constants.ArmAdditionalDiskVhds).([]string)
var dstSnapshotPrefix = stateBag.Get(constants.ArmManagedImageDataDiskSnapshotPrefix).(string)
if len(additionalDisks) == 1 {
s.say(fmt.Sprintf("Snapshotting data disk ..."))
} else {
s.say(fmt.Sprintf("Snapshotting data disks ..."))
}
for i, disk := range additionalDisks {
s.say(fmt.Sprintf(" -> Data Disk : '%s'", disk))
dstSnapshotName := dstSnapshotPrefix + strconv.Itoa(i)
err := s.create(ctx, resourceGroupName, disk, location, tags, dstSnapshotName)
if err != nil {
stateBag.Put(constants.Error, err)
s.error(err)
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (*StepSnapshotDataDisks) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,88 @@
package arm
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStepSnapshotDataDisksShouldFailIfSnapshotFails(t *testing.T) {
var testSubject = &StepSnapshotDataDisks{
create: func(context.Context, string, string, string, map[string]*string, string) error {
return fmt.Errorf("!! Unit Test FAIL !!")
},
say: func(message string) {},
error: func(e error) {},
enable: func() bool { return true },
}
stateBag := createTestStateBagStepSnapshotDataDisks()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepSnapshotDataDisksShouldNotExecute(t *testing.T) {
var testSubject = &StepSnapshotDataDisks{
create: func(context.Context, string, string, string, map[string]*string, string) error {
return fmt.Errorf("!! Unit Test FAIL !!")
},
say: func(message string) {},
error: func(e error) {},
enable: func() bool { return false },
}
var result = testSubject.Run(context.Background(), nil)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
}
func TestStepSnapshotDataDisksShouldPassIfSnapshotPasses(t *testing.T) {
var testSubject = &StepSnapshotDataDisks{
create: func(context.Context, string, string, string, map[string]*string, string) error {
return nil
},
say: func(message string) {},
error: func(e error) {},
enable: func() bool { return true },
}
stateBag := createTestStateBagStepSnapshotDataDisks()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func createTestStateBagStepSnapshotDataDisks() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmManagedImageResourceGroupName, "Unit Test: ResourceGroupName")
stateBag.Put(constants.ArmLocation, "Unit Test: Location")
value := "Unit Test: Tags"
tags := map[string]*string{
"tag01": &value,
}
stateBag.Put(constants.ArmTags, tags)
stateBag.Put(constants.ArmAdditionalDiskVhds, []string{"subscriptions/123-456-789/resourceGroups/existingresourcegroup/providers/Microsoft.Compute/disks/osdisk"})
stateBag.Put(constants.ArmManagedImageDataDiskSnapshotPrefix, "Unit Test: ManagedImageDataDiskSnapshotPrefix")
return stateBag
}

View File

@ -0,0 +1,99 @@
package arm
import (
"context"
"fmt"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
"github.com/Azure/go-autorest/autorest/to"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
type StepSnapshotOSDisk struct {
client *AzureClient
create func(ctx context.Context, resourceGroupName string, srcUriVhd string, location string, tags map[string]*string, dstSnapshotName string) error
say func(message string)
error func(e error)
enable func() bool
}
func NewStepSnapshotOSDisk(client *AzureClient, ui packersdk.Ui, config *Config) *StepSnapshotOSDisk {
var step = &StepSnapshotOSDisk{
client: client,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
enable: func() bool { return config.isManagedImage() && config.ManagedImageOSDiskSnapshotName != "" },
}
step.create = step.createSnapshot
return step
}
func (s *StepSnapshotOSDisk) createSnapshot(ctx context.Context, resourceGroupName string, srcUriVhd string, location string, tags map[string]*string, dstSnapshotName string) error {
srcVhdToSnapshot := compute.Snapshot{
DiskProperties: &compute.DiskProperties{
CreationData: &compute.CreationData{
CreateOption: compute.Copy,
SourceResourceID: to.StringPtr(srcUriVhd),
},
},
Location: to.StringPtr(location),
Tags: tags,
}
f, err := s.client.SnapshotsClient.CreateOrUpdate(ctx, resourceGroupName, dstSnapshotName, srcVhdToSnapshot)
if err != nil {
s.say(s.client.LastError.Error())
return err
}
err = f.WaitForCompletionRef(ctx, s.client.SnapshotsClient.Client)
if err != nil {
s.say(s.client.LastError.Error())
return err
}
createdSnapshot, err := f.Result(s.client.SnapshotsClient)
if err != nil {
s.say(s.client.LastError.Error())
return err
}
s.say(fmt.Sprintf(" -> Snapshot ID : '%s'", *(createdSnapshot.ID)))
return nil
}
func (s *StepSnapshotOSDisk) Run(ctx context.Context, stateBag multistep.StateBag) multistep.StepAction {
if !s.enable() {
return multistep.ActionContinue
}
s.say("Snapshotting OS disk ...")
var resourceGroupName = stateBag.Get(constants.ArmManagedImageResourceGroupName).(string)
var location = stateBag.Get(constants.ArmLocation).(string)
var tags = stateBag.Get(constants.ArmTags).(map[string]*string)
var srcUriVhd = stateBag.Get(constants.ArmOSDiskVhd).(string)
var dstSnapshotName = stateBag.Get(constants.ArmManagedImageOSDiskSnapshotName).(string)
s.say(fmt.Sprintf(" -> OS Disk : '%s'", srcUriVhd))
err := s.create(ctx, resourceGroupName, srcUriVhd, location, tags, dstSnapshotName)
if err != nil {
stateBag.Put(constants.Error, err)
s.error(err)
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (*StepSnapshotOSDisk) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,89 @@
package arm
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStepSnapshotOSDiskShouldFailIfSnapshotFails(t *testing.T) {
var testSubject = &StepSnapshotOSDisk{
create: func(context.Context, string, string, string, map[string]*string, string) error {
return fmt.Errorf("!! Unit Test FAIL !!")
},
say: func(message string) {},
error: func(e error) {},
enable: func() bool { return true },
}
stateBag := createTestStateBagStepSnapshotOSDisk()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepSnapshotOSDiskShouldNotExecute(t *testing.T) {
var testSubject = &StepSnapshotOSDisk{
create: func(context.Context, string, string, string, map[string]*string, string) error {
return fmt.Errorf("!! Unit Test FAIL !!")
},
say: func(message string) {},
error: func(e error) {},
enable: func() bool { return false },
}
var result = testSubject.Run(context.Background(), nil)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
}
func TestStepSnapshotOSDiskShouldPassIfSnapshotPasses(t *testing.T) {
var testSubject = &StepSnapshotOSDisk{
create: func(context.Context, string, string, string, map[string]*string, string) error {
return nil
},
say: func(message string) {},
error: func(e error) {},
enable: func() bool { return true },
}
stateBag := createTestStateBagStepSnapshotOSDisk()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func createTestStateBagStepSnapshotOSDisk() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmManagedImageResourceGroupName, "Unit Test: ResourceGroupName")
stateBag.Put(constants.ArmLocation, "Unit Test: Location")
value := "Unit Test: Tags"
tags := map[string]*string{
"tag01": &value,
}
stateBag.Put(constants.ArmTags, tags)
stateBag.Put(constants.ArmOSDiskVhd, "subscriptions/123-456-789/resourceGroups/existingresourcegroup/providers/Microsoft.Compute/disks/osdisk")
stateBag.Put(constants.ArmManagedImageOSDiskSnapshotName, "Unit Test: ManagedImageOSDiskSnapshotName")
return stateBag
}

View File

@ -0,0 +1,40 @@
package arm
import (
"fmt"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestProcessStepResultShouldContinueForNonErrors(t *testing.T) {
stateBag := new(multistep.BasicStateBag)
code := processStepResult(nil, func(error) { t.Fatal("Should not be called!") }, stateBag)
if _, ok := stateBag.GetOk(constants.Error); ok {
t.Errorf("Error was nil, but was still in the state bag.")
}
if code != multistep.ActionContinue {
t.Errorf("Expected ActionContinue(%d), but got=%d", multistep.ActionContinue, code)
}
}
func TestProcessStepResultShouldHaltOnError(t *testing.T) {
stateBag := new(multistep.BasicStateBag)
isSaidError := false
code := processStepResult(fmt.Errorf("boom"), func(error) { isSaidError = true }, stateBag)
if _, ok := stateBag.GetOk(constants.Error); !ok {
t.Errorf("Error was non nil, but was not in the state bag.")
}
if !isSaidError {
t.Errorf("Expected error to be said, but it was not.")
}
if code != multistep.ActionHalt {
t.Errorf("Expected ActionHalt(%d), but got=%d", multistep.ActionHalt, code)
}
}

View File

@ -0,0 +1,61 @@
package arm
import (
"context"
"fmt"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
type StepValidateTemplate struct {
client *AzureClient
validate func(ctx context.Context, resourceGroupName string, deploymentName string) error
say func(message string)
error func(e error)
config *Config
factory templateFactoryFunc
}
func NewStepValidateTemplate(client *AzureClient, ui packersdk.Ui, config *Config, factory templateFactoryFunc) *StepValidateTemplate {
var step = &StepValidateTemplate{
client: client,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
config: config,
factory: factory,
}
step.validate = step.validateTemplate
return step
}
func (s *StepValidateTemplate) validateTemplate(ctx context.Context, resourceGroupName string, deploymentName string) error {
deployment, err := s.factory(s.config)
if err != nil {
return err
}
_, err = s.client.DeploymentsClient.Validate(ctx, resourceGroupName, deploymentName, *deployment)
if err != nil {
s.say(s.client.LastError.Error())
}
return err
}
func (s *StepValidateTemplate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Validating deployment template ...")
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
var deploymentName = state.Get(constants.ArmDeploymentName).(string)
s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName))
s.say(fmt.Sprintf(" -> DeploymentName : '%s'", deploymentName))
err := s.validate(ctx, resourceGroupName, deploymentName)
return processStepResult(err, s.error, state)
}
func (*StepValidateTemplate) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,91 @@
package arm
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/constants"
)
func TestStepValidateTemplateShouldFailIfValidateFails(t *testing.T) {
var testSubject = &StepValidateTemplate{
validate: func(context.Context, string, string) error { return fmt.Errorf("!! Unit Test FAIL !!") },
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepValidateTemplate()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepValidateTemplateShouldPassIfValidatePasses(t *testing.T) {
var testSubject = &StepValidateTemplate{
validate: func(context.Context, string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepValidateTemplate()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepValidateTemplateShouldTakeStepArgumentsFromStateBag(t *testing.T) {
var actualResourceGroupName string
var actualDeploymentName string
var testSubject = &StepValidateTemplate{
validate: func(ctx context.Context, resourceGroupName string, deploymentName string) error {
actualResourceGroupName = resourceGroupName
actualDeploymentName = deploymentName
return nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepValidateTemplate()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
var expectedDeploymentName = stateBag.Get(constants.ArmDeploymentName).(string)
var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string)
if actualDeploymentName != expectedDeploymentName {
t.Fatal("Expected the step to source 'constants.ArmDeploymentName' from the state bag, but it did not.")
}
if actualResourceGroupName != expectedResourceGroupName {
t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.")
}
}
func createTestStateBagStepValidateTemplate() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmDeploymentName, "Unit Test: DeploymentName")
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
return stateBag
}

View File

@ -0,0 +1,213 @@
package arm
import (
"encoding/json"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-02-01/resources"
"fmt"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/builder/azure/common/template"
)
type templateFactoryFunc func(*Config) (*resources.Deployment, error)
func GetKeyVaultDeployment(config *Config) (*resources.Deployment, error) {
params := &template.TemplateParameters{
KeyVaultName: &template.TemplateParameter{Value: config.tmpKeyVaultName},
KeyVaultSKU: &template.TemplateParameter{Value: config.BuildKeyVaultSKU},
KeyVaultSecretValue: &template.TemplateParameter{Value: config.winrmCertificate},
ObjectId: &template.TemplateParameter{Value: config.ClientConfig.ObjectID},
TenantId: &template.TemplateParameter{Value: config.ClientConfig.TenantID},
}
builder, _ := template.NewTemplateBuilder(template.KeyVault)
builder.SetTags(&config.AzureTags)
doc, _ := builder.ToJSON()
return createDeploymentParameters(*doc, params)
}
func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error) {
params := &template.TemplateParameters{
AdminUsername: &template.TemplateParameter{Value: config.UserName},
AdminPassword: &template.TemplateParameter{Value: config.Password},
DnsNameForPublicIP: &template.TemplateParameter{Value: config.tmpComputeName},
NicName: &template.TemplateParameter{Value: config.tmpNicName},
OSDiskName: &template.TemplateParameter{Value: config.tmpOSDiskName},
DataDiskName: &template.TemplateParameter{Value: config.tmpDataDiskName},
PublicIPAddressName: &template.TemplateParameter{Value: config.tmpPublicIPAddressName},
SubnetName: &template.TemplateParameter{Value: config.tmpSubnetName},
StorageAccountBlobEndpoint: &template.TemplateParameter{Value: config.storageAccountBlobEndpoint},
VirtualNetworkName: &template.TemplateParameter{Value: config.tmpVirtualNetworkName},
NsgName: &template.TemplateParameter{Value: config.tmpNsgName},
VMSize: &template.TemplateParameter{Value: config.VMSize},
VMName: &template.TemplateParameter{Value: config.tmpComputeName},
}
builder, err := template.NewTemplateBuilder(template.BasicTemplate)
if err != nil {
return nil, err
}
osType := compute.Linux
switch config.OSType {
case constants.Target_Linux:
err = builder.BuildLinux(config.sshAuthorizedKey, config.Comm.SSHPassword == "") // if ssh password is not explicitly specified, disable password auth
if err != nil {
return nil, err
}
case constants.Target_Windows:
osType = compute.Windows
err = builder.BuildWindows(config.tmpKeyVaultName, config.tmpWinRMCertificateUrl)
if err != nil {
return nil, err
}
}
if len(config.UserAssignedManagedIdentities) != 0 {
if err := builder.SetIdentity(config.UserAssignedManagedIdentities); err != nil {
return nil, err
}
}
if config.ImageUrl != "" {
err = builder.SetImageUrl(config.ImageUrl, osType, config.diskCachingType)
if err != nil {
return nil, err
}
} else if config.CustomManagedImageName != "" {
err = builder.SetManagedDiskUrl(config.customManagedImageID, config.managedImageStorageAccountType, config.diskCachingType)
if err != nil {
return nil, err
}
} else if config.ManagedImageName != "" && config.ImagePublisher != "" {
imageID := fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Compute/locations/%s/publishers/%s/ArtifactTypes/vmimage/offers/%s/skus/%s/versions/%s",
config.ClientConfig.SubscriptionID,
config.Location,
config.ImagePublisher,
config.ImageOffer,
config.ImageSku,
config.ImageVersion)
builder.SetManagedMarketplaceImage(config.Location, config.ImagePublisher, config.ImageOffer, config.ImageSku, config.ImageVersion, imageID, config.managedImageStorageAccountType, config.diskCachingType)
} 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)
}
err = builder.SetSharedGalleryImage(config.Location, imageID, config.diskCachingType)
if err != nil {
return nil, err
}
} else {
err = builder.SetMarketPlaceImage(config.ImagePublisher, config.ImageOffer, config.ImageSku, config.ImageVersion, config.diskCachingType)
if err != nil {
return nil, err
}
}
if config.OSDiskSizeGB > 0 {
err = builder.SetOSDiskSizeGB(config.OSDiskSizeGB)
if err != nil {
return nil, err
}
}
if len(config.AdditionalDiskSize) > 0 {
isManaged := config.CustomManagedImageName != "" || (config.ManagedImageName != "" && config.ImagePublisher != "") || config.SharedGallery.Subscription != ""
err = builder.SetAdditionalDisks(config.AdditionalDiskSize, config.tmpDataDiskName, isManaged, config.diskCachingType)
if err != nil {
return nil, err
}
}
if config.customData != "" {
err = builder.SetCustomData(config.customData)
if err != nil {
return nil, err
}
}
if config.PlanInfo.PlanName != "" {
err = builder.SetPlanInfo(config.PlanInfo.PlanName, config.PlanInfo.PlanProduct, config.PlanInfo.PlanPublisher, config.PlanInfo.PlanPromotionCode)
if err != nil {
return nil, err
}
}
if config.VirtualNetworkName != "" && DefaultPrivateVirtualNetworkWithPublicIp != config.PrivateVirtualNetworkWithPublicIp {
err = builder.SetPrivateVirtualNetworkWithPublicIp(
config.VirtualNetworkResourceGroupName,
config.VirtualNetworkName,
config.VirtualNetworkSubnetName)
if err != nil {
return nil, err
}
} else if config.VirtualNetworkName != "" {
err = builder.SetVirtualNetwork(
config.VirtualNetworkResourceGroupName,
config.VirtualNetworkName,
config.VirtualNetworkSubnetName)
if err != nil {
return nil, err
}
}
if config.AllowedInboundIpAddresses != nil && len(config.AllowedInboundIpAddresses) >= 1 && config.Comm.Port() != 0 {
err = builder.SetNetworkSecurityGroup(config.AllowedInboundIpAddresses, config.Comm.Port())
if err != nil {
return nil, err
}
}
if config.BootDiagSTGAccount != "" {
err = builder.SetBootDiagnostics(config.BootDiagSTGAccount)
if err != nil {
return nil, err
}
}
err = builder.SetTags(&config.AzureTags)
if err != nil {
return nil, err
}
doc, _ := builder.ToJSON()
return createDeploymentParameters(*doc, params)
}
func createDeploymentParameters(doc string, parameters *template.TemplateParameters) (*resources.Deployment, error) {
var template map[string]interface{}
err := json.Unmarshal(([]byte)(doc), &template)
if err != nil {
return nil, err
}
bs, err := json.Marshal(*parameters)
if err != nil {
return nil, err
}
var templateParameters map[string]interface{}
err = json.Unmarshal(bs, &templateParameters)
if err != nil {
return nil, err
}
return &resources.Deployment{
Properties: &resources.DeploymentProperties{
Mode: resources.Incremental,
Template: &template,
Parameters: &templateParameters,
},
}, nil
}

View File

@ -0,0 +1,76 @@
{
"$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json",
"contentVersion": "1.0.0.0",
"parameters": {
"keyVaultName": {
"type": "string"
},
"keyVaultSKU": {
"type": "string"
},
"keyVaultSecretValue": {
"type": "securestring"
},
"objectId": {
"type": "string"
},
"tenantId": {
"type": "string"
}
},
"resources": [
{
"apiVersion": "[variables('apiVersion')]",
"location": "[variables('location')]",
"name": "[parameters('keyVaultName')]",
"properties": {
"accessPolicies": [
{
"objectId": "[parameters('objectId')]",
"permissions": {
"keys": [
"all"
],
"secrets": [
"all"
]
},
"tenantId": "[parameters('tenantId')]"
}
],
"enableSoftDelete": "true",
"enabledForDeployment": "true",
"enabledForTemplateDeployment": "true",
"sku": {
"family": "A",
"name": "[parameters('keyVaultSKU')]"
},
"tenantId": "[parameters('tenantId')]"
},
"resources": [
{
"apiVersion": "[variables('apiVersion')]",
"dependsOn": [
"[concat('Microsoft.KeyVault/vaults/', parameters('keyVaultName'))]"
],
"name": "[variables('keyVaultSecretName')]",
"properties": {
"value": "[parameters('keyVaultSecretValue')]"
},
"type": "secrets"
}
],
"tags": {
"tag01": "value01",
"tag02": "value02",
"tag03": "value03"
},
"type": "Microsoft.KeyVault/vaults"
}
],
"variables": {
"apiVersion": "2015-06-01",
"keyVaultSecretName": "packerKeyVaultSecret",
"location": "[resourceGroup().location]"
}
}

View File

@ -0,0 +1,211 @@
{
"$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')]"
},
"tags": {
"PlanInfo": "planName00",
"PlanProduct": "planProduct00",
"PlanPromotionCode": "",
"PlanPublisher": "planPublisher00"
},
"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')]"
}
}
]
},
"tags": {
"PlanInfo": "planName00",
"PlanProduct": "planProduct00",
"PlanPromotionCode": "",
"PlanPublisher": "planPublisher00"
},
"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')]"
}
}
}
]
},
"tags": {
"PlanInfo": "planName00",
"PlanProduct": "planProduct00",
"PlanPromotionCode": "",
"PlanPublisher": "planPublisher00"
},
"type": "Microsoft.Network/networkInterfaces"
},
{
"apiVersion": "[variables('apiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', parameters('nicName'))]"
],
"location": "[variables('location')]",
"name": "[parameters('vmName')]",
"plan": {
"name": "planName00",
"product": "planProduct00",
"publisher": "planPublisher00"
},
"properties": {
"diagnosticsProfile": {
"bootDiagnostics": {
"enabled": false
}
},
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('nicName'))]"
}
]
},
"osProfile": {
"adminUsername": "[parameters('adminUsername')]",
"computerName": "[parameters('vmName')]",
"linuxConfiguration": {
"disablePasswordAuthentication": true,
"ssh": {
"publicKeys": [
{
"keyData": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"imageReference": {
"offer": "ignored00",
"publisher": "ignored00",
"sku": "ignored00",
"version": "latest"
},
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"name": "[parameters('osDiskName')]",
"vhd": {
"uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]"
}
}
}
},
"tags": {
"PlanInfo": "planName00",
"PlanProduct": "planProduct00",
"PlanPromotionCode": "",
"PlanPublisher": "planPublisher00"
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"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'))]"
}
}

View File

@ -0,0 +1,216 @@
{
"$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')]"
},
"tags": {
"PlanInfo": "planName00",
"PlanProduct": "planProduct00",
"PlanPromotionCode": "planPromotionCode00",
"PlanPublisher": "planPublisher00",
"dept": "engineering"
},
"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')]"
}
}
]
},
"tags": {
"PlanInfo": "planName00",
"PlanProduct": "planProduct00",
"PlanPromotionCode": "planPromotionCode00",
"PlanPublisher": "planPublisher00",
"dept": "engineering"
},
"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')]"
}
}
}
]
},
"tags": {
"PlanInfo": "planName00",
"PlanProduct": "planProduct00",
"PlanPromotionCode": "planPromotionCode00",
"PlanPublisher": "planPublisher00",
"dept": "engineering"
},
"type": "Microsoft.Network/networkInterfaces"
},
{
"apiVersion": "[variables('apiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', parameters('nicName'))]"
],
"location": "[variables('location')]",
"name": "[parameters('vmName')]",
"plan": {
"name": "planName00",
"product": "planProduct00",
"promotionCode": "planPromotionCode00",
"publisher": "planPublisher00"
},
"properties": {
"diagnosticsProfile": {
"bootDiagnostics": {
"enabled": false
}
},
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('nicName'))]"
}
]
},
"osProfile": {
"adminUsername": "[parameters('adminUsername')]",
"computerName": "[parameters('vmName')]",
"linuxConfiguration": {
"disablePasswordAuthentication": true,
"ssh": {
"publicKeys": [
{
"keyData": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"imageReference": {
"offer": "ignored00",
"publisher": "ignored00",
"sku": "ignored00",
"version": "latest"
},
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"name": "[parameters('osDiskName')]",
"vhd": {
"uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]"
}
}
}
},
"tags": {
"PlanInfo": "planName00",
"PlanProduct": "planProduct00",
"PlanPromotionCode": "planPromotionCode00",
"PlanPublisher": "planPublisher00",
"dept": "engineering"
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"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'))]"
}
}

View File

@ -0,0 +1,182 @@
{
"$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'))]"
],
"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": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"imageReference": {
"offer": "ImageOffer",
"publisher": "ImagePublisher",
"sku": "ImageSku",
"version": "ImageVersion"
},
"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": "2017-03-30",
"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'))]"
}
}

View File

@ -0,0 +1,180 @@
{
"$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'))]"
],
"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": {
"adminUsername": "[parameters('adminUsername')]",
"computerName": "[parameters('vmName')]",
"linuxConfiguration": {
"disablePasswordAuthentication": true,
"ssh": {
"publicKeys": [
{
"keyData": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"image": {
"uri": "https://localhost/custom.vhd"
},
"name": "[parameters('osDiskName')]",
"osType": "Linux",
"vhd": {
"uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]"
}
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"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'))]"
}
}

View File

@ -0,0 +1,141 @@
{
"$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('networkInterfacesApiVersion')]",
"dependsOn": [],
"location": "[variables('location')]",
"name": "[parameters('nicName')]",
"properties": {
"ipConfigurations": [
{
"name": "ipconfig",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"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": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"image": {
"uri": "https://localhost/custom.vhd"
},
"name": "[parameters('osDiskName')]",
"osType": "Linux",
"vhd": {
"uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]"
}
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"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": "virtualNetworkSubnetName",
"subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]",
"virtualNetworkName": "virtualNetworkName",
"virtualNetworkResourceGroup": "virtualNetworkResourceGroupName",
"virtualNetworksApiVersion": "2017-04-01",
"vmStorageAccountContainerName": "images",
"vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
}
}

View File

@ -0,0 +1,200 @@
{
"$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')]"
},
"tags": {
"tag01": "value01",
"tag02": "value02",
"tag03": "value03"
},
"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')]"
}
}
]
},
"tags": {
"tag01": "value01",
"tag02": "value02",
"tag03": "value03"
},
"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')]"
}
}
}
]
},
"tags": {
"tag01": "value01",
"tag02": "value02",
"tag03": "value03"
},
"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": {
"adminUsername": "[parameters('adminUsername')]",
"computerName": "[parameters('vmName')]",
"linuxConfiguration": {
"disablePasswordAuthentication": true,
"ssh": {
"publicKeys": [
{
"keyData": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"image": {
"uri": "https://localhost/custom.vhd"
},
"name": "[parameters('osDiskName')]",
"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": "2017-03-30",
"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'))]"
}
}

View File

@ -0,0 +1,181 @@
{
"$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'))]"
],
"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')]",
"customData": "I2Nsb3VkLWNvbmZpZwpncm93cGFydDoKICBtb2RlOiBvZmYK",
"linuxConfiguration": {
"ssh": {
"publicKeys": [
{
"keyData": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"image": {
"uri": "https://localhost/custom.vhd"
},
"name": "[parameters('osDiskName')]",
"osType": "Linux",
"vhd": {
"uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]"
}
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"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'))]"
}
}

View File

@ -0,0 +1,180 @@
{
"$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'))]"
],
"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": {
"adminUsername": "[parameters('adminUsername')]",
"computerName": "[parameters('vmName')]",
"linuxConfiguration": {
"disablePasswordAuthentication": true,
"ssh": {
"publicKeys": [
{
"keyData": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"imageReference": {
"id": ""
},
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "Standard_LRS"
},
"name": "[parameters('osDiskName')]",
"osType": "Linux"
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"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'))]"
}
}

View File

@ -0,0 +1,183 @@
{
"$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'))]"
],
"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": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"imageReference": {
"offer": "--image-offer--",
"publisher": "--image-publisher--",
"sku": "--image-sku--",
"version": "--version--"
},
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "Standard_LRS"
},
"name": "[parameters('osDiskName')]",
"osType": "Linux"
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"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'))]"
}
}

View File

@ -0,0 +1,161 @@
{
"$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('networkInterfacesApiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/publicIPAddresses/', parameters('publicIPAddressName'))]"
],
"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": {
"adminUsername": "[parameters('adminUsername')]",
"computerName": "[parameters('vmName')]",
"linuxConfiguration": {
"disablePasswordAuthentication": true,
"ssh": {
"publicKeys": [
{
"keyData": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"imageReference": {
"offer": "--image-offer--",
"publisher": "--image-publisher--",
"sku": "--image-sku--",
"version": "--version--"
},
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "Standard_LRS"
},
"name": "[parameters('osDiskName')]",
"osType": "Linux"
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"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": "--virtual_network_subnet_name--",
"subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]",
"virtualNetworkName": "--virtual_network_name--",
"virtualNetworkResourceGroup": "--virtual_network_resource_group_name--",
"virtualNetworksApiVersion": "2017-04-01",
"vmStorageAccountContainerName": "images",
"vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
}
}

View File

@ -0,0 +1,194 @@
{
"$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'))]"
],
"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": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"dataDisks": [
{
"caching": "ReadWrite",
"createOption": "Empty",
"diskSizeGB": 32,
"lun": 0,
"name": "[concat(parameters('dataDiskName'),'-1')]",
"vhd": {
"uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/',parameters('dataDiskName'),'-1','.vhd')]"
}
}
],
"imageReference": {
"offer": "--image-offer--",
"publisher": "--image-publisher--",
"sku": "--image-sku--",
"version": "--version--"
},
"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": "2017-03-30",
"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'))]"
}
}

View File

@ -0,0 +1,195 @@
{
"$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'))]"
],
"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": {
"adminUsername": "[parameters('adminUsername')]",
"computerName": "[parameters('vmName')]",
"linuxConfiguration": {
"disablePasswordAuthentication": true,
"ssh": {
"publicKeys": [
{
"keyData": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"dataDisks": [
{
"caching": "ReadWrite",
"createOption": "Empty",
"diskSizeGB": 32,
"lun": 0,
"managedDisk": {
"storageAccountType": "Standard_LRS"
},
"name": "[concat(parameters('dataDiskName'),'-1')]"
}
],
"imageReference": {
"offer": "--image-offer--",
"publisher": "--image-publisher--",
"sku": "--image-sku--",
"version": "--version--"
},
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "Standard_LRS"
},
"name": "[parameters('osDiskName')]",
"osType": "Linux"
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"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'))]"
}
}

View File

@ -0,0 +1,230 @@
{
"$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('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')]",
"secrets": [
{
"sourceVault": {
"id": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults', '--keyvault-name--')]"
},
"vaultCertificates": [
{
"certificateStore": "My",
"certificateUrl": ""
}
]
}
],
"windowsConfiguration": {
"provisionVMAgent": true,
"winRM": {
"listeners": [
{
"certificateUrl": "",
"protocol": "https"
}
]
}
}
},
"storageProfile": {
"imageReference": {
"offer": "--image-offer--",
"publisher": "--image-publisher--",
"sku": "--image-sku--",
"version": "--version--"
},
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "Standard_LRS"
},
"name": "[parameters('osDiskName')]",
"osType": "Windows"
}
}
},
"type": "Microsoft.Compute/virtualMachines"
},
{
"apiVersion": "[variables('networkSecurityGroupsApiVersion')]",
"location": "[variables('location')]",
"name": "[parameters('nsgName')]",
"properties": {
"securityRules": [
{
"name": "AllowIPsToSshWinRMInbound",
"properties": {
"access": "Allow",
"description": "Allow inbound traffic from specified IP addresses",
"destinationAddressPrefix": "VirtualNetwork",
"destinationPortRange": "5985",
"direction": "Inbound",
"priority": 100,
"protocol": "Tcp",
"sourceAddressPrefixes": [
"127.0.0.1",
"192.168.100.0/24"
],
"sourcePortRange": "*"
}
}
]
},
"type": "Microsoft.Network/networkSecurityGroups"
},
{
"apiVersion": "[variables('virtualNetworksApiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/networkSecurityGroups/', parameters('nsgName'))]"
],
"location": "[variables('location')]",
"name": "[variables('virtualNetworkName')]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"[variables('addressPrefix')]"
]
},
"subnets": [
{
"name": "[variables('subnetName')]",
"properties": {
"addressPrefix": "[variables('subnetAddressPrefix')]",
"networkSecurityGroup": {
"id": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('nsgName'))]"
}
}
}
]
},
"type": "Microsoft.Network/virtualNetworks"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"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'))]"
}
}

View File

@ -0,0 +1,181 @@
{
"$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'))]"
],
"location": "[variables('location')]",
"name": "[parameters('vmName')]",
"properties": {
"diagnosticsProfile": {
"bootDiagnostics": {
"enabled": true,
"storageUri": "https://diagstgaccnt.blob.core.windows.net"
}
},
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('nicName'))]"
}
]
},
"osProfile": {
"adminUsername": "[parameters('adminUsername')]",
"computerName": "[parameters('vmName')]",
"linuxConfiguration": {
"disablePasswordAuthentication": true,
"ssh": {
"publicKeys": [
{
"keyData": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"imageReference": {
"id": ""
},
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "Standard_LRS"
},
"name": "[parameters('osDiskName')]",
"osType": "Linux"
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"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'))]"
}
}

View File

@ -0,0 +1,659 @@
package arm
import (
"encoding/base64"
"encoding/json"
"testing"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-02-01/resources"
approvaltests "github.com/approvals/go-approval-tests"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/builder/azure/common/template"
)
// Ensure the link values are not set, and the concrete values are set.
func TestVirtualMachineDeployment00(t *testing.T) {
var c Config
c.Prepare(getArmBuilderConfiguration(), getPackerConfiguration())
deployment, err := GetVirtualMachineDeployment(&c)
if err != nil {
t.Fatal(err)
}
if deployment.Properties.Mode != resources.Incremental {
t.Errorf("Expected deployment.Properties.Mode to be %s, but got %s", resources.Incremental, deployment.Properties.Mode)
}
if deployment.Properties.ParametersLink != nil {
t.Error("Expected the ParametersLink to be nil!")
}
if deployment.Properties.TemplateLink != nil {
t.Error("Expected the TemplateLink to be nil!")
}
if deployment.Properties.Parameters == nil {
t.Error("Expected the Parameters to not be nil!")
}
if deployment.Properties.Template == nil {
t.Error("Expected the Template to not be nil!")
}
}
// Ensure the Virtual Machine template is a valid JSON document.
func TestVirtualMachineDeployment01(t *testing.T) {
var c Config
c.Prepare(getArmBuilderConfiguration(), getPackerConfiguration())
deployment, err := GetVirtualMachineDeployment(&c)
if err != nil {
t.Fatal(err)
}
_, err = json.Marshal(deployment.Properties.Template)
if err != nil {
t.Fatal(err)
}
}
// Ensure the Virtual Machine template parameters are correct.
func TestVirtualMachineDeployment02(t *testing.T) {
var c Config
c.Prepare(getArmBuilderConfiguration(), getPackerConfiguration())
deployment, err := GetVirtualMachineDeployment(&c)
if err != nil {
t.Fatal(err)
}
bs, err := json.Marshal(deployment.Properties.Parameters)
if err != nil {
t.Fatal(err)
}
var params template.TemplateParameters
err = json.Unmarshal(bs, &params)
if err != nil {
t.Fatal(err)
}
if params.AdminUsername.Value != c.UserName {
t.Errorf("Expected template parameter 'AdminUsername' to be %s, but got %s.", params.AdminUsername.Value, c.UserName)
}
if params.AdminPassword.Value != c.tmpAdminPassword {
t.Errorf("Expected template parameter 'AdminPassword' to be %s, but got %s.", params.AdminPassword.Value, c.tmpAdminPassword)
}
if params.DnsNameForPublicIP.Value != c.tmpComputeName {
t.Errorf("Expected template parameter 'DnsNameForPublicIP' to be %s, but got %s.", params.DnsNameForPublicIP.Value, c.tmpComputeName)
}
if params.OSDiskName.Value != c.tmpOSDiskName {
t.Errorf("Expected template parameter 'OSDiskName' to be %s, but got %s.", params.OSDiskName.Value, c.tmpOSDiskName)
}
if params.StorageAccountBlobEndpoint.Value != c.storageAccountBlobEndpoint {
t.Errorf("Expected template parameter 'StorageAccountBlobEndpoint' to be %s, but got %s.", params.StorageAccountBlobEndpoint.Value, c.storageAccountBlobEndpoint)
}
if params.VMSize.Value != c.VMSize {
t.Errorf("Expected template parameter 'VMSize' to be %s, but got %s.", params.VMSize.Value, c.VMSize)
}
if params.VMName.Value != c.tmpComputeName {
t.Errorf("Expected template parameter 'VMName' to be %s, but got %s.", params.VMName.Value, c.tmpComputeName)
}
}
// Ensure the VM template is correct when using a market place image.
func TestVirtualMachineDeployment03(t *testing.T) {
m := getArmBuilderConfiguration()
m["image_publisher"] = "ImagePublisher"
m["image_offer"] = "ImageOffer"
m["image_sku"] = "ImageSku"
m["image_version"] = "ImageVersion"
var c Config
_, err := c.Prepare(m, getPackerConfiguration(), getPackerSSHPasswordCommunicatorConfiguration())
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 VM template is correct when using a custom image.
func TestVirtualMachineDeployment04(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",
}
var c Config
_, err := c.Prepare(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)
}
}
func TestVirtualMachineDeployment05(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",
"virtual_network_name": "virtualNetworkName",
"virtual_network_resource_group_name": "virtualNetworkResourceGroupName",
"virtual_network_subnet_name": "virtualNetworkSubnetName",
}
var c Config
_, err := c.Prepare(config, getPackerConfiguration(), getPackerSSHPasswordCommunicatorConfiguration())
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)
}
}
// 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",
},
}
var c Config
_, err := c.Prepare(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)
}
}
// Verify that custom data are properly inserted
func TestVirtualMachineDeployment07(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",
}
var c Config
_, err := c.Prepare(config, getPackerConfiguration(), getPackerSSHPasswordCommunicatorConfiguration())
if err != nil {
t.Fatal(err)
}
// The user specifies a configuration value for the setting custom_data_file.
// The config type will read that file, and base64 encode it. The encoded
// contents are then assigned to Config's customData property, which are directly
// injected into the template.
//
// I am not aware of an easy to mimic this situation in a test without having
// a file on disk, which I am loathe to do. The alternative is to inject base64
// encoded data myself, which is what I am doing here.
customData := `#cloud-config
growpart:
mode: off
`
base64CustomData := base64.StdEncoding.EncodeToString([]byte(customData))
c.customData = base64CustomData
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 VM template is correct when building from a custom managed image.
func TestVirtualMachineDeployment08(t *testing.T) {
config := map[string]interface{}{
"location": "ignore",
"subscription_id": "ignore",
"os_type": constants.Target_Linux,
"communicator": "none",
"custom_managed_image_resource_group_name": "CustomManagedImageResourceGroupName",
"custom_managed_image_name": "CustomManagedImageName",
"managed_image_name": "ManagedImageName",
"managed_image_resource_group_name": "ManagedImageResourceGroupName",
}
var c Config
_, err := c.Prepare(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 VM template is correct when building from a platform managed image.
func TestVirtualMachineDeployment09(t *testing.T) {
config := map[string]interface{}{
"location": "ignore",
"subscription_id": "ignore",
"os_type": constants.Target_Linux,
"communicator": "none",
"image_publisher": "--image-publisher--",
"image_offer": "--image-offer--",
"image_sku": "--image-sku--",
"image_version": "--version--",
"managed_image_name": "ManagedImageName",
"managed_image_resource_group_name": "ManagedImageResourceGroupName",
}
var c Config
_, err := c.Prepare(config, getPackerConfiguration(), getPackerSSHPasswordCommunicatorConfiguration())
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 VM template is correct when building with PublicIp and connect to Private Network
func TestVirtualMachineDeployment10(t *testing.T) {
config := map[string]interface{}{
"location": "ignore",
"subscription_id": "ignore",
"os_type": constants.Target_Linux,
"communicator": "none",
"image_publisher": "--image-publisher--",
"image_offer": "--image-offer--",
"image_sku": "--image-sku--",
"image_version": "--version--",
"virtual_network_resource_group_name": "--virtual_network_resource_group_name--",
"virtual_network_name": "--virtual_network_name--",
"virtual_network_subnet_name": "--virtual_network_subnet_name--",
"private_virtual_network_with_public_ip": true,
"managed_image_name": "ManagedImageName",
"managed_image_resource_group_name": "ManagedImageResourceGroupName",
}
var c Config
_, err := c.Prepare(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 VM template is correct when building with additional unmanaged disks
func TestVirtualMachineDeployment11(t *testing.T) {
config := map[string]interface{}{
"location": "ignore",
"subscription_id": "ignore",
"os_type": constants.Target_Linux,
"communicator": "none",
"image_publisher": "--image-publisher--",
"image_offer": "--image-offer--",
"image_sku": "--image-sku--",
"image_version": "--version--",
"disk_additional_size": []uint{32},
"resource_group_name": "packergroup",
"storage_account": "packerartifacts",
"capture_name_prefix": "packer",
"capture_container_name": "packerimages",
}
var c Config
_, err := c.Prepare(config, getPackerConfiguration(), getPackerSSHPasswordCommunicatorConfiguration())
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 VM template is correct when building with additional managed disks
func TestVirtualMachineDeployment12(t *testing.T) {
config := map[string]interface{}{
"location": "ignore",
"subscription_id": "ignore",
"os_type": constants.Target_Linux,
"communicator": "none",
"image_publisher": "--image-publisher--",
"image_offer": "--image-offer--",
"image_sku": "--image-sku--",
"image_version": "--version--",
"disk_additional_size": []uint{32},
"managed_image_name": "ManagedImageName",
"managed_image_resource_group_name": "ManagedImageResourceGroupName",
}
var c Config
_, err := c.Prepare(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 VM template is correct when building with list of allowed IP addresses
func TestVirtualMachineDeployment13(t *testing.T) {
config := map[string]interface{}{
"location": "ignore",
"subscription_id": "ignore",
"os_type": constants.Target_Windows,
"communicator": "winrm",
"winrm_username": "ignore",
"image_publisher": "--image-publisher--",
"image_offer": "--image-offer--",
"image_sku": "--image-sku--",
"image_version": "--version--",
"managed_image_name": "ManagedImageName",
"managed_image_resource_group_name": "ManagedImageResourceGroupName",
"allowed_inbound_ip_addresses": []string{"127.0.0.1", "192.168.100.0/24"},
}
var c Config
_, err := c.Prepare(config, getPackerConfiguration())
if err != nil {
t.Fatal(err)
}
c.tmpKeyVaultName = "--keyvault-name--"
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 VM template is correct when building with bootdiagnostics
func TestVirtualMachineDeployment14(t *testing.T) {
config := map[string]interface{}{
"location": "ignore",
"subscription_id": "ignore",
"os_type": constants.Target_Linux,
"communicator": "none",
"custom_managed_image_resource_group_name": "CustomManagedImageResourceGroupName",
"custom_managed_image_name": "CustomManagedImageName",
"managed_image_name": "ManagedImageName",
"managed_image_resource_group_name": "ManagedImageResourceGroupName",
"boot_diag_storage_account": "diagstgaccnt",
}
var c Config
_, err := c.Prepare(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) {
var c Config
c.Prepare(getArmBuilderConfiguration(), getPackerConfiguration())
deployment, err := GetKeyVaultDeployment(&c)
if err != nil {
t.Fatal(err)
}
if deployment.Properties.Mode != resources.Incremental {
t.Errorf("Expected deployment.Properties.Mode to be %s, but got %s", resources.Incremental, deployment.Properties.Mode)
}
if deployment.Properties.ParametersLink != nil {
t.Error("Expected the ParametersLink to be nil!")
}
if deployment.Properties.TemplateLink != nil {
t.Error("Expected the TemplateLink to be nil!")
}
if deployment.Properties.Parameters == nil {
t.Error("Expected the Parameters to not be nil!")
}
if deployment.Properties.Template == nil {
t.Error("Expected the Template to not be nil!")
}
}
// Ensure the KeyVault template is a valid JSON document.
func TestKeyVaultDeployment01(t *testing.T) {
var c Config
c.Prepare(getArmBuilderConfiguration(), getPackerConfiguration())
deployment, err := GetKeyVaultDeployment(&c)
if err != nil {
t.Fatal(err)
}
_, err = json.Marshal(deployment.Properties.Template)
if err != nil {
t.Fatal(err)
}
}
// Ensure the KeyVault template parameters are correct.
func TestKeyVaultDeployment02(t *testing.T) {
var c Config
c.Prepare(getArmBuilderConfigurationWithWindows(), getPackerConfiguration())
deployment, err := GetKeyVaultDeployment(&c)
if err != nil {
t.Fatal(err)
}
bs, err := json.Marshal(deployment.Properties.Parameters)
if err != nil {
t.Fatal(err)
}
var params template.TemplateParameters
err = json.Unmarshal(bs, &params)
if err != nil {
t.Fatal(err)
}
if params.ObjectId.Value != c.ClientConfig.ObjectID {
t.Errorf("Expected template parameter 'ObjectId' to be %s, but got %s.", params.ObjectId.Value, c.ClientConfig.ObjectID)
}
if params.TenantId.Value != c.ClientConfig.TenantID {
t.Errorf("Expected template parameter 'TenantId' to be %s, but got %s.", params.TenantId.Value, c.ClientConfig.TenantID)
}
if params.KeyVaultName.Value != c.tmpKeyVaultName {
t.Errorf("Expected template parameter 'KeyVaultName' to be %s, but got %s.", params.KeyVaultName.Value, c.tmpKeyVaultName)
}
if params.KeyVaultSecretValue.Value != c.winrmCertificate {
t.Errorf("Expected template parameter 'KeyVaultSecretValue' to be %s, but got %s.", params.KeyVaultSecretValue.Value, c.winrmCertificate)
}
}
// Ensure the KeyVault template is correct when tags are supplied.
func TestKeyVaultDeployment03(t *testing.T) {
tags := map[string]interface{}{
"azure_tags": map[string]string{
"tag01": "value01",
"tag02": "value02",
"tag03": "value03",
},
}
var c Config
c.Prepare(tags, getArmBuilderConfigurationWithWindows(), getPackerConfiguration())
deployment, err := GetKeyVaultDeployment(&c)
if err != nil {
t.Fatal(err)
}
err = approvaltests.VerifyJSONStruct(t, deployment.Properties.Template)
if err != nil {
t.Fatal(err)
}
}
func TestPlanInfo01(t *testing.T) {
planInfo := map[string]interface{}{
"plan_info": map[string]string{
"plan_name": "planName00",
"plan_product": "planProduct00",
"plan_publisher": "planPublisher00",
},
}
var c Config
c.Prepare(planInfo, getArmBuilderConfiguration(), getPackerConfiguration())
deployment, err := GetVirtualMachineDeployment(&c)
if err != nil {
t.Fatal(err)
}
err = approvaltests.VerifyJSONStruct(t, deployment.Properties.Template)
if err != nil {
t.Fatal(err)
}
}
func TestPlanInfo02(t *testing.T) {
planInfo := map[string]interface{}{
"azure_tags": map[string]string{
"dept": "engineering",
},
"plan_info": map[string]string{
"plan_name": "planName00",
"plan_product": "planProduct00",
"plan_publisher": "planPublisher00",
"plan_promotion_code": "planPromotionCode00",
},
}
var c Config
c.Prepare(planInfo, getArmBuilderConfiguration(), getPackerConfiguration())
deployment, err := GetVirtualMachineDeployment(&c)
if err != nil {
t.Fatal(err)
}
err = approvaltests.VerifyJSONStruct(t, deployment.Properties.Template)
if err != nil {
t.Fatal(err)
}
}

View File

@ -0,0 +1,80 @@
package arm
import (
"fmt"
"strings"
"github.com/hashicorp/packer-plugin-sdk/random"
)
type TempName struct {
AdminPassword string
CertificatePassword string
ComputeName string
DeploymentName string
KeyVaultName string
ResourceGroupName string
OSDiskName string
DataDiskName string
NicName string
SubnetName string
PublicIPAddressName string
VirtualNetworkName string
NsgName string
}
func NewTempName(p string) *TempName {
tempName := &TempName{}
suffix := random.AlphaNumLower(5)
if p == "" {
p = "pkr"
suffix = random.AlphaNumLower(10)
}
tempName.ComputeName = fmt.Sprintf("%svm%s", p, suffix)
tempName.DeploymentName = fmt.Sprintf("%sdp%s", p, suffix)
tempName.KeyVaultName = fmt.Sprintf("%skv%s", p, suffix)
tempName.OSDiskName = fmt.Sprintf("%sos%s", p, suffix)
tempName.DataDiskName = fmt.Sprintf("%sdd%s", p, suffix)
tempName.NicName = fmt.Sprintf("%sni%s", p, suffix)
tempName.PublicIPAddressName = fmt.Sprintf("%sip%s", p, suffix)
tempName.SubnetName = fmt.Sprintf("%ssn%s", p, suffix)
tempName.VirtualNetworkName = fmt.Sprintf("%svn%s", p, suffix)
tempName.NsgName = fmt.Sprintf("%ssg%s", p, suffix)
tempName.ResourceGroupName = fmt.Sprintf("%s-Resource-Group-%s", p, suffix)
tempName.AdminPassword = generatePassword()
tempName.CertificatePassword = random.AlphaNum(32)
return tempName
}
// generate a password that is acceptable to Azure
// Three of the four items must be met.
// 1. Contains an uppercase character
// 2. Contains a lowercase character
// 3. Contains a numeric digit
// 4. Contains a special character
func generatePassword() string {
var s string
for i := 0; i < 100; i++ {
s := random.AlphaNum(32)
if !strings.ContainsAny(s, random.PossibleNumbers) {
continue
}
if !strings.ContainsAny(s, random.PossibleLowerCase) {
continue
}
if !strings.ContainsAny(s, random.PossibleUpperCase) {
continue
}
return s
}
// if an acceptable password cannot be generated in 100 tries, give up
return s
}

View File

@ -0,0 +1,143 @@
package arm
import (
"strings"
"testing"
"github.com/hashicorp/packer-plugin-sdk/random"
)
func TestTempNameShouldCreatePrefixedRandomNames(t *testing.T) {
tempName := NewTempName("")
if strings.Index(tempName.ComputeName, "pkrvm") != 0 {
t.Errorf("Expected ComputeName to begin with 'pkrvm', but got '%s'!", tempName.ComputeName)
}
if strings.Index(tempName.DeploymentName, "pkrdp") != 0 {
t.Errorf("Expected ComputeName to begin with 'pkrdp', but got '%s'!", tempName.ComputeName)
}
if strings.Index(tempName.OSDiskName, "pkros") != 0 {
t.Errorf("Expected OSDiskName to begin with 'pkros', but got '%s'!", tempName.OSDiskName)
}
if strings.Index(tempName.NicName, "pkrni") != 0 {
t.Errorf("Expected NicName to begin with 'pkrni', but got '%s'!", tempName.NicName)
}
if strings.Index(tempName.PublicIPAddressName, "pkrip") != 0 {
t.Errorf("Expected PublicIPAddressName to begin with 'pkrip', but got '%s'!", tempName.PublicIPAddressName)
}
if strings.Index(tempName.ResourceGroupName, "pkr-Resource-Group-") != 0 {
t.Errorf("Expected ResourceGroupName to begin with 'pkr-Resource-Group-', but got '%s'!", tempName.ResourceGroupName)
}
if strings.Index(tempName.SubnetName, "pkrsn") != 0 {
t.Errorf("Expected SubnetName to begin with 'pkrip', but got '%s'!", tempName.SubnetName)
}
if strings.Index(tempName.VirtualNetworkName, "pkrvn") != 0 {
t.Errorf("Expected VirtualNetworkName to begin with 'pkrvn', but got '%s'!", tempName.VirtualNetworkName)
}
if strings.Index(tempName.NsgName, "pkrsg") != 0 {
t.Errorf("Expected NsgName to begin with 'pkrsg', but got '%s'!", tempName.NsgName)
}
}
func TestTempAdminPassword(t *testing.T) {
tempName := NewTempName("")
if !strings.ContainsAny(tempName.AdminPassword, random.PossibleNumbers) {
t.Errorf("Expected AdminPassword to contain at least one of '%s'!", random.PossibleNumbers)
}
if !strings.ContainsAny(tempName.AdminPassword, random.PossibleLowerCase) {
t.Errorf("Expected AdminPassword to contain at least one of '%s'!", random.PossibleLowerCase)
}
if !strings.ContainsAny(tempName.AdminPassword, random.PossibleUpperCase) {
t.Errorf("Expected AdminPassword to contain at least one of '%s'!", random.PossibleUpperCase)
}
}
func TestTempNameShouldHaveSameSuffix(t *testing.T) {
tempName := NewTempName("")
suffix := tempName.ComputeName[5:]
if strings.HasSuffix(tempName.ComputeName, suffix) != true {
t.Errorf("Expected ComputeName to end with '%s', but the value is '%s'!", suffix, tempName.ComputeName)
}
if strings.HasSuffix(tempName.DeploymentName, suffix) != true {
t.Errorf("Expected DeploymentName to end with '%s', but the value is '%s'!", suffix, tempName.DeploymentName)
}
if strings.HasSuffix(tempName.OSDiskName, suffix) != true {
t.Errorf("Expected OSDiskName to end with '%s', but the value is '%s'!", suffix, tempName.OSDiskName)
}
if strings.HasSuffix(tempName.NicName, suffix) != true {
t.Errorf("Expected NicName to end with '%s', but the value is '%s'!", suffix, tempName.PublicIPAddressName)
}
if strings.HasSuffix(tempName.PublicIPAddressName, suffix) != true {
t.Errorf("Expected PublicIPAddressName to end with '%s', but the value is '%s'!", suffix, tempName.PublicIPAddressName)
}
if strings.HasSuffix(tempName.ResourceGroupName, suffix) != true {
t.Errorf("Expected ResourceGroupName to end with '%s', but the value is '%s'!", suffix, tempName.ResourceGroupName)
}
if strings.HasSuffix(tempName.SubnetName, suffix) != true {
t.Errorf("Expected SubnetName to end with '%s', but the value is '%s'!", suffix, tempName.SubnetName)
}
if strings.HasSuffix(tempName.VirtualNetworkName, suffix) != true {
t.Errorf("Expected VirtualNetworkName to end with '%s', but the value is '%s'!", suffix, tempName.VirtualNetworkName)
}
if strings.HasSuffix(tempName.NsgName, suffix) != true {
t.Errorf("Expected NsgName to end with '%s', but the value is '%s'!", suffix, tempName.NsgName)
}
}
func TestTempNameShouldCreateCustomPrefix(t *testing.T) {
tempName := NewTempName("CustPrefix")
if strings.Index(tempName.ComputeName, "CustPrefixvm") != 0 {
t.Errorf("Expected ComputeName to begin with 'CustPrefixvm', but got '%s'!", tempName.ComputeName)
}
if strings.Index(tempName.DeploymentName, "CustPrefixdp") != 0 {
t.Errorf("Expected ComputeName to begin with 'CustPrefixdp', but got '%s'!", tempName.ComputeName)
}
if strings.Index(tempName.OSDiskName, "CustPrefixos") != 0 {
t.Errorf("Expected OSDiskName to begin with 'CustPrefixos', but got '%s'!", tempName.OSDiskName)
}
if strings.Index(tempName.NicName, "CustPrefixni") != 0 {
t.Errorf("Expected NicName to begin with 'CustPrefixni', but got '%s'!", tempName.NicName)
}
if strings.Index(tempName.PublicIPAddressName, "CustPrefixip") != 0 {
t.Errorf("Expected PublicIPAddressName to begin with 'CustPrefixip', but got '%s'!", tempName.PublicIPAddressName)
}
if strings.Index(tempName.ResourceGroupName, "CustPrefix-Resource-Group-") != 0 {
t.Errorf("Expected ResourceGroupName to begin with 'packer-Resource-Group-', but got '%s'!", tempName.ResourceGroupName)
}
if strings.Index(tempName.SubnetName, "CustPrefixsn") != 0 {
t.Errorf("Expected SubnetName to begin with 'pkrip', but got '%s'!", tempName.SubnetName)
}
if strings.Index(tempName.VirtualNetworkName, "CustPrefixvn") != 0 {
t.Errorf("Expected VirtualNetworkName to begin with 'CustPrefixvn', but got '%s'!", tempName.VirtualNetworkName)
}
if strings.Index(tempName.NsgName, "CustPrefixsg") != 0 {
t.Errorf("Expected NsgName to begin with 'CustPrefixsg', but got '%s'!", tempName.NsgName)
}
}

View File

@ -0,0 +1,622 @@
//go:generate packer-sdc struct-markdown
//go:generate packer-sdc mapstructure-to-hcl2 -type Config
// Package chroot is able to create an Azure managed image without requiring the
// launch of a new virtual machine for every build. It does this by attaching and
// mounting the root disk and chrooting into that directory.
// It then creates a managed image from that attached disk.
package chroot
import (
"context"
"errors"
"fmt"
"log"
"runtime"
"strings"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer-plugin-sdk/chroot"
"github.com/hashicorp/packer-plugin-sdk/common"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/config"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
azcommon "github.com/hashicorp/packer/builder/azure/common"
"github.com/hashicorp/packer/builder/azure/common/client"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/mitchellh/mapstructure"
)
// BuilderID is the unique ID for this builder
const BuilderID = "azure.chroot"
// Config is the configuration that is chained through the steps and settable
// from the template.
type Config struct {
common.PackerConfig `mapstructure:",squash"`
ClientConfig client.Config `mapstructure:",squash"`
// When set to `true`, starts with an empty, unpartitioned disk. Defaults to `false`.
FromScratch bool `mapstructure:"from_scratch"`
// One of the following can be used as a source for an image:
// - a shared image version resource ID
// - a managed disk resource ID
// - a publisher:offer:sku:version specifier for plaform image sources.
Source string `mapstructure:"source" required:"true"`
sourceType sourceType
// How to run shell commands. This may be useful to set environment variables or perhaps run
// a command with sudo or so on. This is a configuration template where the `.Command` variable
// is replaced with the command to be run. Defaults to `{{.Command}}`.
CommandWrapper string `mapstructure:"command_wrapper"`
// A series of commands to execute after attaching the root volume and before mounting the chroot.
// This is not required unless using `from_scratch`. If so, this should include any partitioning
// and filesystem creation commands. The path to the device is provided by `{{.Device}}`.
PreMountCommands []string `mapstructure:"pre_mount_commands"`
// Options to supply the `mount` command when mounting devices. Each option will be prefixed with
// `-o` and supplied to the `mount` command ran by Packer. Because this command is ran in a shell,
// user discretion is advised. See this manual page for the `mount` command for valid file system specific options.
MountOptions []string `mapstructure:"mount_options"`
// The partition number containing the / partition. By default this is the first partition of the volume.
MountPartition string `mapstructure:"mount_partition"`
// The path where the volume will be mounted. This is where the chroot environment will be. This defaults
// to `/mnt/packer-amazon-chroot-volumes/{{.Device}}`. This is a configuration template where the `.Device`
// variable is replaced with the name of the device where the volume is attached.
MountPath string `mapstructure:"mount_path"`
// As `pre_mount_commands`, but the commands are executed after mounting the root device and before the
// extra mount and copy steps. The device and mount path are provided by `{{.Device}}` and `{{.MountPath}}`.
PostMountCommands []string `mapstructure:"post_mount_commands"`
// This is a list of devices to mount into the chroot environment. This configuration parameter requires
// some additional documentation which is in the "Chroot Mounts" section below. Please read that section
// for more information on how to use this.
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
// Paths to files on the running Azure instance that will be copied into the chroot environment prior to
// provisioning. Defaults to `/etc/resolv.conf` so that DNS lookups work. Pass an empty list to skip copying
// `/etc/resolv.conf`. You may need to do this if you're building an image that uses systemd.
CopyFiles []string `mapstructure:"copy_files"`
// Try to resize the OS disk to this size on the first copy. Disks can only be englarged. If not specified,
// the disk will keep its original size. Required when using `from_scratch`
OSDiskSizeGB int32 `mapstructure:"os_disk_size_gb"`
// The [storage SKU](https://docs.microsoft.com/en-us/rest/api/compute/disks/createorupdate#diskstorageaccounttypes)
// to use for the OS Disk. Defaults to `Standard_LRS`.
OSDiskStorageAccountType string `mapstructure:"os_disk_storage_account_type"`
// The [cache type](https://docs.microsoft.com/en-us/rest/api/compute/images/createorupdate#cachingtypes)
// specified in the resulting image and for attaching it to the Packer VM. Defaults to `ReadOnly`
OSDiskCacheType string `mapstructure:"os_disk_cache_type"`
// The [storage SKU](https://docs.microsoft.com/en-us/rest/api/compute/disks/createorupdate#diskstorageaccounttypes)
// to use for datadisks. Defaults to `Standard_LRS`.
DataDiskStorageAccountType string `mapstructure:"data_disk_storage_account_type"`
// The [cache type](https://docs.microsoft.com/en-us/rest/api/compute/images/createorupdate#cachingtypes)
// specified in the resulting image and for attaching it to the Packer VM. Defaults to `ReadOnly`
DataDiskCacheType string `mapstructure:"data_disk_cache_type"`
// The [Hyper-V generation type](https://docs.microsoft.com/en-us/rest/api/compute/images/createorupdate#hypervgenerationtypes) for Managed Image output.
// Defaults to `V1`.
ImageHyperVGeneration string `mapstructure:"image_hyperv_generation"`
// The id of the temporary OS disk that will be created. Will be generated if not set.
TemporaryOSDiskID string `mapstructure:"temporary_os_disk_id"`
// The id of the temporary OS disk snapshot that will be created. Will be generated if not set.
TemporaryOSDiskSnapshotID string `mapstructure:"temporary_os_disk_snapshot_id"`
// The prefix for the resource ids of the temporary data disks that will be created. The disks will be suffixed with a number. Will be generated if not set.
TemporaryDataDiskIDPrefix string `mapstructure:"temporary_data_disk_id_prefix"`
// The prefix for the resource ids of the temporary data disk snapshots that will be created. The snapshots will be suffixed with a number. Will be generated if not set.
TemporaryDataDiskSnapshotIDPrefix string `mapstructure:"temporary_data_disk_snapshot_id"`
// If set to `true`, leaves the temporary disks and snapshots behind in the Packer VM resource group. Defaults to `false`
SkipCleanup bool `mapstructure:"skip_cleanup"`
// The managed image to create using this build.
ImageResourceID string `mapstructure:"image_resource_id"`
// The shared image to create using this build.
SharedImageGalleryDestination SharedImageGalleryDestination `mapstructure:"shared_image_destination"`
ctx interpolate.Context
}
type sourceType string
const (
sourcePlatformImage sourceType = "PlatformImage"
sourceDisk sourceType = "Disk"
sourceSharedImage sourceType = "SharedImage"
)
// GetContext implements ContextProvider to allow steps to use the config context
// for template interpolation
func (c *Config) GetContext() interpolate.Context {
return c.ctx
}
type Builder struct {
config Config
runner multistep.Runner
}
// verify interface implementation
var _ packersdk.Builder = &Builder{}
func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() }
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
b.config.ctx.Funcs = azcommon.TemplateFuncs
b.config.ctx.Funcs["vm"] = CreateVMMetadataTemplateFunc()
md := &mapstructure.Metadata{}
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderID,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
// these fields are interpolated in the steps,
// when more information is available
"command_wrapper",
"post_mount_commands",
"pre_mount_commands",
"mount_path",
},
},
Metadata: md,
}, raws...)
if err != nil {
return nil, nil, err
}
var errs *packersdk.MultiError
var warns []string
// Defaults
err = b.config.ClientConfig.SetDefaultValues()
if err != nil {
return nil, nil, err
}
if b.config.ChrootMounts == nil {
b.config.ChrootMounts = make([][]string, 0)
}
if len(b.config.ChrootMounts) == 0 {
b.config.ChrootMounts = [][]string{
{"proc", "proc", "/proc"},
{"sysfs", "sysfs", "/sys"},
{"bind", "/dev", "/dev"},
{"devpts", "devpts", "/dev/pts"},
{"binfmt_misc", "binfmt_misc", "/proc/sys/fs/binfmt_misc"},
}
}
// set default copy file if we're not giving our own
if b.config.CopyFiles == nil {
if !b.config.FromScratch {
b.config.CopyFiles = []string{"/etc/resolv.conf"}
}
}
if b.config.CommandWrapper == "" {
b.config.CommandWrapper = "{{.Command}}"
}
if b.config.MountPath == "" {
b.config.MountPath = "/mnt/packer-azure-chroot-disks/{{.Device}}"
}
if b.config.MountPartition == "" {
b.config.MountPartition = "1"
}
if b.config.TemporaryOSDiskID == "" {
if def, err := interpolate.Render(
"/subscriptions/{{ vm `subscription_id` }}/resourceGroups/{{ vm `resource_group` }}/providers/Microsoft.Compute/disks/PackerTemp-osdisk-{{timestamp}}",
&b.config.ctx); err == nil {
b.config.TemporaryOSDiskID = def
} else {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("unable to render temporary disk id: %s", err))
}
}
if b.config.TemporaryOSDiskSnapshotID == "" {
if def, err := interpolate.Render(
"/subscriptions/{{ vm `subscription_id` }}/resourceGroups/{{ vm `resource_group` }}/providers/Microsoft.Compute/snapshots/PackerTemp-osdisk-snapshot-{{timestamp}}",
&b.config.ctx); err == nil {
b.config.TemporaryOSDiskSnapshotID = def
} else {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("unable to render temporary snapshot id: %s", err))
}
}
if b.config.TemporaryDataDiskIDPrefix == "" {
if def, err := interpolate.Render(
"/subscriptions/{{ vm `subscription_id` }}/resourceGroups/{{ vm `resource_group` }}/providers/Microsoft.Compute/disks/PackerTemp-datadisk-{{timestamp}}-",
&b.config.ctx); err == nil {
b.config.TemporaryDataDiskIDPrefix = def
} else {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("unable to render temporary data disk id prefix: %s", err))
}
}
if b.config.TemporaryDataDiskSnapshotIDPrefix == "" {
if def, err := interpolate.Render(
"/subscriptions/{{ vm `subscription_id` }}/resourceGroups/{{ vm `resource_group` }}/providers/Microsoft.Compute/snapshots/PackerTemp-datadisk-snapshot-{{timestamp}}-",
&b.config.ctx); err == nil {
b.config.TemporaryDataDiskSnapshotIDPrefix = def
} else {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("unable to render temporary data disk snapshot id prefix: %s", err))
}
}
if b.config.OSDiskStorageAccountType == "" {
b.config.OSDiskStorageAccountType = string(compute.PremiumLRS)
}
if b.config.OSDiskCacheType == "" {
b.config.OSDiskCacheType = string(compute.CachingTypesReadOnly)
}
if b.config.DataDiskStorageAccountType == "" {
b.config.DataDiskStorageAccountType = string(compute.PremiumLRS)
}
if b.config.DataDiskCacheType == "" {
b.config.DataDiskCacheType = string(compute.CachingTypesReadOnly)
}
if b.config.ImageHyperVGeneration == "" {
b.config.ImageHyperVGeneration = string(compute.V1)
}
// checks, accumulate any errors or warnings
if b.config.FromScratch {
if b.config.Source != "" {
errs = packersdk.MultiErrorAppend(
errs, errors.New("source cannot be specified when building from_scratch"))
}
if b.config.OSDiskSizeGB == 0 {
errs = packersdk.MultiErrorAppend(
errs, errors.New("os_disk_size_gb is required with from_scratch"))
}
if len(b.config.PreMountCommands) == 0 {
errs = packersdk.MultiErrorAppend(
errs, errors.New("pre_mount_commands is required with from_scratch"))
}
} else {
if _, err := client.ParsePlatformImageURN(b.config.Source); err == nil {
log.Println("Source is platform image:", b.config.Source)
b.config.sourceType = sourcePlatformImage
} else if id, err := client.ParseResourceID(b.config.Source); err == nil &&
strings.EqualFold(id.Provider, "Microsoft.Compute") &&
strings.EqualFold(id.ResourceType.String(), "disks") {
log.Println("Source is a disk resource ID:", b.config.Source)
b.config.sourceType = sourceDisk
} else if id, err := client.ParseResourceID(b.config.Source); err == nil &&
strings.EqualFold(id.Provider, "Microsoft.Compute") &&
strings.EqualFold(id.ResourceType.String(), "galleries/images/versions") {
log.Println("Source is a shared image ID:", b.config.Source)
b.config.sourceType = sourceSharedImage
} else {
errs = packersdk.MultiErrorAppend(
errs, fmt.Errorf("source: %q is not a valid platform image specifier, nor is it a disk resource ID", b.config.Source))
}
}
if err := checkDiskCacheType(b.config.OSDiskCacheType); err != nil {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("os_disk_cache_type: %v", err))
}
if err := checkStorageAccountType(b.config.OSDiskStorageAccountType); err != nil {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("os_disk_storage_account_type: %v", err))
}
if err := checkDiskCacheType(b.config.DataDiskCacheType); err != nil {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("data_disk_cache_type: %v", err))
}
if err := checkStorageAccountType(b.config.DataDiskStorageAccountType); err != nil {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("data_disk_storage_account_type: %v", err))
}
if b.config.ImageResourceID != "" {
r, err := azure.ParseResourceID(b.config.ImageResourceID)
if err != nil ||
!strings.EqualFold(r.Provider, "Microsoft.Compute") ||
!strings.EqualFold(r.ResourceType, "images") {
errs = packersdk.MultiErrorAppend(fmt.Errorf(
"image_resource_id: %q is not a valid image resource id", b.config.ImageResourceID))
}
}
if azcommon.StringsContains(md.Keys, "shared_image_destination") {
e, w := b.config.SharedImageGalleryDestination.Validate("shared_image_destination")
if len(e) > 0 {
errs = packersdk.MultiErrorAppend(errs, e...)
}
if len(w) > 0 {
warns = append(warns, w...)
}
}
if !azcommon.StringsContains(md.Keys, "shared_image_destination") && b.config.ImageResourceID == "" {
errs = packersdk.MultiErrorAppend(errs, errors.New("image_resource_id or shared_image_destination is required"))
}
if err := checkHyperVGeneration(b.config.ImageHyperVGeneration); err != nil {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("image_hyperv_generation: %v", err))
}
if errs != nil {
return nil, warns, errs
}
packersdk.LogSecretFilter.Set(b.config.ClientConfig.ClientSecret, b.config.ClientConfig.ClientJWT)
return nil, warns, nil
}
func checkDiskCacheType(s string) interface{} {
for _, v := range compute.PossibleCachingTypesValues() {
if compute.CachingTypes(s) == v {
return nil
}
}
return fmt.Errorf("%q is not a valid value %v",
s, compute.PossibleCachingTypesValues())
}
func checkStorageAccountType(s string) interface{} {
for _, v := range compute.PossibleDiskStorageAccountTypesValues() {
if compute.DiskStorageAccountTypes(s) == v {
return nil
}
}
return fmt.Errorf("%q is not a valid value %v",
s, compute.PossibleDiskStorageAccountTypesValues())
}
func checkHyperVGeneration(s string) interface{} {
for _, v := range compute.PossibleHyperVGenerationValues() {
if compute.HyperVGeneration(s) == v {
return nil
}
}
return fmt.Errorf("%q is not a valid value %v",
s, compute.PossibleHyperVGenerationValues())
}
func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (packersdk.Artifact, error) {
switch runtime.GOOS {
case "linux", "freebsd":
break
default:
return nil, errors.New("the azure-chroot builder only works on Linux and FreeBSD environments")
}
err := b.config.ClientConfig.FillParameters()
if err != nil {
return nil, fmt.Errorf("error setting Azure client defaults: %v", err)
}
azcli, err := client.New(b.config.ClientConfig, ui.Say)
if err != nil {
return nil, fmt.Errorf("error creating Azure client: %v", err)
}
wrappedCommand := func(command string) (string, error) {
ictx := b.config.ctx
ictx.Data = &struct{ Command string }{Command: command}
return interpolate.Render(b.config.CommandWrapper, &ictx)
}
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
state.Put("hook", hook)
state.Put("ui", ui)
state.Put("azureclient", azcli)
state.Put("wrappedCommand", common.CommandWrapper(wrappedCommand))
info, err := azcli.MetadataClient().GetComputeInfo()
if err != nil {
log.Printf("MetadataClient().GetComputeInfo(): error: %+v", err)
err := fmt.Errorf(
"Error retrieving information ARM resource ID and location" +
"of the VM that Packer is running on.\n" +
"Please verify that Packer is running on a proper Azure VM.")
ui.Error(err.Error())
return nil, err
}
state.Put("instance", info)
// Build the step array from the config
steps := buildsteps(b.config, info)
// Run!
b.runner = commonsteps.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(ctx, state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
// Build the artifact and return it
artifact := &azcommon.Artifact{
BuilderIdValue: BuilderID,
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
AzureClientSet: azcli,
}
if b.config.ImageResourceID != "" {
artifact.Resources = append(artifact.Resources, b.config.ImageResourceID)
}
if e, _ := b.config.SharedImageGalleryDestination.Validate(""); len(e) == 0 {
artifact.Resources = append(artifact.Resources, b.config.SharedImageGalleryDestination.ResourceID(info.SubscriptionID))
}
if b.config.SkipCleanup {
if d, ok := state.GetOk(stateBagKey_Diskset); ok {
for _, disk := range d.(Diskset) {
artifact.Resources = append(artifact.Resources, disk.String())
}
}
if d, ok := state.GetOk(stateBagKey_Snapshotset); ok {
for _, snapshot := range d.(Diskset) {
artifact.Resources = append(artifact.Resources, snapshot.String())
}
}
}
return artifact, nil
}
func buildsteps(config Config, info *client.ComputeInfo) []multistep.Step {
// Build the steps
var steps []multistep.Step
addSteps := func(s ...multistep.Step) { // convenience function
steps = append(steps, s...)
}
e, _ := config.SharedImageGalleryDestination.Validate("")
hasValidSharedImage := len(e) == 0
if hasValidSharedImage {
// validate destination early
addSteps(
&StepVerifySharedImageDestination{
Image: config.SharedImageGalleryDestination,
Location: info.Location,
},
)
}
if config.FromScratch {
addSteps(&StepCreateNewDiskset{
OSDiskID: config.TemporaryOSDiskID,
OSDiskSizeGB: config.OSDiskSizeGB,
OSDiskStorageAccountType: config.OSDiskStorageAccountType,
HyperVGeneration: config.ImageHyperVGeneration,
Location: info.Location})
} else {
switch config.sourceType {
case sourcePlatformImage:
if pi, err := client.ParsePlatformImageURN(config.Source); err == nil {
if strings.EqualFold(pi.Version, "latest") {
addSteps(
&StepResolvePlatformImageVersion{
PlatformImage: pi,
Location: info.Location,
})
}
addSteps(
&StepCreateNewDiskset{
OSDiskID: config.TemporaryOSDiskID,
OSDiskSizeGB: config.OSDiskSizeGB,
OSDiskStorageAccountType: config.OSDiskStorageAccountType,
HyperVGeneration: config.ImageHyperVGeneration,
Location: info.Location,
SourcePlatformImage: pi,
SkipCleanup: config.SkipCleanup,
})
} else {
panic("Couldn't parse platfrom image urn: " + config.Source + " err: " + err.Error())
}
case sourceDisk:
addSteps(
&StepVerifySourceDisk{
SourceDiskResourceID: config.Source,
Location: info.Location,
},
&StepCreateNewDiskset{
OSDiskID: config.TemporaryOSDiskID,
OSDiskSizeGB: config.OSDiskSizeGB,
OSDiskStorageAccountType: config.OSDiskStorageAccountType,
HyperVGeneration: config.ImageHyperVGeneration,
SourceOSDiskResourceID: config.Source,
Location: info.Location,
SkipCleanup: config.SkipCleanup,
})
case sourceSharedImage:
addSteps(
&StepVerifySharedImageSource{
SharedImageID: config.Source,
SubscriptionID: info.SubscriptionID,
Location: info.Location,
},
&StepCreateNewDiskset{
OSDiskID: config.TemporaryOSDiskID,
DataDiskIDPrefix: config.TemporaryDataDiskIDPrefix,
OSDiskSizeGB: config.OSDiskSizeGB,
OSDiskStorageAccountType: config.OSDiskStorageAccountType,
DataDiskStorageAccountType: config.DataDiskStorageAccountType,
SourceImageResourceID: config.Source,
Location: info.Location,
SkipCleanup: config.SkipCleanup,
})
default:
panic(fmt.Errorf("Unknown source type: %+q", config.sourceType))
}
}
addSteps(
&StepAttachDisk{}, // uses os_disk_resource_id and sets 'device' in stateBag
&chroot.StepPreMountCommands{
Commands: config.PreMountCommands,
},
&StepMountDevice{
MountOptions: config.MountOptions,
MountPartition: config.MountPartition,
MountPath: config.MountPath,
},
&chroot.StepPostMountCommands{
Commands: config.PostMountCommands,
},
&chroot.StepMountExtra{
ChrootMounts: config.ChrootMounts,
},
&chroot.StepCopyFiles{
Files: config.CopyFiles,
},
&chroot.StepChrootProvision{},
&chroot.StepEarlyCleanup{},
)
if config.ImageResourceID != "" {
addSteps(&StepCreateImage{
ImageResourceID: config.ImageResourceID,
ImageOSState: string(compute.Generalized),
OSDiskCacheType: config.OSDiskCacheType,
OSDiskStorageAccountType: config.OSDiskStorageAccountType,
Location: info.Location,
})
}
if hasValidSharedImage {
addSteps(
&StepCreateSnapshotset{
OSDiskSnapshotID: config.TemporaryOSDiskSnapshotID,
DataDiskSnapshotIDPrefix: config.TemporaryDataDiskSnapshotIDPrefix,
Location: info.Location,
SkipCleanup: config.SkipCleanup,
},
&StepCreateSharedImageVersion{
Destination: config.SharedImageGalleryDestination,
OSDiskCacheType: config.OSDiskCacheType,
Location: info.Location,
},
)
}
return steps
}

View File

@ -0,0 +1,111 @@
// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT.
package chroot
import (
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
)
// FlatConfig is an auto-generated flat version of Config.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatConfig struct {
PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"`
PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"`
PackerCoreVersion *string `mapstructure:"packer_core_version" cty:"packer_core_version" hcl:"packer_core_version"`
PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"`
PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"`
PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"`
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
CloudEnvironmentName *string `mapstructure:"cloud_environment_name" required:"false" cty:"cloud_environment_name" hcl:"cloud_environment_name"`
ClientID *string `mapstructure:"client_id" cty:"client_id" hcl:"client_id"`
ClientSecret *string `mapstructure:"client_secret" cty:"client_secret" hcl:"client_secret"`
ClientCertPath *string `mapstructure:"client_cert_path" cty:"client_cert_path" hcl:"client_cert_path"`
ClientCertExpireTimeout *string `mapstructure:"client_cert_token_timeout" required:"false" cty:"client_cert_token_timeout" hcl:"client_cert_token_timeout"`
ClientJWT *string `mapstructure:"client_jwt" cty:"client_jwt" hcl:"client_jwt"`
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"`
UseAzureCLIAuth *bool `mapstructure:"use_azure_cli_auth" required:"false" cty:"use_azure_cli_auth" hcl:"use_azure_cli_auth"`
FromScratch *bool `mapstructure:"from_scratch" cty:"from_scratch" hcl:"from_scratch"`
Source *string `mapstructure:"source" required:"true" cty:"source" hcl:"source"`
CommandWrapper *string `mapstructure:"command_wrapper" cty:"command_wrapper" hcl:"command_wrapper"`
PreMountCommands []string `mapstructure:"pre_mount_commands" cty:"pre_mount_commands" hcl:"pre_mount_commands"`
MountOptions []string `mapstructure:"mount_options" cty:"mount_options" hcl:"mount_options"`
MountPartition *string `mapstructure:"mount_partition" cty:"mount_partition" hcl:"mount_partition"`
MountPath *string `mapstructure:"mount_path" cty:"mount_path" hcl:"mount_path"`
PostMountCommands []string `mapstructure:"post_mount_commands" cty:"post_mount_commands" hcl:"post_mount_commands"`
ChrootMounts [][]string `mapstructure:"chroot_mounts" cty:"chroot_mounts" hcl:"chroot_mounts"`
CopyFiles []string `mapstructure:"copy_files" cty:"copy_files" hcl:"copy_files"`
OSDiskSizeGB *int32 `mapstructure:"os_disk_size_gb" cty:"os_disk_size_gb" hcl:"os_disk_size_gb"`
OSDiskStorageAccountType *string `mapstructure:"os_disk_storage_account_type" cty:"os_disk_storage_account_type" hcl:"os_disk_storage_account_type"`
OSDiskCacheType *string `mapstructure:"os_disk_cache_type" cty:"os_disk_cache_type" hcl:"os_disk_cache_type"`
DataDiskStorageAccountType *string `mapstructure:"data_disk_storage_account_type" cty:"data_disk_storage_account_type" hcl:"data_disk_storage_account_type"`
DataDiskCacheType *string `mapstructure:"data_disk_cache_type" cty:"data_disk_cache_type" hcl:"data_disk_cache_type"`
ImageHyperVGeneration *string `mapstructure:"image_hyperv_generation" cty:"image_hyperv_generation" hcl:"image_hyperv_generation"`
TemporaryOSDiskID *string `mapstructure:"temporary_os_disk_id" cty:"temporary_os_disk_id" hcl:"temporary_os_disk_id"`
TemporaryOSDiskSnapshotID *string `mapstructure:"temporary_os_disk_snapshot_id" cty:"temporary_os_disk_snapshot_id" hcl:"temporary_os_disk_snapshot_id"`
TemporaryDataDiskIDPrefix *string `mapstructure:"temporary_data_disk_id_prefix" cty:"temporary_data_disk_id_prefix" hcl:"temporary_data_disk_id_prefix"`
TemporaryDataDiskSnapshotIDPrefix *string `mapstructure:"temporary_data_disk_snapshot_id" cty:"temporary_data_disk_snapshot_id" hcl:"temporary_data_disk_snapshot_id"`
SkipCleanup *bool `mapstructure:"skip_cleanup" cty:"skip_cleanup" hcl:"skip_cleanup"`
ImageResourceID *string `mapstructure:"image_resource_id" cty:"image_resource_id" hcl:"image_resource_id"`
SharedImageGalleryDestination *FlatSharedImageGalleryDestination `mapstructure:"shared_image_destination" cty:"shared_image_destination" hcl:"shared_image_destination"`
}
// FlatMapstructure returns a new FlatConfig.
// FlatConfig is an auto-generated flat version of Config.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatConfig)
}
// HCL2Spec returns the hcl spec of a Config.
// This spec is used by HCL to read the fields of Config.
// 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_core_version": &hcldec.AttrSpec{Name: "packer_core_version", 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_cert_token_timeout": &hcldec.AttrSpec{Name: "client_cert_token_timeout", 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},
"use_azure_cli_auth": &hcldec.AttrSpec{Name: "use_azure_cli_auth", Type: cty.Bool, Required: false},
"from_scratch": &hcldec.AttrSpec{Name: "from_scratch", Type: cty.Bool, Required: false},
"source": &hcldec.AttrSpec{Name: "source", Type: cty.String, Required: false},
"command_wrapper": &hcldec.AttrSpec{Name: "command_wrapper", Type: cty.String, Required: false},
"pre_mount_commands": &hcldec.AttrSpec{Name: "pre_mount_commands", Type: cty.List(cty.String), Required: false},
"mount_options": &hcldec.AttrSpec{Name: "mount_options", Type: cty.List(cty.String), Required: false},
"mount_partition": &hcldec.AttrSpec{Name: "mount_partition", Type: cty.String, Required: false},
"mount_path": &hcldec.AttrSpec{Name: "mount_path", Type: cty.String, Required: false},
"post_mount_commands": &hcldec.AttrSpec{Name: "post_mount_commands", Type: cty.List(cty.String), Required: false},
"chroot_mounts": &hcldec.AttrSpec{Name: "chroot_mounts", Type: cty.List(cty.List(cty.String)), Required: false},
"copy_files": &hcldec.AttrSpec{Name: "copy_files", Type: cty.List(cty.String), Required: false},
"os_disk_size_gb": &hcldec.AttrSpec{Name: "os_disk_size_gb", Type: cty.Number, Required: false},
"os_disk_storage_account_type": &hcldec.AttrSpec{Name: "os_disk_storage_account_type", Type: cty.String, Required: false},
"os_disk_cache_type": &hcldec.AttrSpec{Name: "os_disk_cache_type", Type: cty.String, Required: false},
"data_disk_storage_account_type": &hcldec.AttrSpec{Name: "data_disk_storage_account_type", Type: cty.String, Required: false},
"data_disk_cache_type": &hcldec.AttrSpec{Name: "data_disk_cache_type", Type: cty.String, Required: false},
"image_hyperv_generation": &hcldec.AttrSpec{Name: "image_hyperv_generation", Type: cty.String, Required: false},
"temporary_os_disk_id": &hcldec.AttrSpec{Name: "temporary_os_disk_id", Type: cty.String, Required: false},
"temporary_os_disk_snapshot_id": &hcldec.AttrSpec{Name: "temporary_os_disk_snapshot_id", Type: cty.String, Required: false},
"temporary_data_disk_id_prefix": &hcldec.AttrSpec{Name: "temporary_data_disk_id_prefix", Type: cty.String, Required: false},
"temporary_data_disk_snapshot_id": &hcldec.AttrSpec{Name: "temporary_data_disk_snapshot_id", Type: cty.String, Required: false},
"skip_cleanup": &hcldec.AttrSpec{Name: "skip_cleanup", Type: cty.Bool, Required: false},
"image_resource_id": &hcldec.AttrSpec{Name: "image_resource_id", Type: cty.String, Required: false},
"shared_image_destination": &hcldec.BlockSpec{TypeName: "shared_image_destination", Nested: hcldec.ObjectSpec((*FlatSharedImageGalleryDestination)(nil).HCL2Spec())},
}
return s
}

View File

@ -0,0 +1,234 @@
package chroot
import (
"strings"
"testing"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/builder/azure/common/client"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
)
func TestBuilder_Prepare(t *testing.T) {
type config map[string]interface{}
type regexMatchers map[string]string // map of regex : error message
tests := []struct {
name string
config config
validate func(Config)
wantErr bool
}{
{
name: "platform image to managed disk",
config: config{
"client_id": "123",
"client_secret": "456",
"subscription_id": "789",
"source": "credativ:Debian:9:latest",
"image_resource_id": "/subscriptions/789/resourceGroups/otherrgname/providers/Microsoft.Compute/images/MyDebianOSImage-{{timestamp}}",
"shared_image_destination": config{
"resource_group": "otherrgname",
"gallery_name": "myGallery",
"image_name": "imageName",
"image_version": "1.0.2",
},
},
validate: func(c Config) {
if c.OSDiskSizeGB != 0 {
t.Errorf("Expected OSDiskSizeGB to be 0, was %+v", c.OSDiskSizeGB)
}
if c.MountPartition != "1" {
t.Errorf("Expected MountPartition to be %s, but found %s", "1", c.MountPartition)
}
if c.OSDiskStorageAccountType != string(compute.PremiumLRS) {
t.Errorf("Expected OSDiskStorageAccountType to be %s, but found %s", string(compute.PremiumLRS), c.OSDiskStorageAccountType)
}
if c.OSDiskCacheType != string(compute.CachingTypesReadOnly) {
t.Errorf("Expected OSDiskCacheType to be %s, but found %s", string(compute.CachingTypesReadOnly), c.OSDiskCacheType)
}
if c.ImageHyperVGeneration != string(compute.V1) {
t.Errorf("Expected ImageHyperVGeneration to be %s, but found %s", string(compute.V1), c.ImageHyperVGeneration)
}
},
},
{
name: "disk to managed image, validate temp disk id expansion",
config: config{
"source": "/subscriptions/789/resourceGroups/testrg/providers/Microsoft.Compute/disks/diskname",
"image_resource_id": "/subscriptions/789/resourceGroups/otherrgname/providers/Microsoft.Compute/images/MyDebianOSImage-{{timestamp}}",
},
validate: func(c Config) {
prefix := "/subscriptions/testSubscriptionID/resourceGroups/testResourceGroup/providers/Microsoft.Compute/disks/PackerTemp-osdisk-"
if !strings.HasPrefix(c.TemporaryOSDiskID, prefix) {
t.Errorf("Expected TemporaryOSDiskID to start with %q, but got %q", prefix, c.TemporaryOSDiskID)
}
},
},
{
name: "disk to both managed image and shared image",
config: config{
"source": "/subscriptions/789/resourceGroups/testrg/providers/Microsoft.Compute/disks/diskname",
"image_resource_id": "/subscriptions/789/resourceGroups/otherrgname/providers/Microsoft.Compute/images/MyDebianOSImage-{{timestamp}}",
"shared_image_destination": config{
"resource_group": "rg",
"gallery_name": "galleryName",
"image_name": "imageName",
"image_version": "0.1.0",
},
},
},
{
name: "disk to both managed image and shared image with missing property",
config: config{
"source": "/subscriptions/789/resourceGroups/testrg/providers/Microsoft.Compute/disks/diskname",
"image_resource_id": "/subscriptions/789/resourceGroups/otherrgname/providers/Microsoft.Compute/images/MyDebianOSImage-{{timestamp}}",
"shared_image_destination": config{
"resource_group": "rg",
"gallery_name": "galleryName",
"image_version": "0.1.0",
},
},
wantErr: true,
},
{
name: "from shared image",
config: config{
"shared_image_destination": config{
"resource_group": "otherrgname",
"gallery_name": "myGallery",
"image_name": "imageName",
"image_version": "1.0.2",
},
"source": "/subscriptions/789/resourceGroups/testrg/providers/Microsoft.Compute/disks/diskname",
},
wantErr: false,
},
{
name: "err: no output",
config: config{
"source": "/subscriptions/789/resourceGroups/testrg/providers/Microsoft.Compute/disks/diskname",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
withMetadataStub(func() {
b := &Builder{}
_, _, err := b.Prepare(tt.config)
if (err != nil) != tt.wantErr {
t.Errorf("Builder.Prepare() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.validate != nil {
tt.validate(b.config)
}
})
})
}
}
func Test_buildsteps(t *testing.T) {
info := &client.ComputeInfo{
Location: "northpole",
Name: "unittestVM",
ResourceGroupName: "unittestResourceGroup",
SubscriptionID: "96854241-60c7-426d-9a27-3fdeec8957f4",
}
tests := []struct {
name string
config Config
verify func([]multistep.Step, *testing.T)
}{
{
name: "Source FromScrath creates empty disk",
config: Config{FromScratch: true},
verify: func(steps []multistep.Step, _ *testing.T) {
for _, s := range steps {
if s, ok := s.(*StepCreateNewDiskset); ok {
if s.SourceOSDiskResourceID == "" &&
s.SourcePlatformImage == nil {
return
}
t.Errorf("found misconfigured StepCreateNewDisk: %+v", s)
}
}
t.Error("did not find a StepCreateNewDisk")
}},
{
name: "Source Platform image disk creation",
config: Config{Source: "publisher:offer:sku:version", sourceType: sourcePlatformImage},
verify: func(steps []multistep.Step, _ *testing.T) {
for _, s := range steps {
if s, ok := s.(*StepCreateNewDiskset); ok {
if s.SourceOSDiskResourceID == "" &&
s.SourcePlatformImage != nil &&
s.SourcePlatformImage.Publisher == "publisher" {
return
}
t.Errorf("found misconfigured StepCreateNewDisk: %+v", s)
}
}
t.Error("did not find a StepCreateNewDisk")
}},
{
name: "Source Platform image with version latest adds StepResolvePlatformImageVersion",
config: Config{Source: "publisher:offer:sku:latest", sourceType: sourcePlatformImage},
verify: func(steps []multistep.Step, _ *testing.T) {
for _, s := range steps {
if s, ok := s.(*StepResolvePlatformImageVersion); ok {
if s.PlatformImage != nil &&
s.Location == info.Location {
return
}
t.Errorf("found misconfigured StepResolvePlatformImageVersion: %+v", s)
}
}
t.Error("did not find a StepResolvePlatformImageVersion")
}},
{
name: "Source Disk adds correct disk creation",
config: Config{Source: "diskresourceid", sourceType: sourceDisk},
verify: func(steps []multistep.Step, _ *testing.T) {
for _, s := range steps {
if s, ok := s.(*StepCreateNewDiskset); ok {
if s.SourceOSDiskResourceID == "diskresourceid" &&
s.SourcePlatformImage == nil {
return
}
t.Errorf("found misconfigured StepCreateNewDisk: %+v", s)
}
}
t.Error("did not find a StepCreateNewDisk")
}},
{
name: "Source disk adds StepVerifySourceDisk",
config: Config{Source: "diskresourceid", sourceType: sourceDisk},
verify: func(steps []multistep.Step, _ *testing.T) {
for _, s := range steps {
if s, ok := s.(*StepVerifySourceDisk); ok {
if s.SourceDiskResourceID == "diskresourceid" &&
s.Location == info.Location {
return
}
t.Errorf("found misconfigured StepVerifySourceDisk: %+v", s)
}
}
t.Error("did not find a StepVerifySourceDisk")
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
withMetadataStub(func() { // ensure that values are taken from info, instead of retrieved again
got := buildsteps(tt.config, info)
tt.verify(got, t)
})
})
}
}

View File

@ -0,0 +1,6 @@
package chroot
const (
stateBagKey_Diskset = "diskset"
stateBagKey_Snapshotset = "snapshotset"
)

View File

@ -0,0 +1,190 @@
package chroot
import (
"context"
"errors"
"log"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
"github.com/hashicorp/packer/builder/azure/common/client"
)
type DiskAttacher interface {
AttachDisk(ctx context.Context, disk string) (lun int32, err error)
WaitForDevice(ctx context.Context, i int32) (device string, err error)
DetachDisk(ctx context.Context, disk string) (err error)
WaitForDetach(ctx context.Context, diskID string) error
}
var NewDiskAttacher = func(azureClient client.AzureClientSet) DiskAttacher {
return &diskAttacher{
azcli: azureClient,
}
}
type diskAttacher struct {
azcli client.AzureClientSet
vm *client.ComputeInfo // store info about this VM so that we don't have to ask metadata service on every call
}
var DiskNotFoundError = errors.New("Disk not found")
func (da *diskAttacher) DetachDisk(ctx context.Context, diskID string) error {
log.Println("Fetching list of disks currently attached to VM")
currentDisks, err := da.getDisks(ctx)
if err != nil {
return err
}
log.Printf("Removing %q from list of disks currently attached to VM", diskID)
newDisks := []compute.DataDisk{}
for _, disk := range currentDisks {
if disk.ManagedDisk != nil &&
!strings.EqualFold(to.String(disk.ManagedDisk.ID), diskID) {
newDisks = append(newDisks, disk)
}
}
if len(currentDisks) == len(newDisks) {
return DiskNotFoundError
}
log.Println("Updating new list of disks attached to VM")
err = da.setDisks(ctx, newDisks)
if err != nil {
return err
}
return nil
}
func (da *diskAttacher) WaitForDetach(ctx context.Context, diskID string) error {
for { // loop until disk is not attached, timeout or error
list, err := da.getDisks(ctx)
if err != nil {
return err
}
if findDiskInList(list, diskID) == nil {
log.Println("Disk is no longer in VM model, assuming detached")
return nil
}
select {
case <-time.After(time.Second): //continue
case <-ctx.Done():
return ctx.Err()
}
}
}
func (da *diskAttacher) AttachDisk(ctx context.Context, diskID string) (int32, error) {
dataDisks, err := da.getDisks(ctx)
if err != nil {
return -1, err
}
// check to see if disk is already attached, remember lun if found
if disk := findDiskInList(dataDisks, diskID); disk != nil {
// disk is already attached, just take this lun
if disk.Lun == nil {
return -1, errors.New("disk is attached, but lun was not set in VM model (possibly an error in the Azure APIs)")
}
return to.Int32(disk.Lun), nil
}
// disk was not found on VM, go and actually attach it
var lun int32 = -1
findFreeLun:
for lun = 0; lun < 64; lun++ {
for _, v := range dataDisks {
if to.Int32(v.Lun) == lun {
continue findFreeLun
}
}
// no datadisk is using this lun
break
}
// append new data disk to collection
dataDisks = append(dataDisks, compute.DataDisk{
CreateOption: compute.DiskCreateOptionTypesAttach,
ManagedDisk: &compute.ManagedDiskParameters{
ID: to.StringPtr(diskID),
},
Lun: to.Int32Ptr(lun),
})
// prepare resource object for update operation
err = da.setDisks(ctx, dataDisks)
if err != nil {
return -1, err
}
return lun, nil
}
func (da *diskAttacher) getThisVM(ctx context.Context) (compute.VirtualMachine, error) {
// getting resource info for this VM
if da.vm == nil {
vm, err := da.azcli.MetadataClient().GetComputeInfo()
if err != nil {
return compute.VirtualMachine{}, err
}
da.vm = vm
}
// retrieve actual VM
vmResource, err := da.azcli.VirtualMachinesClient().Get(ctx, da.vm.ResourceGroupName, da.vm.Name, "")
if err != nil {
return compute.VirtualMachine{}, err
}
if vmResource.StorageProfile == nil {
return compute.VirtualMachine{}, errors.New("properties.storageProfile is not set on VM, this is unexpected")
}
return vmResource, nil
}
func (da diskAttacher) getDisks(ctx context.Context) ([]compute.DataDisk, error) {
vmResource, err := da.getThisVM(ctx)
if err != nil {
return []compute.DataDisk{}, err
}
return *vmResource.StorageProfile.DataDisks, nil
}
func (da diskAttacher) setDisks(ctx context.Context, disks []compute.DataDisk) error {
vmResource, err := da.getThisVM(ctx)
if err != nil {
return err
}
id, err := azure.ParseResourceID(to.String(vmResource.ID))
if err != nil {
return err
}
vmResource.StorageProfile.DataDisks = &disks
vmResource.Resources = nil
// update the VM resource, attach disk
_, err = da.azcli.VirtualMachinesClient().CreateOrUpdate(ctx, id.ResourceGroup, id.ResourceName, vmResource)
return err
}
func findDiskInList(list []compute.DataDisk, diskID string) *compute.DataDisk {
for _, disk := range list {
if disk.ManagedDisk != nil &&
strings.EqualFold(to.String(disk.ManagedDisk.ID), diskID) {
return &disk
}
}
return nil
}

View File

@ -0,0 +1,54 @@
package chroot
import (
"bufio"
"bytes"
"context"
"fmt"
"os/exec"
"regexp"
"strings"
"time"
)
func (da diskAttacher) WaitForDevice(ctx context.Context, lun int32) (device string, err error) {
// This builder will always be running in Azure, where data disks show up
// on scbus5 target 0. The camcontrol command always outputs LUNs in
// unpadded hexadecimal format.
regexStr := fmt.Sprintf(`at scbus5 target 0 lun %x \(.*?da([\d]+)`, lun)
devRegex, err := regexp.Compile(regexStr)
if err != nil {
return "", err
}
for {
cmd := exec.Command("camcontrol", "devlist")
var out bytes.Buffer
cmd.Stdout = &out
err = cmd.Run()
if err != nil {
return "", err
}
outString := out.String()
scanner := bufio.NewScanner(strings.NewReader(outString))
for scanner.Scan() {
line := scanner.Text()
// Check if this is the correct bus, target, and LUN.
if matches := devRegex.FindStringSubmatch(line); matches != nil {
// If this function immediately returns, devfs won't have
// created the device yet.
time.Sleep(1000 * time.Millisecond)
return fmt.Sprintf("/dev/da%s", matches[1]), nil
}
}
if err = scanner.Err(); err != nil {
return "", err
}
select {
case <-time.After(100 * time.Millisecond):
// continue
case <-ctx.Done():
return "", ctx.Err()
}
}
}

View File

@ -0,0 +1,36 @@
package chroot
import (
"context"
"fmt"
"os"
"path/filepath"
"syscall"
"time"
)
func diskPathForLun(lun int32) string {
return fmt.Sprintf("/dev/disk/azure/scsi1/lun%d", lun)
}
func (da diskAttacher) WaitForDevice(ctx context.Context, lun int32) (device string, err error) {
path := diskPathForLun(lun)
for {
link, err := os.Readlink(path)
if err == nil {
return filepath.Abs("/dev/disk/azure/scsi1/" + link)
} else if err != os.ErrNotExist {
if pe, ok := err.(*os.PathError); ok && pe.Err != syscall.ENOENT {
return "", err
}
}
select {
case <-time.After(100 * time.Millisecond):
// continue
case <-ctx.Done():
return "", ctx.Err()
}
}
}

View File

@ -0,0 +1,11 @@
// +build !linux,!freebsd
package chroot
import (
"context"
)
func (da diskAttacher) WaitForDevice(ctx context.Context, lun int32) (device string, err error) {
panic("The azure-chroot builder does not work on this platform.")
}

View File

@ -0,0 +1,87 @@
package chroot
import (
"context"
"testing"
"github.com/Azure/go-autorest/autorest/to"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
"github.com/hashicorp/packer/builder/azure/common/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
testvm = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/Microsoft.Compute/virtualMachines/testVM"
testdisk = "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/testGroup2/Microsoft.Compute/disks/testDisk"
)
// Tests assume current machine is capable of running chroot builder (i.e. an Azure VM)
func Test_DiskAttacherAttachesDiskToVM(t *testing.T) {
azcli, err := client.GetTestClientSet(t) // integration test
require.Nil(t, err)
da := NewDiskAttacher(azcli)
testDiskName := t.Name()
vm, err := azcli.MetadataClient().GetComputeInfo()
require.Nil(t, err, "Test needs to run on an Azure VM, unable to retrieve VM information")
t.Log("Creating new disk '", testDiskName, "' in ", vm.ResourceGroupName)
disk, err := azcli.DisksClient().Get(context.TODO(), vm.ResourceGroupName, testDiskName)
if err == nil {
t.Log("Disk already exists")
if disk.DiskState == compute.Attached {
t.Log("Disk is attached, assuming to this machine, trying to detach")
err = da.DetachDisk(context.TODO(), to.String(disk.ID))
require.Nil(t, err)
}
t.Log("Deleting disk")
result, err := azcli.DisksClient().Delete(context.TODO(), vm.ResourceGroupName, testDiskName)
require.Nil(t, err)
err = result.WaitForCompletionRef(context.TODO(), azcli.PollClient())
require.Nil(t, err)
}
t.Log("Creating disk")
r, err := azcli.DisksClient().CreateOrUpdate(context.TODO(), vm.ResourceGroupName, testDiskName, compute.Disk{
Location: to.StringPtr(vm.Location),
Sku: &compute.DiskSku{
Name: compute.StandardLRS,
},
DiskProperties: &compute.DiskProperties{
DiskSizeGB: to.Int32Ptr(30),
CreationData: &compute.CreationData{CreateOption: compute.Empty},
},
})
require.Nil(t, err)
err = r.WaitForCompletionRef(context.TODO(), azcli.PollClient())
require.Nil(t, err)
t.Log("Retrieving disk properties")
d, err := azcli.DisksClient().Get(context.TODO(), vm.ResourceGroupName, testDiskName)
require.Nil(t, err)
assert.NotNil(t, d)
t.Log("Attaching disk")
lun, err := da.AttachDisk(context.TODO(), to.String(d.ID))
assert.Nil(t, err)
t.Log("Waiting for device")
dev, err := da.WaitForDevice(context.TODO(), lun)
assert.Nil(t, err)
t.Log("Device path:", dev)
t.Log("Detaching disk")
err = da.DetachDisk(context.TODO(), to.String(d.ID))
require.Nil(t, err)
t.Log("Deleting disk")
result, err := azcli.DisksClient().Delete(context.TODO(), vm.ResourceGroupName, testDiskName)
if err == nil {
err = result.WaitForCompletionRef(context.TODO(), azcli.PollClient())
}
require.Nil(t, err)
}

View File

@ -0,0 +1,23 @@
package chroot
import "github.com/hashicorp/packer/builder/azure/common/client"
// Diskset represents all of the disks or snapshots associated with an image.
// It maps lun to resource ids. The OS disk is stored with lun=-1.
type Diskset map[int32]client.Resource
// OS return the OS disk resource ID or nil if it is not assigned
func (ds Diskset) OS() *client.Resource {
if r, ok := ds[-1]; ok {
return &r
}
return nil
}
// Data return the data disk resource ID or nil if it is not assigned
func (ds Diskset) Data(lun int32) *client.Resource {
if r, ok := ds[lun]; ok {
return &r
}
return nil
}

View File

@ -0,0 +1,16 @@
package chroot
import "github.com/hashicorp/packer/builder/azure/common/client"
// diskset easily creates a diskset for testing
func diskset(ids ...string) Diskset {
diskset := make(Diskset)
for i, id := range ids {
r, err := client.ParseResourceID(id)
if err != nil {
panic(err)
}
diskset[int32(i-1)] = r
}
return diskset
}

View File

@ -0,0 +1,18 @@
package chroot
import "github.com/hashicorp/packer/builder/azure/common/client"
func withMetadataStub(f func()) {
mdc := client.DefaultMetadataClient
defer func() { client.DefaultMetadataClient = mdc }()
client.DefaultMetadataClient = client.MetadataClientStub{
ComputeInfo: client.ComputeInfo{
SubscriptionID: "testSubscriptionID",
ResourceGroupName: "testResourceGroup",
Name: "testVM",
Location: "testLocation",
},
}
f()
}

View File

@ -0,0 +1,19 @@
package chroot
import (
"io/ioutil"
"strings"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
// testUI returns a test ui plus a function to retrieve the errors written to the ui
func testUI() (packersdk.Ui, func() string) {
errorBuffer := &strings.Builder{}
ui := &packersdk.BasicUi{
Reader: strings.NewReader(""),
Writer: ioutil.Discard,
ErrorWriter: errorBuffer,
}
return ui, errorBuffer.String
}

View File

@ -0,0 +1,71 @@
//go:generate packer-sdc struct-markdown
//go:generate packer-sdc mapstructure-to-hcl2 -type SharedImageGalleryDestination,TargetRegion
package chroot
import (
"fmt"
"regexp"
)
// SharedImageGalleryDestination models an image version in a Shared
// Image Gallery that can be used as a destination.
type SharedImageGalleryDestination struct {
ResourceGroup string `mapstructure:"resource_group" required:"true"`
GalleryName string `mapstructure:"gallery_name" required:"true"`
ImageName string `mapstructure:"image_name" required:"true"`
ImageVersion string `mapstructure:"image_version" required:"true"`
TargetRegions []TargetRegion `mapstructure:"target_regions"`
ExcludeFromLatest bool `mapstructure:"exclude_from_latest"`
ExcludeFromLatestTypo bool `mapstructure:"exlude_from_latest" undocumented:"true"`
}
// TargetRegion describes a region where the shared image should be replicated
type TargetRegion struct {
// Name of the Azure region
Name string `mapstructure:"name" required:"true"`
// Number of replicas in this region. Default: 1
ReplicaCount int32 `mapstructure:"replicas"`
// Storage account type: Standard_LRS or Standard_ZRS. Default: Standard_ZRS
StorageAccountType string `mapstructure:"storage_account_type"`
}
// ResourceID returns the resource ID string
func (sigd SharedImageGalleryDestination) ResourceID(subscriptionID string) string {
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/galleries/%s/images/%s/versions/%s",
subscriptionID,
sigd.ResourceGroup,
sigd.GalleryName,
sigd.ImageName,
sigd.ImageVersion)
}
// Validate validates that the values in the shared image are valid (without checking them on the network)
func (sigd *SharedImageGalleryDestination) Validate(prefix string) (errs []error, warns []string) {
if sigd.ResourceGroup == "" {
errs = append(errs, fmt.Errorf("%s.resource_group is required", prefix))
}
if sigd.GalleryName == "" {
errs = append(errs, fmt.Errorf("%s.gallery_name is required", prefix))
}
if sigd.ImageName == "" {
errs = append(errs, fmt.Errorf("%s.image_name is required", prefix))
}
if match, err := regexp.MatchString("^[0-9]+\\.[0-9]+\\.[0-9]+$", sigd.ImageVersion); !match {
if err != nil {
warns = append(warns, fmt.Sprintf("Error matching pattern for %s.image_version: %s (this is probably a bug)", prefix, err))
}
errs = append(errs, fmt.Errorf("%s.image_version should match '^[0-9]+\\.[0-9]+\\.[0-9]+$'", prefix))
}
if len(sigd.TargetRegions) == 0 {
warns = append(warns,
fmt.Sprintf("%s.target_regions is empty; image will only be available in the region of the gallery", prefix))
}
if sigd.ExcludeFromLatestTypo == true && sigd.ExcludeFromLatest == false {
warns = append(warns,
fmt.Sprintf("%s.exlude_from_latest is being deprecated, please use exclude_from_latest", prefix))
sigd.ExcludeFromLatest = sigd.ExcludeFromLatestTypo
}
return
}

View File

@ -0,0 +1,70 @@
// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT.
package chroot
import (
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
)
// FlatSharedImageGalleryDestination is an auto-generated flat version of SharedImageGalleryDestination.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatSharedImageGalleryDestination struct {
ResourceGroup *string `mapstructure:"resource_group" required:"true" cty:"resource_group" hcl:"resource_group"`
GalleryName *string `mapstructure:"gallery_name" required:"true" cty:"gallery_name" hcl:"gallery_name"`
ImageName *string `mapstructure:"image_name" required:"true" cty:"image_name" hcl:"image_name"`
ImageVersion *string `mapstructure:"image_version" required:"true" cty:"image_version" hcl:"image_version"`
TargetRegions []FlatTargetRegion `mapstructure:"target_regions" cty:"target_regions" hcl:"target_regions"`
ExcludeFromLatest *bool `mapstructure:"exclude_from_latest" cty:"exclude_from_latest" hcl:"exclude_from_latest"`
ExcludeFromLatestTypo *bool `mapstructure:"exlude_from_latest" undocumented:"true" cty:"exlude_from_latest" hcl:"exlude_from_latest"`
}
// FlatMapstructure returns a new FlatSharedImageGalleryDestination.
// FlatSharedImageGalleryDestination is an auto-generated flat version of SharedImageGalleryDestination.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*SharedImageGalleryDestination) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatSharedImageGalleryDestination)
}
// HCL2Spec returns the hcl spec of a SharedImageGalleryDestination.
// This spec is used by HCL to read the fields of SharedImageGalleryDestination.
// The decoded values from this spec will then be applied to a FlatSharedImageGalleryDestination.
func (*FlatSharedImageGalleryDestination) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"resource_group": &hcldec.AttrSpec{Name: "resource_group", Type: cty.String, Required: false},
"gallery_name": &hcldec.AttrSpec{Name: "gallery_name", Type: cty.String, Required: false},
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
"image_version": &hcldec.AttrSpec{Name: "image_version", Type: cty.String, Required: false},
"target_regions": &hcldec.BlockListSpec{TypeName: "target_regions", Nested: hcldec.ObjectSpec((*FlatTargetRegion)(nil).HCL2Spec())},
"exclude_from_latest": &hcldec.AttrSpec{Name: "exclude_from_latest", Type: cty.Bool, Required: false},
"exlude_from_latest": &hcldec.AttrSpec{Name: "exlude_from_latest", Type: cty.Bool, Required: false},
}
return s
}
// FlatTargetRegion is an auto-generated flat version of TargetRegion.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatTargetRegion struct {
Name *string `mapstructure:"name" required:"true" cty:"name" hcl:"name"`
ReplicaCount *int32 `mapstructure:"replicas" cty:"replicas" hcl:"replicas"`
StorageAccountType *string `mapstructure:"storage_account_type" cty:"storage_account_type" hcl:"storage_account_type"`
}
// FlatMapstructure returns a new FlatTargetRegion.
// FlatTargetRegion is an auto-generated flat version of TargetRegion.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*TargetRegion) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatTargetRegion)
}
// HCL2Spec returns the hcl spec of a TargetRegion.
// This spec is used by HCL to read the fields of TargetRegion.
// The decoded values from this spec will then be applied to a FlatTargetRegion.
func (*FlatTargetRegion) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"name": &hcldec.AttrSpec{Name: "name", Type: cty.String, Required: false},
"replicas": &hcldec.AttrSpec{Name: "replicas", Type: cty.Number, Required: false},
"storage_account_type": &hcldec.AttrSpec{Name: "storage_account_type", Type: cty.String, Required: false},
}
return s
}

View File

@ -0,0 +1,158 @@
package chroot
import (
"reflect"
"strings"
"testing"
)
func TestSharedImageGalleryDestination_ResourceID(t *testing.T) {
sigd := SharedImageGalleryDestination{
ResourceGroup: "ResourceGroup",
GalleryName: "GalleryName",
ImageName: "ImageName",
ImageVersion: "ImageVersion",
}
want := "/subscriptions/SubscriptionID/resourceGroups/ResourceGroup/providers/Microsoft.Compute/galleries/GalleryName/images/ImageName/versions/ImageVersion"
if got := sigd.ResourceID("SubscriptionID"); !strings.EqualFold(got, want) {
t.Errorf("SharedImageGalleryDestination.ResourceID() = %v, want %v", got, want)
}
}
func TestSharedImageGalleryDestination_Validate(t *testing.T) {
type fields struct {
ResourceGroup string
GalleryName string
ImageName string
ImageVersion string
TargetRegions []TargetRegion
ExcludeFromLatest bool
ExcludeFromLatestTypo bool
}
tests := []struct {
name string
fields fields
wantErrs []string
wantWarns []string
}{
{
name: "complete",
fields: fields{
ResourceGroup: "ResourceGroup",
GalleryName: "GalleryName",
ImageName: "ImageName",
ImageVersion: "0.1.2",
TargetRegions: []TargetRegion{
TargetRegion{
Name: "region1",
ReplicaCount: 5,
StorageAccountType: "Standard_ZRS",
},
TargetRegion{
Name: "region2",
ReplicaCount: 3,
StorageAccountType: "Standard_LRS",
},
},
ExcludeFromLatest: true,
},
},
{
name: "warn if target regions not specified",
fields: fields{
ResourceGroup: "ResourceGroup",
GalleryName: "GalleryName",
ImageName: "ImageName",
ImageVersion: "0.1.2",
},
wantWarns: []string{"sigdest.target_regions is empty; image will only be available in the region of the gallery"},
},
{
name: "warn if using exlude_from_latest",
fields: fields{
ResourceGroup: "ResourceGroup",
GalleryName: "GalleryName",
ImageName: "ImageName",
ImageVersion: "0.1.2",
TargetRegions: []TargetRegion{
TargetRegion{
Name: "region1",
ReplicaCount: 5,
StorageAccountType: "Standard_ZRS",
},
TargetRegion{
Name: "region2",
ReplicaCount: 3,
StorageAccountType: "Standard_LRS",
},
},
ExcludeFromLatestTypo: true,
},
wantWarns: []string{"sigdest.exlude_from_latest is being deprecated, please use exclude_from_latest"},
},
{
name: "version format",
wantErrs: []string{
"sigdest.image_version should match '^[0-9]+\\.[0-9]+\\.[0-9]+$'",
},
fields: fields{
ResourceGroup: "ResourceGroup",
GalleryName: "GalleryName",
ImageName: "ImageName",
ImageVersion: "0.1.2alpha",
TargetRegions: []TargetRegion{
TargetRegion{
Name: "region1",
ReplicaCount: 5,
StorageAccountType: "Standard_ZRS",
},
TargetRegion{
Name: "region2",
ReplicaCount: 3,
StorageAccountType: "Standard_LRS",
},
},
ExcludeFromLatest: true,
},
},
{
name: "required fields",
wantErrs: []string{
"sigdest.resource_group is required",
"sigdest.gallery_name is required",
"sigdest.image_name is required",
"sigdest.image_version should match '^[0-9]+\\.[0-9]+\\.[0-9]+$'",
},
wantWarns: []string{"sigdest.target_regions is empty; image will only be available in the region of the gallery"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sigd := &SharedImageGalleryDestination{
ResourceGroup: tt.fields.ResourceGroup,
GalleryName: tt.fields.GalleryName,
ImageName: tt.fields.ImageName,
ImageVersion: tt.fields.ImageVersion,
TargetRegions: tt.fields.TargetRegions,
ExcludeFromLatest: tt.fields.ExcludeFromLatest,
ExcludeFromLatestTypo: tt.fields.ExcludeFromLatestTypo,
}
gotErrs, gotWarns := sigd.Validate("sigdest")
var gotStrErrs []string
if gotErrs != nil {
gotStrErrs = make([]string, len(gotErrs))
for i, e := range gotErrs {
gotStrErrs[i] = e.Error()
}
}
if !reflect.DeepEqual(gotStrErrs, tt.wantErrs) {
t.Errorf("SharedImageGalleryDestination.Validate() gotErrs = %q, want %q", gotStrErrs, tt.wantErrs)
}
if !reflect.DeepEqual(gotWarns, tt.wantWarns) {
t.Errorf("SharedImageGalleryDestination.Validate() gotWarns = %q, want %q", gotWarns, tt.wantWarns)
}
})
}
}

View File

@ -0,0 +1,85 @@
package chroot
import (
"context"
"fmt"
"log"
"time"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/client"
)
var _ multistep.Step = &StepAttachDisk{}
type StepAttachDisk struct {
attached bool
}
func (s *StepAttachDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
azcli := state.Get("azureclient").(client.AzureClientSet)
ui := state.Get("ui").(packersdk.Ui)
diskset := state.Get(stateBagKey_Diskset).(Diskset)
diskResourceID := diskset.OS().String()
ui.Say(fmt.Sprintf("Attaching disk '%s'", diskResourceID))
da := NewDiskAttacher(azcli)
lun, err := da.AttachDisk(ctx, diskResourceID)
if err != nil {
log.Printf("StepAttachDisk.Run: error: %+v", err)
err := fmt.Errorf(
"error attaching disk '%s': %v", diskResourceID, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Disk attached, waiting for device to show up")
ctx, cancel := context.WithTimeout(ctx, time.Minute*3) // in case is not configured correctly
defer cancel()
device, err := da.WaitForDevice(ctx, lun)
if err != nil {
log.Printf("StepAttachDisk.Run: error: %+v", err)
err := fmt.Errorf(
"error attaching disk '%s': %v", diskResourceID, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("Disk available at %q", device))
s.attached = true
state.Put("device", device)
state.Put("attach_cleanup", s)
return multistep.ActionContinue
}
func (s *StepAttachDisk) Cleanup(state multistep.StateBag) {
ui := state.Get("ui").(packersdk.Ui)
if err := s.CleanupFunc(state); err != nil {
ui.Error(err.Error())
}
}
func (s *StepAttachDisk) CleanupFunc(state multistep.StateBag) error {
if s.attached {
azcli := state.Get("azureclient").(client.AzureClientSet)
ui := state.Get("ui").(packersdk.Ui)
diskset := state.Get(stateBagKey_Diskset).(Diskset)
diskResourceID := diskset.OS().String()
ui.Say(fmt.Sprintf("Detaching disk '%s'", diskResourceID))
da := NewDiskAttacher(azcli)
err := da.DetachDisk(context.Background(), diskResourceID)
if err != nil {
return fmt.Errorf("error detaching %q: %v", diskResourceID, err)
}
s.attached = false
}
return nil
}

View File

@ -0,0 +1,131 @@
package chroot
import (
"context"
"errors"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
"github.com/Azure/go-autorest/autorest"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/client"
)
func TestStepAttachDisk_Run(t *testing.T) {
type fields struct {
GetDiskResponseCode int
GetDiskResponseBody string
attachError error
waitForDeviceError error
}
tests := []struct {
name string
fields fields
want multistep.StepAction
}{
{
name: "HappyPath",
want: multistep.ActionContinue,
},
{
name: "AttachError",
fields: fields{
attachError: errors.New("unit test"),
},
want: multistep.ActionHalt,
},
{
name: "WaitForDeviceError",
fields: fields{
waitForDeviceError: errors.New("unit test"),
},
want: multistep.ActionHalt,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &StepAttachDisk{}
NewDiskAttacher = func(azcli client.AzureClientSet) DiskAttacher {
return &fakeDiskAttacher{
attachError: tt.fields.attachError,
waitForDeviceError: tt.fields.waitForDeviceError,
}
}
dm := compute.NewDisksClient("subscriptionId")
dm.Sender = autorest.SenderFunc(func(r *http.Request) (*http.Response, error) {
return &http.Response{
Request: r,
Body: ioutil.NopCloser(strings.NewReader(tt.fields.GetDiskResponseBody)),
StatusCode: tt.fields.GetDiskResponseCode,
}, nil
})
errorBuffer := &strings.Builder{}
ui := &packersdk.BasicUi{
Reader: strings.NewReader(""),
Writer: ioutil.Discard,
ErrorWriter: errorBuffer,
}
state := new(multistep.BasicStateBag)
state.Put("azureclient", &client.AzureClientSetMock{})
state.Put("ui", ui)
state.Put(stateBagKey_Diskset, diskset("/subscriptions/12345/resourceGroups/group1/providers/Microsoft.Compute/disks/disk1"))
got := s.Run(context.TODO(), state)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("StepAttachDisk.Run() = %v, want %v", got, tt.want)
}
if got == multistep.ActionHalt {
if _, ok := state.GetOk("error"); !ok {
t.Fatal("Expected 'error' to be set in statebag after failure")
}
}
})
}
}
type fakeDiskAttacher struct {
attachError error
waitForDeviceError error
}
var _ DiskAttacher = &fakeDiskAttacher{}
func (da *fakeDiskAttacher) AttachDisk(ctx context.Context, disk string) (lun int32, err error) {
if da.attachError != nil {
return 0, da.attachError
}
return 3, nil
}
func (da *fakeDiskAttacher) DiskPathForLun(lun int32) string {
panic("not implemented")
}
func (da *fakeDiskAttacher) WaitForDevice(ctx context.Context, lun int32) (device string, err error) {
if da.waitForDeviceError != nil {
return "", da.waitForDeviceError
}
if lun == 3 {
return "/dev/sdq", nil
}
panic("expected lun==3")
}
func (da *fakeDiskAttacher) DetachDisk(ctx context.Context, disk string) (err error) {
panic("not implemented")
}
func (da *fakeDiskAttacher) WaitForDetach(ctx context.Context, diskID string) error {
panic("not implemented")
}

View File

@ -0,0 +1,112 @@
package chroot
import (
"context"
"fmt"
"log"
"sort"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/builder/azure/common/client"
)
var _ multistep.Step = &StepCreateImage{}
type StepCreateImage struct {
ImageResourceID string
ImageOSState string
OSDiskStorageAccountType string
OSDiskCacheType string
DataDiskStorageAccountType string
DataDiskCacheType string
Location string
}
func (s *StepCreateImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
azcli := state.Get("azureclient").(client.AzureClientSet)
ui := state.Get("ui").(packersdk.Ui)
diskset := state.Get(stateBagKey_Diskset).(Diskset)
diskResourceID := diskset.OS().String()
ui.Say(fmt.Sprintf("Creating image %s\n using %s for os disk.",
s.ImageResourceID,
diskResourceID))
imageResource, err := azure.ParseResourceID(s.ImageResourceID)
if err != nil {
log.Printf("StepCreateImage.Run: error: %+v", err)
err := fmt.Errorf(
"error parsing image resource id '%s': %v", s.ImageResourceID, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
image := compute.Image{
Location: to.StringPtr(s.Location),
ImageProperties: &compute.ImageProperties{
StorageProfile: &compute.ImageStorageProfile{
OsDisk: &compute.ImageOSDisk{
OsState: compute.OperatingSystemStateTypes(s.ImageOSState),
OsType: compute.Linux,
ManagedDisk: &compute.SubResource{
ID: &diskResourceID,
},
StorageAccountType: compute.StorageAccountTypes(s.OSDiskStorageAccountType),
Caching: compute.CachingTypes(s.OSDiskCacheType),
},
// DataDisks: nil,
// ZoneResilient: nil,
},
},
// Tags: nil,
}
var datadisks []compute.ImageDataDisk
for lun, resource := range diskset {
if lun != -1 {
ui.Say(fmt.Sprintf(" using %q for data disk (lun %d).", resource, lun))
datadisks = append(datadisks, compute.ImageDataDisk{
Lun: to.Int32Ptr(lun),
ManagedDisk: &compute.SubResource{ID: to.StringPtr(resource.String())},
StorageAccountType: compute.StorageAccountTypes(s.DataDiskStorageAccountType),
Caching: compute.CachingTypes(s.DataDiskCacheType),
})
}
}
if datadisks != nil {
sort.Slice(datadisks, func(i, j int) bool {
return *datadisks[i].Lun < *datadisks[j].Lun
})
image.ImageProperties.StorageProfile.DataDisks = &datadisks
}
f, err := azcli.ImagesClient().CreateOrUpdate(
ctx,
imageResource.ResourceGroup,
imageResource.ResourceName,
image)
if err == nil {
log.Println("Image creation in process...")
err = f.WaitForCompletionRef(ctx, azcli.PollClient())
}
if err != nil {
log.Printf("StepCreateImage.Run: error: %+v", err)
err := fmt.Errorf(
"error creating image '%s': %v", s.ImageResourceID, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Image creation complete: %s", f.Status())
return multistep.ActionContinue
}
func (*StepCreateImage) Cleanup(bag multistep.StateBag) {} // this is the final artifact, don't delete

Some files were not shown because too many files have changed in this diff Show More