Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Azure keyvault secret datasource #89

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions datasource/keyvaultsecret/data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//go:generate packer-sdc struct-markdown
//go:generate packer-sdc mapstructure-to-hcl2 -type DatasourceOutput,Config
package keyvaultsecret

import (
"context"
"fmt"
"log"
"net/url"

"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
"github.com/Azure/go-autorest/autorest"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer-plugin-azure/builder/azure/common/client"
"github.com/hashicorp/packer-plugin-sdk/hcl2helper"
"github.com/hashicorp/packer-plugin-sdk/template/config"
"github.com/zclconf/go-cty/cty"

packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)

type Config struct {
// Specifies the name of the Key Vault Secret.
Name string `mapstructure:"name" required:"true"`

// Specifies the ID of the Key Vault instance where the Secret resides.
KeyvaultId string `mapstructure:"keyvault_id" required:"true"`

// Authentication via OAUTH
ClientConfig client.Config `mapstructure:",squash"`
}

type Datasource struct {
config Config
}

type DatasourceOutput struct {
// The Key Vault Secret ID.
Id string `mapstructure:"id"`
// The value of the Key Vault Secret.
Value string `mapstructure:"value"`
// The content type for the Key Vault Secret.
ContentType string `mapstructure:"content_type"`
// Any tags assigned to this resource.
Tags map[string]*string `mapstructure:"tags"`
}

func (d *Datasource) ConfigSpec() hcldec.ObjectSpec {
return d.config.FlatMapstructure().HCL2Spec()
}

func (d *Datasource) Configure(raws ...interface{}) error {
err := config.Decode(&d.config, nil, raws...)
if err != nil {
return err
}

var errs *packersdk.MultiError
if d.config.Name == "" {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("a 'name' must be provided"))
}
if d.config.KeyvaultId == "" {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("a 'keyvault_id' must be provided"))
}

err = d.config.ClientConfig.SetDefaultValues()
if err != nil {
errs = packersdk.MultiErrorAppend(errs, err)
}

d.config.ClientConfig.Validate(errs)
if errs != nil && len(errs.Errors) > 0 {
return errs
}

return nil
}

func (d *Datasource) OutputSpec() hcldec.ObjectSpec {
return (&DatasourceOutput{}).FlatMapstructure().HCL2Spec()
}

// We need to stub packersdk ui "say" function. UI is not available from this package, I guess this is the best we can do for now
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this sounds like something we need to think about 🤔 nice workaround though!

Copy link
Contributor

@nywilken nywilken May 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @sylviamoss that we need to think about the use of UI within datasources. But IMHO I don't think it is needed for this datasource.

The use of the say function is solely for displaying the debug like messages from the GetServicePrincipalTokens function which can add some additional noise to the output since datasources execute before an actual build.

For this case I would suggestion going with the logSay function but with a few small tweaks. Which will only show up when running a Packer build with the debug logs enabled PACKER_LOG=1 packer build template.pkr.hcl.

func logSay(s string) {
  log.Println("[DEBUG] packer-datasource-azure-keyvaultsecret:", s)
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

func logSay(s string) {
log.Println("[DEBUG] packer-datasource-azure-keyvaultsecret:", s)
}

func getVaultUrl(keyvaultURL *url.URL, vaultName string) string {
return fmt.Sprintf("%s://%s.%s/", keyvaultURL.Scheme, vaultName, keyvaultURL.Host)
}

func (d *Datasource) Execute() (cty.Value, error) {
err := d.config.ClientConfig.FillParameters()
if err != nil {
return cty.NullVal(cty.EmptyObject), err
}

keyVaultURL, err := url.Parse(d.config.ClientConfig.CloudEnvironment().KeyVaultEndpoint)
if err != nil {
return cty.NullVal(cty.EmptyObject), err
}

/* Get token from client configuration */
_, servicePrincipalTokenVault, err := d.config.ClientConfig.GetServicePrincipalTokens(logSay)
if err != nil {
return cty.NullVal(cty.EmptyObject), err
}

/* Configure keyvault client */
basicClient := keyvault.New()
basicClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalTokenVault)

/* Get secret value */
secret, err := basicClient.GetSecret(context.TODO(), getVaultUrl(keyVaultURL, d.config.KeyvaultId), d.config.Name, "")
if err != nil {
return cty.NullVal(cty.EmptyObject), err
}

/* Build output object */
var output DatasourceOutput
if secret.Value != nil {
output.Value = *secret.Value
}
if secret.ID != nil {
output.Id = *secret.ID
}
if secret.ContentType != nil {
output.ContentType = *secret.ContentType
}
if secret.Tags != nil {
output.Tags = secret.Tags
}

return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil
}
84 changes: 84 additions & 0 deletions datasource/keyvaultsecret/data.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

124 changes: 124 additions & 0 deletions datasource/keyvaultsecret/data_acc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package keyvaultsecret

import (
"context"
_ "embed"
"fmt"
"io/ioutil"
"os"
"os/exec"
"regexp"
"testing"

"github.com/Azure/azure-sdk-for-go/services/keyvault/auth"
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
"github.com/Azure/go-autorest/autorest"
"github.com/hashicorp/packer-plugin-sdk/acctest"
)

//go:embed test-fixtures/template.pkr.hcl
var testDatasourceBasic string

func TestAccAzureKeyvaultSecret(t *testing.T) {
testEnv := "dev"
secret := &KeyvaultSecret{
Name: "packer-datasource-keyvault-test-secret",
Value: "this_is_the_packer_test_secret_value",
ContentType: "text/html",
Tags: map[string]*string{"environment": &testEnv},
}

testCase := &acctest.PluginTestCase{
Name: "azure_keyvault_secret_datasource_basic_test",
Setup: func() error {
return secret.Create()
},
Teardown: func() error {
return secret.Delete()
},
Template: testDatasourceBasic,
Check: func(buildCommand *exec.Cmd, logfile string) error {
if buildCommand.ProcessState != nil {
if buildCommand.ProcessState.ExitCode() != 0 {
return fmt.Errorf("Bad exit code. Logfile: %s", logfile)
}
}

logs, err := os.Open(logfile)
if err != nil {
return fmt.Errorf("Unable find %s", logfile)
}
defer logs.Close()

logsBytes, err := ioutil.ReadAll(logs)
if err != nil {
return fmt.Errorf("Unable to read %s", logfile)
}
logsString := string(logsBytes)

valueLog := fmt.Sprintf("null.basic-example: secret value: %s", secret.Value)
contentTypeLog := fmt.Sprintf("null.basic-example: secret content_type: %s", secret.ContentType)
environmentLog := fmt.Sprintf("null.basic-example: secret environment: %s", *secret.Tags["environment"])

if matched, _ := regexp.MatchString(valueLog+".*", logsString); !matched {
t.Fatalf("logs doesn't contain expected secret value %q", logsString)
}
if matched, _ := regexp.MatchString(contentTypeLog+".*", logsString); !matched {
t.Fatalf("logs doesn't contain expected secret ContentType %q", logsString)
}
if matched, _ := regexp.MatchString(environmentLog+".*", logsString); !matched {
t.Fatalf("logs doesn't contain expected secret tag %q", logsString)
}

return nil
},
}
acctest.TestPlugin(t, testCase)
}

type KeyvaultSecret struct {
Name string
Value string
Id string
ContentType string
Tags map[string]*string

client keyvault.BaseClient
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Run go fmt against the files to format the source.

}

func getAuthorizer() (autorest.Authorizer, error) {
os.Setenv("AZURE_AD_RESOURCE", "https://vault.azure.net")
os.Setenv("AZURE_TENANT_ID", os.Getenv("PKR_VAR_tenant_id"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To simplify the acceptance testing setup the expected environment variables should be the same as expected by the current builder acceptance tests. This way you don't have to set anything, maybe other than the AZURE_AD_RESOURCE which looks like it can be a const for the test cases.

Copy link
Contributor

@nywilken nywilken May 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After your last comment about the AZURE_* variables I understand why you have this setup. Instead of PKR_VAR_* I recommend using the environment variables already required for the the acceptance tests. For example

os.Setenv("AZURE_TENANT_ID", os.Getenv("ARM_TENANT_ID"))
os.Setenv("AZURE_CLIENT_ID", os.Getenv("ARM_CLIENT_ID"))
...

I also recommend that a comment be added to the top of this file indicating which env variables are needed for the tests.

os.Setenv("AZURE_CLIENT_ID", os.Getenv("PKR_VAR_client_id"))
os.Setenv("AZURE_CLIENT_SECRET", os.Getenv("PKR_VAR_client_secret"))

credAuthorizer, err := auth.NewAuthorizerFromEnvironment()
return credAuthorizer, err
}

func (as *KeyvaultSecret) Create() error {
as.client = keyvault.New()
authorizer, err := getAuthorizer()
if err != nil {
return err
}

var parameters keyvault.SecretSetParameters
parameters.ContentType = &as.ContentType
parameters.Value = &as.Value
parameters.Tags = as.Tags

as.client.Authorizer = authorizer

_, err = as.client.SetSecret(context.TODO(), "https://"+os.Getenv("PKR_VAR_keyvault_id")+".vault.azure.net", as.Name, parameters)
if err != nil {
return err
}
return err
}

func (as *KeyvaultSecret) Delete() error {
var err error
_, err = as.client.DeleteSecret(context.TODO(), "https://"+os.Getenv("PKR_VAR_keyvault_id")+".vault.azure.net", as.Name)
return err
}
37 changes: 37 additions & 0 deletions datasource/keyvaultsecret/data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package keyvaultsecret

import (
"testing"
)

func TestDatasourceConfigure_EmptySecretName(t *testing.T) {
datasource := Datasource{
config: Config{},
}
if err := datasource.Configure(nil); err == nil {
t.Fatalf("Should error if secret name is not specified")
}
}

func TestDatasourceConfigure_KeyvaultID(t *testing.T) {
datasource := Datasource{
config: Config{
Name: "my-secret",
},
}
if err := datasource.Configure(nil); err == nil {
t.Fatalf("Should error if keyvault id is not specified")
}
}

func TestDatasourceConfigure_OkConfig(t *testing.T) {
datasource := Datasource{
config: Config{
Name: "my-secret",
KeyvaultId: "my-keyvault",
},
}
if err := datasource.Configure(nil); err != nil {
t.Fatalf("Should not issue error if configuration is okay")
}
}
Loading