Skip to content

Commit

Permalink
refactor: move equinix-sdk-go Metal client setup into the config pack…
Browse files Browse the repository at this point in the history
…age (#563)

When we need to use a Metal API client inside a terraform resource or
datasource, we need to ensure that the client is instrumented correctly.
Previously this was done by repeating the same function calls in every
CRUD function for every resource and datasource; as a result we were
often missing instrumentation, and there was a high risk of losing
instrumentation when converting from the old packngo SDK to
equinix-sdk-go.

This refactors the equinix-sdk-go client setup code to better ensure
that we get a correctly-configured client in all situations and help
ensure that we maintain visibility into usage after doing SDK
conversions.
  • Loading branch information
ctreatma committed Feb 14, 2024
1 parent 27ee859 commit b910a66
Show file tree
Hide file tree
Showing 17 changed files with 109 additions and 89 deletions.
2 changes: 1 addition & 1 deletion equinix/data_source_metal_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ func dataSourceMetalDevice() *schema.Resource {
}

func dataSourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForSDK(d)

hostnameRaw, hostnameOK := d.GetOk("hostname")
projectIdRaw, projectIdOK := d.GetOk("project_id")
Expand Down
2 changes: 1 addition & 1 deletion equinix/data_source_metal_device_bgp_neighbors.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func dataSourceMetalDeviceBGPNeighbors() *schema.Resource {
}

func dataSourceMetalDeviceBGPNeighborsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForSDK(d)
deviceID := d.Get("device_id").(string)

bgpNeighborsRaw, _, err := client.DevicesApi.GetBgpNeighborData(ctx, deviceID).Execute()
Expand Down
4 changes: 2 additions & 2 deletions equinix/data_source_metal_devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ func dataSourceMetalDevices() *schema.Resource {
return datalist.NewResource(dataListConfig)
}

func getDevices(meta interface{}, extra map[string]interface{}) ([]interface{}, error) {
client := meta.(*config.Config).Metalgo
func getDevices(d *schema.ResourceData, meta interface{}, extra map[string]interface{}) ([]interface{}, error) {
client := meta.(*config.Config).NewMetalClientForSDK(d)
projectID := extra["project_id"].(string)
orgID := extra["organization_id"].(string)

Expand Down
2 changes: 1 addition & 1 deletion equinix/data_source_metal_plans.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func dataSourceMetalPlans() *schema.Resource {
return datalist.NewResource(dataListConfig)
}

func getPlans(meta interface{}, extra map[string]interface{}) ([]interface{}, error) {
func getPlans(_ *schema.ResourceData, meta interface{}, extra map[string]interface{}) ([]interface{}, error) {
client := meta.(*config.Config).Metal
opts := &packngo.ListOptions{
Includes: []string{"available_in", "available_in_metros"},
Expand Down
3 changes: 2 additions & 1 deletion equinix/helpers_device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ func Test_waitUntilReservationProvisionable(t *testing.T) {
}
meta.Load(ctx)

if err := waitUntilReservationProvisionable(ctx, meta.Metalgo, tt.args.reservationId, tt.args.instanceId, 50*time.Millisecond, 1*time.Second, 50*time.Millisecond); (err != nil) != tt.wantErr {
client := meta.NewMetalClientForTesting()
if err := waitUntilReservationProvisionable(ctx, client, tt.args.reservationId, tt.args.instanceId, 50*time.Millisecond, 1*time.Second, 50*time.Millisecond); (err != nil) != tt.wantErr {
t.Errorf("waitUntilReservationProvisionable() error = %v, wantErr %v", err, tt.wantErr)
}

Expand Down
15 changes: 5 additions & 10 deletions equinix/resource_metal_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,8 +509,7 @@ func reinstallDisabledAndNoChangesAllowed(attribute string) customdiff.ResourceC
}

func resourceMetalDeviceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
meta.(*config.Config).AddModuleToMetalGoUserAgent(d)
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForSDK(d)

createRequest := metalv1.CreateDeviceRequest{}

Expand Down Expand Up @@ -569,8 +568,7 @@ func resourceMetalDeviceCreate(ctx context.Context, d *schema.ResourceData, meta
}

func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
meta.(*config.Config).AddModuleToMetalGoUserAgent(d)
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForSDK(d)

device, resp, err := client.DevicesApi.FindDeviceById(context.Background(), d.Id()).Include(deviceCommonIncludes).Execute()
if err != nil {
Expand Down Expand Up @@ -676,8 +674,7 @@ func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta i
}

func resourceMetalDeviceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
meta.(*config.Config).AddModuleToMetalGoUserAgent(d)
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForSDK(d)

ur := metalv1.DeviceUpdateInput{}

Expand Down Expand Up @@ -781,8 +778,7 @@ func doReinstall(ctx context.Context, client *metalv1.APIClient, d *schema.Resou
}

func resourceMetalDeviceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
meta.(*config.Config).AddModuleToMetalGoUserAgent(d)
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForSDK(d)

fdvIf, fdvOk := d.GetOk("force_detach_volumes")
fdv := false
Expand Down Expand Up @@ -821,8 +817,7 @@ func waitForActiveDevice(ctx context.Context, d *schema.ResourceData, meta inter
Pending: pending,
Target: targets,
Refresh: func() (interface{}, string, error) {
meta.(*config.Config).AddModuleToMetalGoUserAgent(d)
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForSDK(d)

device, _, err := client.DevicesApi.FindDeviceById(ctx, d.Id()).Include([]string{"project"}).Execute()
if err == nil {
Expand Down
5 changes: 2 additions & 3 deletions equinix/resource_metal_device_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ func testAccMetalDeviceExists(n string, device *metalv1.Device) resource.TestChe
return fmt.Errorf("No Record ID is set")
}

client := testAccProvider.Meta().(*config.Config).Metalgo
client := testAccProvider.Meta().(*config.Config).NewMetalClientForTesting()

foundDevice, _, err := client.DevicesApi.FindDeviceById(context.TODO(), rs.Primary.ID).Execute()
if err != nil {
Expand Down Expand Up @@ -1092,8 +1092,7 @@ func testAccWaitForMetalDeviceActive(project, deviceHostName string) resource.Im

meta := testAccProvider.Meta()
rd := new(schema.ResourceData)
meta.(*config.Config).AddModuleToMetalGoUserAgent(rd)
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForTesting()
resp, _, err := client.DevicesApi.FindProjectDevices(context.TODO(), rs.Primary.ID).Search(deviceHostName).Execute()
if err != nil {
return "", fmt.Errorf("error while fetching devices for project [%s], error: %w", rs.Primary.ID, err)
Expand Down
2 changes: 1 addition & 1 deletion internal/acceptance/ssh_key_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestAccCheckMetalSSHKeyExists(n string, key *metalv1.SSHKey) resource.TestC
return fmt.Errorf("No Record ID is set")
}

client := TestAccProvider.Meta().(*config.Config).Metalgo
client := TestAccProvider.Meta().(*config.Config).NewMetalClientForTesting()

foundKey, _, err := client.SSHKeysApi.FindSSHKeyById(context.Background(), rs.Primary.ID).Execute()
if err != nil {
Expand Down
105 changes: 73 additions & 32 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"path"
"regexp"
"runtime/debug"
"strings"
"time"

Expand All @@ -24,7 +25,6 @@ import (
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/meta"
"github.com/packethost/packngo"
xoauth2 "golang.org/x/oauth2"
)
Expand Down Expand Up @@ -95,15 +95,13 @@ type Config struct {
PageSize int
Token string

Ecx ecx.Client
Ne ne.Client
Metal *packngo.Client
Metalgo *metalv1.APIClient
Ecx ecx.Client
Ne ne.Client
Metal *packngo.Client

ecxUserAgent string
neUserAgent string
metalUserAgent string
metalGoUserAgent string
ecxUserAgent string
neUserAgent string
metalUserAgent string

TerraformVersion string
FabricClient *v4.APIClient
Expand Down Expand Up @@ -163,19 +161,18 @@ func (c *Config) Load(ctx context.Context) error {
ecxClient.SetPageSize(c.PageSize)
neClient.SetPageSize(c.PageSize)
}
c.ecxUserAgent = c.fullUserAgent("equinix/ecx-go")
c.ecxUserAgent = c.tfSdkUserAgent("equinix/ecx-go")
ecxClient.SetHeaders(map[string]string{
"User-agent": c.ecxUserAgent,
})
c.neUserAgent = c.fullUserAgent("equinix/ecx-go")
c.neUserAgent = c.tfSdkUserAgent("equinix/ecx-go")
neClient.SetHeaders(map[string]string{
"User-agent": c.neUserAgent,
})

c.Ecx = ecxClient
c.Ne = neClient
c.Metal = c.NewMetalClient()
c.Metalgo = c.NewMetalGoClient()
c.FabricClient = c.NewFabricClient()
return nil
}
Expand Down Expand Up @@ -204,6 +201,7 @@ func (c *Config) NewFabricClient() *v4.APIClient {
}

// NewMetalClient returns a new packngo client for accessing Equinix Metal's API.
// Deprecated: migrate to NewMetalClientForSdk or NewMetalClientForFramework instead
func (c *Config) NewMetalClient() *packngo.Client {
transport := http.DefaultTransport
// transport = &DumpTransport{http.DefaultTransport} // Debug only
Expand All @@ -218,13 +216,41 @@ func (c *Config) NewMetalClient() *packngo.Client {
baseURL, _ := url.Parse(c.BaseURL)
baseURL.Path = path.Join(baseURL.Path, metalBasePath) + "/"
client, _ := packngo.NewClientWithBaseURL(consumerToken, c.AuthToken, standardClient, baseURL.String())
client.UserAgent = c.fullUserAgent(client.UserAgent)
client.UserAgent = c.tfSdkUserAgent(client.UserAgent)
c.metalUserAgent = client.UserAgent
return client
}

// NewMetalGoClient returns a new metal-go client for accessing Equinix Metal's API.
func (c *Config) NewMetalGoClient() *metalv1.APIClient {
func (c *Config) NewMetalClientForSDK(d *schema.ResourceData) *metalv1.APIClient {
client := c.newMetalClient()

baseUserAgent := c.tfSdkUserAgent(client.GetConfig().UserAgent)
client.GetConfig().UserAgent = generateModuleUserAgentString(d, baseUserAgent)

return client
}

func (c *Config) NewMetalClientForFramework(ctx context.Context, meta tfsdk.Config) *metalv1.APIClient {
client := c.newMetalClient()

baseUserAgent := c.tfFrameworkUserAgent(client.GetConfig().UserAgent)
client.GetConfig().UserAgent = generateFwModuleUserAgentString(ctx, meta, baseUserAgent)

return client
}

// This is a short-term shim to allow tests to continue to have a client for cleanup and validation
// code that is outside of the resource or datasource under test
// Deprecated: when possible, API clients for test cleanup/validation should be moved to the acceptance package
func (c *Config) NewMetalClientForTesting() *metalv1.APIClient {
client := c.newMetalClient()

client.GetConfig().UserAgent = fmt.Sprintf("tf-acceptance-tests %v", client.GetConfig().UserAgent)

return client
}

func (c *Config) newMetalClient() *metalv1.APIClient {
transport := http.DefaultTransport
transport = logging.NewTransport("Equinix Metal (metal-go)", transport)
retryClient := retryablehttp.NewClient()
Expand All @@ -246,9 +272,7 @@ func (c *Config) NewMetalGoClient() *metalv1.APIClient {
}
configuration.HTTPClient = standardClient
configuration.AddDefaultHeader("X-Auth-Token", c.AuthToken)
configuration.UserAgent = c.fullUserAgent(configuration.UserAgent)
client := metalv1.NewAPIClient(configuration)
c.metalGoUserAgent = client.GetConfig().UserAgent
return client
}

Expand Down Expand Up @@ -282,10 +306,7 @@ func MetalRetryPolicy(ctx context.Context, resp *http.Response, err error) (bool
return false, nil
}

func terraformUserAgent(version string) string {
ua := fmt.Sprintf("HashiCorp Terraform/%s (+https://www.terraform.io) Terraform Plugin SDK/%s",
version, meta.SDKVersionString())

func appendUserAgentFromEnv(ua string) string {
if add := os.Getenv(uaEnvVar); add != "" {
add = strings.TrimSpace(add)
if len(add) > 0 {
Expand Down Expand Up @@ -320,10 +341,6 @@ func (c *Config) AddFwModuleToMetalUserAgent(ctx context.Context, meta tfsdk.Con
c.Metal.UserAgent = generateFwModuleUserAgentString(ctx, meta, c.metalUserAgent)
}

func (c *Config) AddFwModuleToMetalGoUserAgent(ctx context.Context, meta tfsdk.Config) {
c.Metalgo.GetConfig().UserAgent = generateFwModuleUserAgentString(ctx, meta, c.metalGoUserAgent)
}

func generateFwModuleUserAgentString(ctx context.Context, meta tfsdk.Config, baseUserAgent string) string {
var m ProviderMeta
diags := meta.Get(ctx, &m)
Expand All @@ -341,10 +358,6 @@ func (c *Config) AddModuleToMetalUserAgent(d *schema.ResourceData) {
c.Metal.UserAgent = generateModuleUserAgentString(d, c.metalUserAgent)
}

func (c *Config) AddModuleToMetalGoUserAgent(d *schema.ResourceData) {
c.Metalgo.GetConfig().UserAgent = generateModuleUserAgentString(d, c.metalGoUserAgent)
}

func generateModuleUserAgentString(d *schema.ResourceData, baseUserAgent string) string {
var m ProviderMeta
err := d.GetProviderMeta(&m)
Expand All @@ -359,8 +372,36 @@ func generateModuleUserAgentString(d *schema.ResourceData, baseUserAgent string)
return baseUserAgent
}

func (c *Config) fullUserAgent(suffix string) string {
tfUserAgent := terraformUserAgent(c.TerraformVersion)
userAgent := fmt.Sprintf("%s terraform-provider-equinix/%s %s", tfUserAgent, version.ProviderVersion, suffix)
func (c *Config) tfSdkUserAgent(suffix string) string {
sdkModulePath := "github.com/hashicorp/terraform-plugin-sdk"
baseUserAgent := fmt.Sprintf("HashiCorp Terraform/%s (+https://www.terraform.io) Terraform Plugin SDK/%s",
c.TerraformVersion, moduleVersionFromBuild(sdkModulePath))
baseUserAgent = appendUserAgentFromEnv(baseUserAgent)
userAgent := fmt.Sprintf("%s terraform-provider-equinix/%s %s", baseUserAgent, version.ProviderVersion, suffix)
return strings.TrimSpace(userAgent)
}

func (c *Config) tfFrameworkUserAgent(suffix string) string {
frameworkModulePath := "github.com/hashicorp/terraform-plugin-framework"
baseUserAgent := fmt.Sprintf("HashiCorp Terraform/%s (+https://www.terraform.io) Terraform Plugin Framework/%s",
c.TerraformVersion, moduleVersionFromBuild(frameworkModulePath))
baseUserAgent = appendUserAgentFromEnv(baseUserAgent)
userAgent := fmt.Sprintf("%s terraform-provider-equinix/%s %s", baseUserAgent, version.ProviderVersion, suffix)
return strings.TrimSpace(userAgent)
}

func moduleVersionFromBuild(modulePath string) string {
buildInfo, ok := debug.ReadBuildInfo()

if !ok {
return "buildinfo-failed"
}

for _, dependency := range buildInfo.Deps {
if dependency.Path == modulePath {
return dependency.Version
}
}

return "unknown-version"
}
4 changes: 2 additions & 2 deletions internal/datalist/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type ResourceConfig struct {
// Return all of the records on which the data list resource should operate.
// The `meta` argument is the same meta argument passed into the resource's Read
// function.
GetRecords func(meta interface{}, extra map[string]interface{}) ([]interface{}, error)
GetRecords func(d *schema.ResourceData, meta interface{}, extra map[string]interface{}) ([]interface{}, error)

// Extra parameters to expose on the datasource alongside `filter` and `sort`.
ExtraQuerySchema map[string]*schema.Schema
Expand Down Expand Up @@ -89,7 +89,7 @@ func dataListResourceRead(config *ResourceConfig) schema.ReadContextFunc {
extra[attr] = d.Get(attr)
}

records, err := config.GetRecords(meta, extra)
records, err := config.GetRecords(d, meta, extra)
if err != nil {
return diag.Errorf("Unable to load records: %s", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/resources/metal/project/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func DataSource() *schema.Resource {
}

func dataSourceMetalProjectRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForSDK(d)
nameRaw, nameOK := d.GetOk("name")
projectIdRaw, projectIdOK := d.GetOk("project_id")

Expand Down
12 changes: 4 additions & 8 deletions internal/resources/metal/project/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,7 @@ func expandBGPConfig(d *schema.ResourceData) (*metalv1.BgpConfigRequestInput, er
}

func resourceMetalProjectCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
meta.(*config.Config).AddModuleToMetalGoUserAgent(d)
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForSDK(d)

createRequest := metalv1.ProjectCreateFromRootInput{
Name: d.Get("name").(string),
Expand Down Expand Up @@ -177,8 +176,7 @@ func resourceMetalProjectCreate(ctx context.Context, d *schema.ResourceData, met
}

func resourceMetalProjectRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
meta.(*config.Config).AddModuleToMetalUserAgent(d)
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForSDK(d)

proj, resp, err := client.ProjectsApi.FindProjectById(ctx, d.Id()).Execute()
if err != nil {
Expand Down Expand Up @@ -249,8 +247,7 @@ func flattenBGPConfig(l *metalv1.BgpConfig) []map[string]interface{} {
}

func resourceMetalProjectUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
meta.(*config.Config).AddModuleToMetalGoUserAgent(d)
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForSDK(d)
updateRequest := metalv1.ProjectUpdateInput{}
if d.HasChange("name") {
pName := d.Get("name").(string)
Expand Down Expand Up @@ -304,8 +301,7 @@ func resourceMetalProjectUpdate(ctx context.Context, d *schema.ResourceData, met
}

func resourceMetalProjectDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
meta.(*config.Config).AddModuleToMetalGoUserAgent(d)
client := meta.(*config.Config).Metalgo
client := meta.(*config.Config).NewMetalClientForSDK(d)

resp, err := client.ProjectsApi.DeleteProject(ctx, d.Id()).Execute()
if equinix_errors.IgnoreHttpResponseErrors(equinix_errors.HttpForbidden, equinix_errors.HttpNotFound)(resp, err) != nil {
Expand Down
3 changes: 1 addition & 2 deletions internal/resources/metal/project_ssh_key/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ func (r *DataSource) Read(
req datasource.ReadRequest,
resp *datasource.ReadResponse,
) {
r.Meta.AddFwModuleToMetalGoUserAgent(ctx, req.ProviderMeta)
client := r.Meta.Metalgo
client := r.Meta.NewMetalClientForFramework(ctx, req.ProviderMeta)

// Retrieve values from plan
var data DataSourceModel
Expand Down
Loading

0 comments on commit b910a66

Please sign in to comment.