diff --git a/builder/azure/arm/azure_client.go b/builder/azure/arm/azure_client.go index 08dd95eae..c4d8b544d 100644 --- a/builder/azure/arm/azure_client.go +++ b/builder/azure/arm/azure_client.go @@ -48,6 +48,7 @@ type AzureClient struct { InspectorMaxLength int Template *CaptureTemplate LastError azureErrorResponse + VaultClientDelete common.VaultClient } func getCaptureResponse(body string) *CaptureTemplate { @@ -209,6 +210,20 @@ func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string azureClient.VaultClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient)) azureClient.VaultClient.UserAgent += packerUserAgent + // TODO(boumenot) - SDK still does not have a full KeyVault client. + // There are two ways that KeyVault has to be accessed, and each one has their own SPN. An authenticated SPN + // is tied to the URL, and the URL associated with getting the secret is different than the URL + // associated with deleting the KeyVault. As a result, I need to have *two* different clients to + // access KeyVault. I did not want to split it into two separate files, so I am starting with this. + // + // I do not like this implementation. It is getting long in the tooth, and should be re-examined now + // that we have a "working" solution. + azureClient.VaultClientDelete = common.NewVaultClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID) + azureClient.VaultClientDelete.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken) + azureClient.VaultClientDelete.RequestInspector = withInspection(maxlen) + azureClient.VaultClientDelete.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient)) + azureClient.VaultClientDelete.UserAgent += packerUserAgent + // If this is a managed disk build, this should be ignored. if resourceGroupName != "" && storageAccountName != "" { accountKeys, err := azureClient.AccountsClient.ListKeys(resourceGroupName, storageAccountName) diff --git a/builder/azure/arm/builder.go b/builder/azure/arm/builder.go index 3874cdb05..c6b8bcbe1 100644 --- a/builder/azure/arm/builder.go +++ b/builder/azure/arm/builder.go @@ -127,11 +127,13 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe b.setImageParameters(b.stateBag) var steps []multistep.Step + deploymentName := b.stateBag.Get(constants.ArmDeploymentName).(string) + if b.config.OSType == constants.Target_Linux { steps = []multistep.Step{ NewStepCreateResourceGroup(azureClient, ui), NewStepValidateTemplate(azureClient, ui, b.config, GetVirtualMachineDeployment), - NewStepDeployTemplate(azureClient, ui, b.config, GetVirtualMachineDeployment), + NewStepDeployTemplate(azureClient, ui, b.config, deploymentName, GetVirtualMachineDeployment), NewStepGetIPAddress(azureClient, ui, endpointConnectType), &communicator.StepConnectSSH{ Config: &b.config.Comm, @@ -146,14 +148,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe NewStepDeleteOSDisk(azureClient, ui), } } else if b.config.OSType == constants.Target_Windows { + keyVaultDeploymentName := b.stateBag.Get(constants.ArmKeyVaultDeploymentName).(string) steps = []multistep.Step{ NewStepCreateResourceGroup(azureClient, ui), NewStepValidateTemplate(azureClient, ui, b.config, GetKeyVaultDeployment), - NewStepDeployTemplate(azureClient, ui, b.config, GetKeyVaultDeployment), + NewStepDeployTemplate(azureClient, ui, b.config, keyVaultDeploymentName, GetKeyVaultDeployment), NewStepGetCertificate(azureClient, ui), NewStepSetCertificate(b.config, ui), NewStepValidateTemplate(azureClient, ui, b.config, GetVirtualMachineDeployment), - NewStepDeployTemplate(azureClient, ui, b.config, GetVirtualMachineDeployment), + NewStepDeployTemplate(azureClient, ui, b.config, deploymentName, GetVirtualMachineDeployment), NewStepGetIPAddress(azureClient, ui, endpointConnectType), &communicator.StepConnectWinRM{ Config: &b.config.Comm, @@ -283,6 +286,9 @@ func (b *Builder) configureStateBag(stateBag multistep.StateBag) { stateBag.Put(constants.ArmTags, &b.config.AzureTags) stateBag.Put(constants.ArmComputeName, b.config.tmpComputeName) stateBag.Put(constants.ArmDeploymentName, b.config.tmpDeploymentName) + if b.config.OSType == constants.Target_Windows { + stateBag.Put(constants.ArmKeyVaultDeploymentName, fmt.Sprintf("kv%s", b.config.tmpDeploymentName)) + } stateBag.Put(constants.ArmKeyVaultName, b.config.tmpKeyVaultName) stateBag.Put(constants.ArmLocation, b.config.Location) stateBag.Put(constants.ArmNicName, DefaultNicName) diff --git a/builder/azure/arm/step_delete_resource_group.go b/builder/azure/arm/step_delete_resource_group.go index 147a33349..c3db6de0a 100644 --- a/builder/azure/arm/step_delete_resource_group.go +++ b/builder/azure/arm/step_delete_resource_group.go @@ -37,54 +37,19 @@ func (s *StepDeleteResourceGroup) deleteResourceGroup(state multistep.StateBag, if state.Get(constants.ArmIsExistingResourceGroup).(bool) { s.say("\nThe resource group was not created by Packer, only deleting individual resources ...") var deploymentName = state.Get(constants.ArmDeploymentName).(string) - if deploymentName != "" { - maxResources := int32(maxResourcesToDelete) - deploymentOperations, err := s.client.DeploymentOperationsClient.List(resourceGroupName, deploymentName, &maxResources) + err = s.deleteDeploymentResources(deploymentName, resourceGroupName) + if err != nil { + return err + } + + if keyVaultDeploymentName, ok := state.GetOk(constants.ArmKeyVaultDeploymentName); ok { + err = s.deleteDeploymentResources(keyVaultDeploymentName.(string), resourceGroupName) if err != nil { - s.say(fmt.Sprintf("Error deleting resources. Please delete them manually.\n\n"+ - "Name: %s\n"+ - "Error: %s", resourceGroupName, err)) - s.error(err) - } - for _, deploymentOperation := range *deploymentOperations.Value { - // Sometimes an empty operation is added to the list by Azure - if deploymentOperation.Properties.TargetResource == nil { - continue - } - s.say(fmt.Sprintf(" -> %s : '%s'", - *deploymentOperation.Properties.TargetResource.ResourceType, - *deploymentOperation.Properties.TargetResource.ResourceName)) - var networkDeleteFunction func(string, string, <-chan struct{}) (<-chan autorest.Response, <-chan error) - switch *deploymentOperation.Properties.TargetResource.ResourceType { - case "Microsoft.Compute/virtualMachines": - _, errChan := s.client.VirtualMachinesClient.Delete(resourceGroupName, *deploymentOperation.Properties.TargetResource.ResourceName, nil) - err := <-errChan - if err != nil { - s.say(fmt.Sprintf("Error deleting resource. Please delete manually.\n\n"+ - "Name: %s\n"+ - "Error: %s", *deploymentOperation.Properties.TargetResource.ResourceName, err.Error())) - s.error(err) - } - case "Microsoft.Network/networkInterfaces": - networkDeleteFunction = s.client.InterfacesClient.Delete - case "Microsoft.Network/virtualNetworks": - networkDeleteFunction = s.client.VirtualNetworksClient.Delete - case "Microsoft.Network/publicIPAddresses": - networkDeleteFunction = s.client.PublicIPAddressesClient.Delete - } - if networkDeleteFunction != nil { - _, errChan := networkDeleteFunction(resourceGroupName, *deploymentOperation.Properties.TargetResource.ResourceName, nil) - err := <-errChan - if err != nil { - s.say(fmt.Sprintf("Error deleting resource. Please delete manually.\n\n"+ - "Name: %s\n"+ - "Error: %s", *deploymentOperation.Properties.TargetResource.ResourceName, err.Error())) - s.error(err) - } - } + return err } } - return err + + return nil } else { s.say("\nThe resource group was created by Packer, deleting ...") _, errChan := s.client.GroupsClient.Delete(resourceGroupName, cancelCh) @@ -97,6 +62,61 @@ func (s *StepDeleteResourceGroup) deleteResourceGroup(state multistep.StateBag, } } +func (s *StepDeleteResourceGroup) deleteDeploymentResources(deploymentName, resourceGroupName string) error { + maxResources := int32(maxResourcesToDelete) + + deploymentOperations, err := s.client.DeploymentOperationsClient.List(resourceGroupName, deploymentName, &maxResources) + if err != nil { + s.reportIfError(err, resourceGroupName) + return err + } + + for _, deploymentOperation := range *deploymentOperations.Value { + // Sometimes an empty operation is added to the list by Azure + if deploymentOperation.Properties.TargetResource == nil { + continue + } + s.say(fmt.Sprintf(" -> %s : '%s'", + *deploymentOperation.Properties.TargetResource.ResourceType, + *deploymentOperation.Properties.TargetResource.ResourceName)) + + var networkDeleteFunction func(string, string, <-chan struct{}) (<-chan autorest.Response, <-chan error) + resourceName := *deploymentOperation.Properties.TargetResource.ResourceName + + switch *deploymentOperation.Properties.TargetResource.ResourceType { + case "Microsoft.Compute/virtualMachines": + _, errChan := s.client.VirtualMachinesClient.Delete(resourceGroupName, resourceName, nil) + err := <-errChan + s.reportIfError(err, resourceName) + case "Microsoft.KeyVault/vaults": + _, err := s.client.VaultClientDelete.Delete(resourceGroupName, resourceName) + s.reportIfError(err, resourceName) + case "Microsoft.Network/networkInterfaces": + networkDeleteFunction = s.client.InterfacesClient.Delete + case "Microsoft.Network/virtualNetworks": + networkDeleteFunction = s.client.VirtualNetworksClient.Delete + case "Microsoft.Network/publicIPAddresses": + networkDeleteFunction = s.client.PublicIPAddressesClient.Delete + } + if networkDeleteFunction != nil { + _, errChan := networkDeleteFunction(resourceGroupName, resourceName, nil) + err := <-errChan + s.reportIfError(err, resourceName) + } + } + + return nil +} + +func (s *StepDeleteResourceGroup) 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) + } +} + func (s *StepDeleteResourceGroup) Run(state multistep.StateBag) multistep.StepAction { s.say("Deleting resource group ...") diff --git a/builder/azure/arm/step_deploy_template.go b/builder/azure/arm/step_deploy_template.go index 53b639d51..b67dd5c1d 100644 --- a/builder/azure/arm/step_deploy_template.go +++ b/builder/azure/arm/step_deploy_template.go @@ -22,15 +22,17 @@ type StepDeployTemplate struct { error func(e error) config *Config factory templateFactoryFunc + name string } -func NewStepDeployTemplate(client *AzureClient, ui packer.Ui, config *Config, factory templateFactoryFunc) *StepDeployTemplate { +func NewStepDeployTemplate(client *AzureClient, ui packer.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 @@ -59,15 +61,14 @@ func (s *StepDeployTemplate) Run(state multistep.StateBag) multistep.StepAction s.say("Deploying 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)) + s.say(fmt.Sprintf(" -> DeploymentName : '%s'", s.name)) result := common.StartInterruptibleTask( func() bool { return common.IsStateCancelled(state) }, func(cancelCh <-chan struct{}) error { - return s.deploy(resourceGroupName, deploymentName, cancelCh) + return s.deploy(resourceGroupName, s.name, cancelCh) }, ) @@ -104,6 +105,9 @@ func (s *StepDeployTemplate) deleteOperationResource(resourceType string, resour return err } + case "Microsoft.KeyVault/vaults": + _, err := s.client.VaultClientDelete.Delete(resourceGroupName, resourceName) + return err case "Microsoft.Network/networkInterfaces": networkDeleteFunction = s.client.InterfacesClient.Delete case "Microsoft.Network/virtualNetworks": @@ -142,7 +146,6 @@ func (s *StepDeployTemplate) deleteImage(imageType string, imageName string, res blob := s.client.BlobStorageClient.GetContainerReference(storageAccountName).GetBlobReference(blobName) err = blob.Delete(nil) return err - } func (s *StepDeployTemplate) Cleanup(state multistep.StateBag) { @@ -158,7 +161,7 @@ func (s *StepDeployTemplate) Cleanup(state multistep.StateBag) { var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) var computeName = state.Get(constants.ArmComputeName).(string) - var deploymentName = state.Get(constants.ArmDeploymentName).(string) + var deploymentName = s.name imageType, imageName, err := s.disk(resourceGroupName, computeName) if err != nil { ui.Error("Could not retrieve OS Image details") diff --git a/builder/azure/arm/step_deploy_template_test.go b/builder/azure/arm/step_deploy_template_test.go index 3a1d0c0bc..d812e0ffa 100644 --- a/builder/azure/arm/step_deploy_template_test.go +++ b/builder/azure/arm/step_deploy_template_test.go @@ -61,6 +61,7 @@ func TestStepDeployTemplateShouldTakeStepArgumentsFromStateBag(t *testing.T) { }, say: func(message string) {}, error: func(e error) {}, + name: "--deployment-name--", } stateBag := createTestStateBagStepValidateTemplate() @@ -70,10 +71,9 @@ func TestStepDeployTemplateShouldTakeStepArgumentsFromStateBag(t *testing.T) { 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 { + if actualDeploymentName != "--deployment-name--" { t.Fatal("Expected StepValidateTemplate to source 'constants.ArmDeploymentName' from the state bag, but it did not.") } diff --git a/builder/azure/common/constants/stateBag.go b/builder/azure/common/constants/stateBag.go index 6d5a93068..720979b9c 100644 --- a/builder/azure/common/constants/stateBag.go +++ b/builder/azure/common/constants/stateBag.go @@ -15,6 +15,7 @@ const ( ArmComputeName string = "arm.ComputeName" ArmImageParameters string = "arm.ImageParameters" ArmCertificateUrl string = "arm.CertificateUrl" + ArmKeyVaultDeploymentName string = "arm.KeyVaultDeploymentName" ArmDeploymentName string = "arm.DeploymentName" ArmNicName string = "arm.NicName" ArmKeyVaultName string = "arm.KeyVaultName" diff --git a/builder/azure/common/vault.go b/builder/azure/common/vault.go index db5c6db0c..ab54ee493 100644 --- a/builder/azure/common/vault.go +++ b/builder/azure/common/vault.go @@ -9,15 +9,18 @@ import ( "net/url" "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" ) const ( - AzureVaultApiVersion = "2015-06-01" + AzureVaultApiVersion = "2016-10-01" ) type VaultClient struct { autorest.Client keyVaultEndpoint url.URL + SubscriptionID string + baseURI string } func NewVaultClient(keyVaultEndpoint url.URL) VaultClient { @@ -26,6 +29,13 @@ func NewVaultClient(keyVaultEndpoint url.URL) VaultClient { } } +func NewVaultClientWithBaseURI(baseURI, subscriptionID string) VaultClient { + return VaultClient{ + baseURI: baseURI, + SubscriptionID: subscriptionID, + } +} + type Secret struct { ID *string `json:"id,omitempty"` Value string `json:"value"` @@ -76,6 +86,72 @@ func (client *VaultClient) GetSecret(vaultName, secretName string) (*Secret, err return &secret, nil } +// Delete deletes the specified Azure key vault. +// +// resourceGroupName is the name of the Resource Group to which the vault belongs. vaultName is the name of the vault +// to delete +func (client *VaultClient) Delete(resourceGroupName string, vaultName string) (result autorest.Response, err error) { + req, err := client.DeletePreparer(resourceGroupName, vaultName) + if err != nil { + err = autorest.NewErrorWithError(err, "keyvault.VaultsClient", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result.Response = resp + err = autorest.NewErrorWithError(err, "keyvault.VaultsClient", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "keyvault.VaultsClient", "Delete", resp, "Failure responding to request") + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client *VaultClient) DeletePreparer(resourceGroupName string, vaultName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "resourceGroupName": autorest.Encode("path", resourceGroupName), + "SubscriptionID": autorest.Encode("path", client.SubscriptionID), + "vaultName": autorest.Encode("path", vaultName), + } + + queryParameters := map[string]interface{}{ + "api-version": AzureVaultApiVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(client.baseURI), + autorest.WithPathParameters("/subscriptions/{SubscriptionID}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{vaultName}", pathParameters), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare(&http.Request{}) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client *VaultClient) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, + req, + azure.DoPollForAsynchronous(client.PollingDelay)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client *VaultClient) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = resp + return +} + func (client *VaultClient) getVaultUrl(vaultName string) string { return fmt.Sprintf("%s://%s.%s/", client.keyVaultEndpoint.Scheme, vaultName, client.keyVaultEndpoint.Host) }