Skip to content

Commit

Permalink
Merge pull request #1097 from hashicorp/TF-9803-terraform-provider-tf…
Browse files Browse the repository at this point in the history
…e-gh-issue-1086-v-0-49-0-no-longers-finds-local-credentials
  • Loading branch information
brandonc authored Oct 10, 2023
2 parents ed14850 + 140f199 commit 8f1bed3
Show file tree
Hide file tree
Showing 21 changed files with 985 additions and 664 deletions.
4 changes: 2 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ issues:
- staticcheck
path: (data_source_outputs|resource_tfe_agent_pool|resource_tfe_notification_configuration|resource_tfe_organization|resource_tfe_registry_module|
-|resource_tfe_run_trigger|resource_tfe_team_access|resource_tfe_organization_membership|resource_tfe_policy_set|resource_tfe_team_member|
-|resource_tfe_workspace|resource_tfe_team_organization_member|resource_tfe_variable_set)\.go
-|resource_tfe_workspace|resource_tfe_team_organization_member|resource_tfe_variable_set|resource_tfe_team_project_access)\.go
text: "SA1019"
- linters:
- wrapcheck
path: (config_unix|data_source_slug|data_source_workspace|provider|resource_tfe_variable|provider_test)\.go
path: (config_unix|data_source_slug|data_source_workspace|provider|resource_tfe_variable|resource_tfe_workspace|provider_test)\.go
text: "error returned from external package is unwrapped"
- linters:
- wrapcheck
Expand Down
2 changes: 1 addition & 1 deletion GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ vet:
lint:
@golangci-lint run ; if [ $$? -ne 0 ]; then \
echo ""; \
echo "golangci-lint found some code style issues." \
echo "golangci-lint found some code style issues."; \
exit 1; \
fi

Expand Down
266 changes: 266 additions & 0 deletions internal/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package client

import (
"errors"
"fmt"
"log"
"net/url"
"os"
"sort"
"strings"
"sync"

tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/go-version"
providerVersion "github.com/hashicorp/terraform-provider-tfe/version"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/hashicorp/terraform-svchost/disco"
)

const (
DefaultHostname = "app.terraform.io"
)

var (
ErrMissingAuthToken = errors.New("required token could not be found. Please set the token using an input variable in the provider configuration block or by using the TFE_TOKEN environment variable")
tfeServiceIDs = []string{"tfe.v2.2"}
)

type ClientConfigMap struct {
mu sync.Mutex
values map[string]*tfe.Client
}

func (c *ClientConfigMap) GetByConfig(config *ClientConfiguration) *tfe.Client {
if c.mu.TryLock() {
defer c.Unlock()
}

return c.values[config.Key()]
}

func (c *ClientConfigMap) Lock() {
c.mu.Lock()
}

func (c *ClientConfigMap) Unlock() {
c.mu.Unlock()
}

func (c *ClientConfigMap) Set(client *tfe.Client, config *ClientConfiguration) {
if c.mu.TryLock() {
defer c.Unlock()
}
c.values[config.Key()] = client
}

func getTokenFromEnv() string {
log.Printf("[DEBUG] TFE_TOKEN used for token value")
return os.Getenv("TFE_TOKEN")
}

func getTokenFromCreds(services *disco.Disco, hostname svchost.Hostname) string {
log.Printf("[DEBUG] Attempting to fetch token from Terraform CLI configuration for hostname %q...", hostname)
creds, err := services.CredentialsForHost(hostname)
if err != nil {
log.Printf("[DEBUG] Failed to get credentials for %s: %s (ignoring)", hostname, err)
}
if creds != nil {
return creds.Token()
}
return ""
}

// GetClient encapsulates the logic for configuring a go-tfe client instance for
// the provider, including fallback to values from environment variables. This
// is useful because we're muxing multiple provider servers together and each
// one needs an identically configured client.
//
// Internally, this function caches configured clients using the specified
// parameters
func GetClient(tfeHost, token string, insecure bool) (*tfe.Client, error) {
config, err := configure(tfeHost, token, insecure)
if err != nil {
return nil, err
}

clientCache.Lock()
defer clientCache.Unlock()

// Try to retrieve the client from cache
cached := clientCache.GetByConfig(config)
if cached != nil {
return cached, nil
}

// Discover the Terraform Enterprise address.
host, err := config.Services.Discover(config.TFEHost)
if err != nil {
return nil, fmt.Errorf("failed to create client: %w", err)
}

// Get the full Terraform Enterprise service address.
var address *url.URL
var discoErr error
for _, tfeServiceID := range tfeServiceIDs {
service, err := host.ServiceURL(tfeServiceID)
target := &disco.ErrVersionNotSupported{}
if err != nil && !errors.As(err, &target) {
return nil, fmt.Errorf("failed to create client: %w", err)
}

// If discoErr is nil we save the first error. When multiple services
// are checked and we found one that didn't give an error we need to
// reset the discoErr. So if err is nil, we assign it as well.
if discoErr == nil || err == nil {
discoErr = err
}

if service != nil {
address = service
break
}
}

if providerVersion.ProviderVersion != "dev" {
// We purposefully ignore the error and return the previous error, as
// checking for version constraints is considered optional.
constraints, _ := host.VersionConstraints(tfeServiceIDs[0], "tfe-provider")

// First check any constraints we might have received.
if constraints != nil {
if err := CheckConstraints(constraints); err != nil {
return nil, err
}
}
}

// When we don't have any constraints errors, also check for discovery
// errors before we continue.
if discoErr != nil {
return nil, discoErr
}

// Create a new TFE client.
client, err := tfe.NewClient(&tfe.Config{
Address: address.String(),
Token: config.Token,
HTTPClient: config.HTTPClient,
})
if err != nil {
return nil, fmt.Errorf("failed to create client: %w", err)
}

client.RetryServerErrors(true)
clientCache.Set(client, config)

return client, nil
}

// CheckConstraints checks service version constrains against our own
// version and returns rich and informational diagnostics in case any
// incompatibilities are detected.
func CheckConstraints(c *disco.Constraints) error {
if c == nil || c.Minimum == "" || c.Maximum == "" {
return nil
}

// Generate a parsable constraints string.
excluding := ""
if len(c.Excluding) > 0 {
excluding = fmt.Sprintf(", != %s", strings.Join(c.Excluding, ", != "))
}
constStr := fmt.Sprintf(">= %s%s, <= %s", c.Minimum, excluding, c.Maximum)

// Create the constraints to check against.
constraints, err := version.NewConstraint(constStr)
if err != nil {
return checkConstraintsWarning(err)
}

// Create the version to check.
v, err := version.NewVersion(providerVersion.ProviderVersion)
if err != nil {
return checkConstraintsWarning(err)
}

// Return if we satisfy all constraints.
if constraints.Check(v) {
return nil
}

// Find out what action (upgrade/downgrade) we should advice.
minimum, err := version.NewVersion(c.Minimum)
if err != nil {
return checkConstraintsWarning(err)
}

maximum, err := version.NewVersion(c.Maximum)
if err != nil {
return checkConstraintsWarning(err)
}

var excludes []*version.Version
for _, exclude := range c.Excluding {
v, err := version.NewVersion(exclude)
if err != nil {
return checkConstraintsWarning(err)
}
excludes = append(excludes, v)
}

// Sort all the excludes.
sort.Sort(version.Collection(excludes))

var action, toVersion string
switch {
case minimum.GreaterThan(v):
action = "upgrade"
toVersion = ">= " + minimum.String()
case maximum.LessThan(v):
action = "downgrade"
toVersion = "<= " + maximum.String()
case len(excludes) > 0:
// Get the latest excluded version.
action = "upgrade"
toVersion = "> " + excludes[len(excludes)-1].String()
}

switch {
case len(excludes) == 1:
excluding = fmt.Sprintf(", excluding version %s", excludes[0].String())
case len(excludes) > 1:
var vs []string
for _, v := range excludes {
vs = append(vs, v.String())
}
excluding = fmt.Sprintf(", excluding versions %s", strings.Join(vs, ", "))
default:
excluding = ""
}

summary := fmt.Sprintf("Incompatible TFE provider version v%s", v.String())
details := fmt.Sprintf(
"The configured Terraform Enterprise backend is compatible with TFE provider\n"+
"versions >= %s, <= %s%s.", c.Minimum, c.Maximum, excluding,
)

if action != "" && toVersion != "" {
summary = fmt.Sprintf("Please %s the TFE provider to %s", action, toVersion)
}

// Return the customized and informational error message.
return fmt.Errorf("%s\n\n%s", summary, details)
}

func checkConstraintsWarning(err error) error {
return fmt.Errorf(
"failed to check version constraints: %v\n\n"+
"checking version constraints is considered optional, but this is an\n"+
"unexpected error which should be reported",
err,
)
}
Loading

0 comments on commit 8f1bed3

Please sign in to comment.