Merge pull request #8704 from hashicorp/custom_vault_azure

Custom vault azure
This commit is contained in:
Megan Marsh 2020-02-10 10:51:06 -08:00 committed by GitHub
commit dfefe7e8d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 253 additions and 8 deletions

View File

@ -231,8 +231,16 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
keyVaultDeploymentName := b.stateBag.Get(constants.ArmKeyVaultDeploymentName).(string)
steps = []multistep.Step{
NewStepCreateResourceGroup(azureClient, ui),
NewStepValidateTemplate(azureClient, ui, &b.config, GetKeyVaultDeployment),
NewStepDeployTemplate(azureClient, ui, &b.config, keyVaultDeploymentName, GetKeyVaultDeployment),
}
if b.config.BuildKeyVaultName == "" {
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),
@ -261,7 +269,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
NewStepDeleteResourceGroup(azureClient, ui),
NewStepDeleteOSDisk(azureClient, ui),
NewStepDeleteAdditionalDisks(azureClient, ui),
}
)
} else {
return nil, fmt.Errorf("Builder does not support the os_type '%s'", b.config.OSType)
}
@ -396,6 +404,13 @@ func (b *Builder) configureStateBag(stateBag multistep.StateBag) {
}
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)

View File

@ -253,7 +253,10 @@ type Config struct {
// group is deleted at the end of the build.
TempResourceGroupName string `mapstructure:"temp_resource_group_name"`
// Specify an existing resource group to run the build in.
BuildResourceGroupName string `mapstructure:"build_resource_group_name"`
BuildResourceGroupName string `mapstructure:"build_resource_group_name"`
// Specify an existing key vault to use for uploading certificates to the
// instance to connect.
BuildKeyVaultName string `mapstructure:"build_key_vault_name"`
storageAccountBlobEndpoint string
// This value allows you to
// set a virtual_network_name and obtain a public IP. If this value is not

View File

@ -53,6 +53,7 @@ type FlatConfig struct {
TempComputeName *string `mapstructure:"temp_compute_name" required:"false" cty:"temp_compute_name"`
TempResourceGroupName *string `mapstructure:"temp_resource_group_name" cty:"temp_resource_group_name"`
BuildResourceGroupName *string `mapstructure:"build_resource_group_name" cty:"build_resource_group_name"`
BuildKeyVaultName *string `mapstructure:"build_key_vault_name" cty:"build_key_vault_name"`
PrivateVirtualNetworkWithPublicIp *bool `mapstructure:"private_virtual_network_with_public_ip" required:"false" cty:"private_virtual_network_with_public_ip"`
VirtualNetworkName *string `mapstructure:"virtual_network_name" required:"false" cty:"virtual_network_name"`
VirtualNetworkSubnetName *string `mapstructure:"virtual_network_subnet_name" required:"false" cty:"virtual_network_subnet_name"`
@ -166,6 +167,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"temp_compute_name": &hcldec.AttrSpec{Name: "temp_compute_name", Type: cty.String, Required: false},
"temp_resource_group_name": &hcldec.AttrSpec{Name: "temp_resource_group_name", Type: cty.String, Required: false},
"build_resource_group_name": &hcldec.AttrSpec{Name: "build_resource_group_name", Type: cty.String, Required: false},
"build_key_vault_name": &hcldec.AttrSpec{Name: "build_key_vault_name", Type: cty.String, Required: false},
"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},

View File

@ -0,0 +1,45 @@
package arm
import (
"context"
"fmt"
"github.com/hashicorp/packer/builder/azure/common"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type StepCertificateInKeyVault struct {
config *Config
client common.AZVaultClientIface
say func(message string)
error func(e error)
}
func NewStepCertificateInKeyVault(cli common.AZVaultClientIface, ui packer.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"
azcommon "github.com/hashicorp/packer/builder/azure/common"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
func TestNewStepCertificateInKeyVault(t *testing.T) {
cli := azcommon.MockAZVaultClient{}
ui := &packer.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 := &packer.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

@ -44,9 +44,12 @@ func (s *StepDeleteResourceGroup) deleteResourceGroup(ctx context.Context, state
}
if keyVaultDeploymentName, ok := state.GetOk(constants.ArmKeyVaultDeploymentName); ok {
err = s.deleteDeploymentResources(ctx, keyVaultDeploymentName.(string), resourceGroupName)
if err != nil {
return err
// Only delete if custom keyvault was not provided.
if exists := state.Get(constants.ArmIsExistingKeyVault).(bool); exists {
err = s.deleteDeploymentResources(ctx, keyVaultDeploymentName.(string), resourceGroupName)
if err != nil {
return err
}
}
}

View File

@ -36,6 +36,7 @@ const (
ArmTags string = "arm.Tags"
ArmVirtualMachineCaptureParameters string = "arm.VirtualMachineCaptureParameters"
ArmIsExistingResourceGroup string = "arm.IsExistingResourceGroup"
ArmIsExistingKeyVault string = "arm.IsExistingKeyVault"
ArmIsManagedImage string = "arm.IsManagedImage"
ArmManagedImageResourceGroupName string = "arm.ManagedImageResourceGroupName"

View File

@ -16,6 +16,15 @@ const (
AzureVaultApiVersion = "2016-10-01"
)
// Enables us to test steps that access this cli
type AZVaultClientIface interface {
GetSecret(string, string) (*Secret, error)
SetSecret(string, string, string) error
DeletePreparer(string, string) (*http.Request, error)
DeleteResponder(*http.Response) (autorest.Response, error)
DeleteSender(*http.Request) (*http.Response, error)
}
type VaultClient struct {
autorest.Client
keyVaultEndpoint url.URL
@ -54,7 +63,8 @@ func (client *VaultClient) GetSecret(vaultName, secretName string) (*Secret, err
autorest.AsGet(),
autorest.WithBaseURL(client.getVaultUrl(vaultName)),
autorest.WithPathParameters("/secrets/{secret-name}", p),
autorest.WithQueryParameters(q))
autorest.WithQueryParameters(q),
)
if err != nil {
return nil, err
@ -86,6 +96,47 @@ func (client *VaultClient) GetSecret(vaultName, secretName string) (*Secret, err
return &secret, nil
}
func (client *VaultClient) SetSecret(vaultName, secretName string, secretValue string) error {
p := map[string]interface{}{
"secret-name": autorest.Encode("path", secretName),
}
q := map[string]interface{}{
"api-version": AzureVaultApiVersion,
}
jsonBody := fmt.Sprintf(`{"value": "%s"}`, secretValue)
req, err := autorest.Prepare(
&http.Request{},
autorest.AsPut(),
autorest.AsContentType("application/json; charset=utf-8"),
autorest.WithBaseURL(client.getVaultUrl(vaultName)),
autorest.WithPathParameters("/secrets/{secret-name}", p),
autorest.WithQueryParameters(q),
autorest.WithString(jsonBody),
)
if err != nil {
return err
}
resp, err := autorest.SendWithSender(client, req)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return fmt.Errorf(
"Failed to set secret to %s/%s, HTTP status code=%d (%s)",
vaultName,
secretName,
resp.StatusCode,
http.StatusText(resp.StatusCode))
}
return 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

View File

@ -0,0 +1,56 @@
package common
import (
"fmt"
"net/http"
"github.com/Azure/go-autorest/autorest"
)
type MockAZVaultClient struct {
GetSecretCalled bool
SetSecretCalled bool
SetSecretVaultName string
SetSecretSecretName string
SetSecretCert string
DeleteResponderCalled bool
DeletePreparerCalled bool
DeleteSenderCalled bool
IsError bool
}
func (m *MockAZVaultClient) GetSecret(vaultName, secretName string) (*Secret, error) {
m.GetSecretCalled = true
var secret Secret
return &secret, nil
}
func (m *MockAZVaultClient) SetSecret(vaultName, secretName string, secretValue string) error {
m.SetSecretCalled = true
m.SetSecretVaultName = vaultName
m.SetSecretSecretName = secretName
m.SetSecretCert = secretValue
if m.IsError {
return fmt.Errorf("generic error!!")
}
return nil
}
func (m *MockAZVaultClient) DeletePreparer(resourceGroupName string, vaultName string) (*http.Request, error) {
m.DeletePreparerCalled = true
return nil, nil
}
func (m *MockAZVaultClient) DeleteResponder(resp *http.Response) (autorest.Response, error) {
m.DeleteResponderCalled = true
var result autorest.Response
return result, nil
}
func (m *MockAZVaultClient) DeleteSender(req *http.Request) (*http.Response, error) {
m.DeleteSenderCalled = true
return nil, nil
}

View File

@ -132,6 +132,9 @@
- `build_resource_group_name` (string) - Specify an existing resource group to run the build in.
- `build_key_vault_name` (string) - Specify an existing key vault to use for uploading certificates to the
instance to connect.
- `private_virtual_network_with_public_ip` (bool) - This value allows you to
set a virtual_network_name and obtain a public IP. If this value is not
set and virtual_network_name is defined Packer is only allowed to be