-
Notifications
You must be signed in to change notification settings - Fork 82
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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 | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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 | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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")) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
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 | ||||
} |
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") | ||
} | ||
} |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done