Skip to content

Commit

Permalink
test: Enables simulation of cloud-dev using hoverfly in alert configu…
Browse files Browse the repository at this point in the history
…ration acceptance tests (#2057)

* run capture mode in CI test to verify correct configuration of hoverfly ca cert

* remove general https proxy variable and adjust specific go client

* specify latest version of hoverfly

* workaround in client to be able to use hoverfly sdk

* adjust client code to use dynamic proxy port

* define env variables for capturing and simulating

* adding serialization of execution variables

* linter fixes

* define a single execution variables file per test

* avoid creating project during simulate mode

* fixing linter issues

* define separate function for mux provider factory explicit for testing

* replace hoverfly go sdk with cli to avoid usage of tls insecure flag

* addressing PR comments, removing repetition of t.cleanup in each test

* include small note in contribution guide

* double quotes in scripts

* simplify calls to ManageProjectID by changing signature

* adjust docs
  • Loading branch information
AgustinBettati committed Mar 26, 2024
1 parent 69d69da commit 3c68921
Show file tree
Hide file tree
Showing 18 changed files with 443 additions and 140 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -32,6 +32,7 @@ log.*
test.env
__debug_*
coverage.out
simulations

website/vendor

Expand Down
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Expand Up @@ -10,6 +10,7 @@ Thanks for your interest in contributing to MongoDB Atlas Terraform Provider, th
- [Open a Pull Request](#open-a-pull-request)
- [Testing the Provider](#testing-the-provider)
- [Running Acceptance Tests](#running-acceptance-tests)
- [Replaying HTTP Requests with Hoverfly](#replaying-http-requests-with-hoverfly)
- [Code and Test Best Practices](#code-and-test-best-practices)
- [Creating New Resource and Data Sources](#creating-new-resources-and-data-sources)
- [Scaffolding Initial Code and File Structure](#scaffolding-initial-code-and-file-structure)
Expand Down Expand Up @@ -252,7 +253,14 @@ You must also configure the following environment variables before running the t
~> **Notice:** Acceptance tests create real resources, and often cost money to run. Please note in any PRs made if you are unable to pay to run acceptance tests for your contribution. We will accept "best effort" implementations of acceptance tests in this case and run them for you on our side. This may delay the contribution but we do not want your contribution blocked by funding.
- Run `make testacc`

#### Replaying HTTP requests with hoverfly

Some resources allow recording and replaying http requests using hoverfly when running tests (e.g. alert_configuration acceptance tests). You will be able to identify this if the test calls `replay.SetupReplayProxy(t)`.

- For capturing http traffic of an execution you have to configure the environment variable `REPLAY_MODE=capture`. Captured request/responses will be present in the directory `./simulations`.
- For replaying http traffic of an execution you have to configure the environment variable `REPLAY_MODE=simulate` which will use files present in the simulation directory.

**Note**: [Hoverfly](https://docs.hoverfly.io/en/latest/pages/introduction/introduction.html) is the proxy server used for capturing and simulating request. You must use the following [installation docs](https://docs.hoverfly.io/en/latest/pages/introduction/downloadinstallation.html#download-and-installation) to have the CLI available, as well as setting up the [hoverfly CA cert](https://docs.hoverfly.io/en/latest/pages/tutorials/basic/https/https.html) in your trust store.

### Testing Atlas Provider Versions that are NOT hosted on Terraform Registry (i.e. pre-release versions)
To test development / pre-release versions of the Terraform Atlas Provider that are not hosted on the Terraform Registry, you will need to create a [Terraform Provider Network Mirror](https://developer.hashicorp.com/terraform/internals/provider-network-mirror-protocol).
Expand Down
9 changes: 9 additions & 0 deletions internal/config/client.go
Expand Up @@ -38,6 +38,7 @@ type MongoDBClient struct {
// Config contains the configurations needed to use SDKs
type Config struct {
AssumeRole *AssumeRole
ProxyPort *int
PublicKey string
PrivateKey string
BaseURL string
Expand Down Expand Up @@ -66,6 +67,14 @@ func (c *Config) NewClient(ctx context.Context) (any, error) {
// setup a transport to handle digest
transport := digest.NewTransport(cast.ToString(c.PublicKey), cast.ToString(c.PrivateKey))

// proxy is only used for testing purposes to connect with hoverfly for capturing/replaying requests
if c.ProxyPort != nil {
proxyURL, _ := url.Parse(fmt.Sprintf("http://localhost:%d", *c.ProxyPort))
transport.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
}

// initialize the client
client, err := transport.Client()
if err != nil {
Expand Down
18 changes: 9 additions & 9 deletions internal/provider/credentials.go
Expand Up @@ -19,11 +19,11 @@ const (
endPointSTSDefault = "https://sts.amazonaws.com"
)

func configureCredentialsSTS(cfg config.Config, secret, region, awsAccessKeyID, awsSecretAccessKey, awsSessionToken, endpoint string) (config.Config, error) {
func configureCredentialsSTS(cfg *config.Config, secret, region, awsAccessKeyID, awsSecretAccessKey, awsSessionToken, endpoint string) (config.Config, error) {
ep, err := endpoints.GetSTSRegionalEndpoint("regional")
if err != nil {
log.Printf("GetSTSRegionalEndpoint error: %s", err)
return cfg, err
return *cfg, err
}

defaultResolver := endpoints.DefaultResolver()
Expand Down Expand Up @@ -56,35 +56,35 @@ func configureCredentialsSTS(cfg config.Config, secret, region, awsAccessKeyID,
_, err = sess.Config.Credentials.Get()
if err != nil {
log.Printf("Session get credentials error: %s", err)
return cfg, err
return *cfg, err
}
_, err = creds.Get()
if err != nil {
log.Printf("STS get credentials error: %s", err)
return cfg, err
return *cfg, err
}
secretString, err := secretsManagerGetSecretValue(sess, &aws.Config{Credentials: creds, Region: aws.String(region)}, secret)
if err != nil {
log.Printf("Get Secrets error: %s", err)
return cfg, err
return *cfg, err
}

var secretData SecretData
err = json.Unmarshal([]byte(secretString), &secretData)
if err != nil {
return cfg, err
return *cfg, err
}
if secretData.PrivateKey == "" {
return cfg, fmt.Errorf("secret missing value for credential PrivateKey")
return *cfg, fmt.Errorf("secret missing value for credential PrivateKey")
}

if secretData.PublicKey == "" {
return cfg, fmt.Errorf("secret missing value for credential PublicKey")
return *cfg, fmt.Errorf("secret missing value for credential PublicKey")
}

cfg.PublicKey = secretData.PublicKey
cfg.PrivateKey = secretData.PrivateKey
return cfg, nil
return *cfg, nil
}

func secretsManagerGetSecretValue(sess *session.Session, creds *aws.Config, secret string) (string, error) {
Expand Down
27 changes: 20 additions & 7 deletions internal/provider/provider.go
Expand Up @@ -45,7 +45,9 @@ const (
MissingAuthAttrError = "either Atlas Programmatic API Keys or AWS Secrets Manager attributes must be set"
)

type MongodbtlasProvider struct{}
type MongodbtlasProvider struct {
proxyPort *int
}

type tfMongodbAtlasProviderModel struct {
AssumeRole types.List `tfsdk:"assume_role"`
Expand Down Expand Up @@ -229,6 +231,7 @@ func (p *MongodbtlasProvider) Configure(ctx context.Context, req provider.Config
PrivateKey: data.PrivateKey.ValueString(),
BaseURL: data.BaseURL.ValueString(),
RealmBaseURL: data.RealmBaseURL.ValueString(),
ProxyPort: p.proxyPort,
}

var assumeRoles []tfAssumeRoleModel
Expand All @@ -243,7 +246,7 @@ func (p *MongodbtlasProvider) Configure(ctx context.Context, req provider.Config
awsSessionToken := data.AwsSessionToken.ValueString()
endpoint := data.StsEndpoint.ValueString()
var err error
cfg, err = configureCredentialsSTS(cfg, secret, region, awsAccessKeyID, awsSecretAccessKey, awsSessionToken, endpoint)
cfg, err = configureCredentialsSTS(&cfg, secret, region, awsAccessKeyID, awsSecretAccessKey, awsSessionToken, endpoint)
if err != nil {
resp.Diagnostics.AddError("failed to configure credentials STS", err.Error())
return
Expand Down Expand Up @@ -448,13 +451,23 @@ func (p *MongodbtlasProvider) Resources(context.Context) []func() resource.Resou
return resources
}

func NewFrameworkProvider() provider.Provider {
return &MongodbtlasProvider{}
func NewFrameworkProvider(proxyPort *int) provider.Provider {
return &MongodbtlasProvider{
proxyPort: proxyPort,
}
}

func MuxProviderFactory() func() tfprotov6.ProviderServer {
return muxProviderFactory(nil)
}

func MuxProviderFactoryForTesting(proxyPort *int) func() tfprotov6.ProviderServer {
return muxProviderFactory(proxyPort)
}

func MuxedProviderFactory() func() tfprotov6.ProviderServer {
v2Provider := NewSdkV2Provider()
newProvider := NewFrameworkProvider()
func muxProviderFactory(proxyPort *int) func() tfprotov6.ProviderServer {
v2Provider := NewSdkV2Provider(proxyPort)
newProvider := NewFrameworkProvider(proxyPort)
ctx := context.Background()
upgradedSdkProvider, err := tf5to6server.UpgradeServer(ctx, v2Provider.GRPCProvider)
if err != nil {
Expand Down
69 changes: 36 additions & 33 deletions internal/provider/provider_sdk2.go
Expand Up @@ -72,7 +72,7 @@ type SecretData struct {
}

// NewSdkV2Provider returns the provider to be use by the code.
func NewSdkV2Provider() *schema.Provider {
func NewSdkV2Provider(proxyPort *int) *schema.Provider {
provider := &schema.Provider{
Schema: map[string]*schema.Schema{
"public_key": {
Expand Down Expand Up @@ -135,7 +135,7 @@ func NewSdkV2Provider() *schema.Provider {
},
DataSourcesMap: getDataSourcesMap(),
ResourcesMap: getResourcesMap(),
ConfigureContextFunc: providerConfigure,
ConfigureContextFunc: providerConfigure(proxyPort),
}
addPreviewFeatures(provider)
return provider
Expand Down Expand Up @@ -283,41 +283,44 @@ func addPreviewFeatures(provider *schema.Provider) {
}
}

func providerConfigure(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) {
diagnostics := setDefaultsAndValidations(d)
if diagnostics.HasError() {
return nil, diagnostics
}

cfg := config.Config{
PublicKey: d.Get("public_key").(string),
PrivateKey: d.Get("private_key").(string),
BaseURL: d.Get("base_url").(string),
RealmBaseURL: d.Get("realm_base_url").(string),
}

assumeRoleValue, ok := d.GetOk("assume_role")
awsRoleDefined := ok && len(assumeRoleValue.([]any)) > 0 && assumeRoleValue.([]any)[0] != nil
if awsRoleDefined {
cfg.AssumeRole = expandAssumeRole(assumeRoleValue.([]any)[0].(map[string]any))
secret := d.Get("secret_name").(string)
region := conversion.MongoDBRegionToAWSRegion(d.Get("region").(string))
awsAccessKeyID := d.Get("aws_access_key_id").(string)
awsSecretAccessKey := d.Get("aws_secret_access_key").(string)
awsSessionToken := d.Get("aws_session_token").(string)
endpoint := d.Get("sts_endpoint").(string)
var err error
cfg, err = configureCredentialsSTS(cfg, secret, region, awsAccessKeyID, awsSecretAccessKey, awsSessionToken, endpoint)
func providerConfigure(proxyPort *int) func(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) {
return func(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) {
diagnostics := setDefaultsAndValidations(d)
if diagnostics.HasError() {
return nil, diagnostics
}

cfg := config.Config{
PublicKey: d.Get("public_key").(string),
PrivateKey: d.Get("private_key").(string),
BaseURL: d.Get("base_url").(string),
RealmBaseURL: d.Get("realm_base_url").(string),
ProxyPort: proxyPort,
}

assumeRoleValue, ok := d.GetOk("assume_role")
awsRoleDefined := ok && len(assumeRoleValue.([]any)) > 0 && assumeRoleValue.([]any)[0] != nil
if awsRoleDefined {
cfg.AssumeRole = expandAssumeRole(assumeRoleValue.([]any)[0].(map[string]any))
secret := d.Get("secret_name").(string)
region := conversion.MongoDBRegionToAWSRegion(d.Get("region").(string))
awsAccessKeyID := d.Get("aws_access_key_id").(string)
awsSecretAccessKey := d.Get("aws_secret_access_key").(string)
awsSessionToken := d.Get("aws_session_token").(string)
endpoint := d.Get("sts_endpoint").(string)
var err error
cfg, err = configureCredentialsSTS(&cfg, secret, region, awsAccessKeyID, awsSecretAccessKey, awsSessionToken, endpoint)
if err != nil {
return nil, append(diagnostics, diag.FromErr(err)...)
}
}

client, err := cfg.NewClient(ctx)
if err != nil {
return nil, append(diagnostics, diag.FromErr(err)...)
}
return client, diagnostics
}

client, err := cfg.NewClient(ctx)
if err != nil {
return nil, append(diagnostics, diag.FromErr(err)...)
}
return client, diagnostics
}

func setDefaultsAndValidations(d *schema.ResourceData) diag.Diagnostics {
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/provider_sdk2_test.go
Expand Up @@ -7,7 +7,7 @@ import (
)

func TestSdkV2Provider(t *testing.T) {
if err := provider.NewSdkV2Provider().InternalValidate(); err != nil {
if err := provider.NewSdkV2Provider(nil).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
4 changes: 2 additions & 2 deletions internal/provider/provider_test.go
Expand Up @@ -13,7 +13,7 @@ import (
func TestResourceSchemas(t *testing.T) {
t.Parallel()
ctxProvider := context.Background()
prov := provider.NewFrameworkProvider()
prov := provider.NewFrameworkProvider(nil)
var provReq providerfw.MetadataRequest
var provRes providerfw.MetadataResponse
prov.Metadata(ctxProvider, provReq, &provRes)
Expand Down Expand Up @@ -45,7 +45,7 @@ func TestResourceSchemas(t *testing.T) {
func TestDataSourceSchemas(t *testing.T) {
t.Parallel()
ctxProvider := context.Background()
prov := provider.NewFrameworkProvider()
prov := provider.NewFrameworkProvider(nil)
var provReq providerfw.MetadataRequest
var provRes providerfw.MetadataResponse
prov.Metadata(ctxProvider, provReq, &provRes)
Expand Down
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc"
)

Expand Down Expand Up @@ -237,3 +238,11 @@ func configWithPagerDutyDS(projectID, serviceKey string, enabled bool) string {
}
`, projectID, serviceKey, enabled)
}

func checkExists(resourceName string) resource.TestCheckFunc {
return checkExistsUsingProxy(nil, resourceName)
}

func checkDestroy(s *terraform.State) error {
return checkDestroyUsingProxy(nil)(s)
}

0 comments on commit 3c68921

Please sign in to comment.