Skip to content

Commit 4e13136

Browse files
authored
Merge pull request #1034 from hashicorp/TF-9008-tfe-provider-is-doing-service-discovery-twice-sdk-framework
add client caching by config, extract client and logging packages
2 parents 7b9abd9 + c79b543 commit 4e13136

File tree

12 files changed

+707
-567
lines changed

12 files changed

+707
-567
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
BUG FIXES:
44
* `r/tfe_team_project_access`: Fixes a panic that occurs when the client is configured against an older TFE release, by @sebasslash [1011](https://github.com/hashicorp/terraform-provider-tfe/pull/1011)
5+
* The provider no longer makes two service discovery requests per provider config, by @brandonc [1034](https://github.com/hashicorp/terraform-provider-tfe/pull/1034)
56

67
FEATURES:
78
* `d/tfe_organization_membership`: Add `organization_membership_id` attribute, by @laurenolivia [997](https://github.com/hashicorp/terraform-provider-tfe/pull/997)

internal/client/client.go

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package client
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"log"
10+
"net/url"
11+
"os"
12+
"sort"
13+
"strings"
14+
"sync"
15+
16+
tfe "github.com/hashicorp/go-tfe"
17+
"github.com/hashicorp/go-version"
18+
providerVersion "github.com/hashicorp/terraform-provider-tfe/version"
19+
svchost "github.com/hashicorp/terraform-svchost"
20+
"github.com/hashicorp/terraform-svchost/disco"
21+
)
22+
23+
const (
24+
DefaultHostname = "app.terraform.io"
25+
)
26+
27+
var (
28+
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")
29+
tfeServiceIDs = []string{"tfe.v2.2"}
30+
)
31+
32+
type ClientConfigMap struct {
33+
mu sync.Mutex
34+
values map[string]*tfe.Client
35+
}
36+
37+
func (c *ClientConfigMap) GetByConfig(config *ClientConfiguration) *tfe.Client {
38+
if c.mu.TryLock() {
39+
defer c.Unlock()
40+
}
41+
42+
return c.values[config.Key()]
43+
}
44+
45+
func (c *ClientConfigMap) Lock() {
46+
c.mu.Lock()
47+
}
48+
49+
func (c *ClientConfigMap) Unlock() {
50+
c.mu.Unlock()
51+
}
52+
53+
func (c *ClientConfigMap) Set(client *tfe.Client, config *ClientConfiguration) {
54+
if c.mu.TryLock() {
55+
defer c.Unlock()
56+
}
57+
c.values[config.Key()] = client
58+
}
59+
60+
func getTokenFromEnv() string {
61+
log.Printf("[DEBUG] TFE_TOKEN used for token value")
62+
return os.Getenv("TFE_TOKEN")
63+
}
64+
65+
func getTokenFromCreds(services *disco.Disco, hostname svchost.Hostname) string {
66+
log.Printf("[DEBUG] Attempting to fetch token from Terraform CLI configuration for hostname %s...", hostname)
67+
creds, err := services.CredentialsForHost(hostname)
68+
if err != nil {
69+
log.Printf("[DEBUG] Failed to get credentials for %s: %s (ignoring)", hostname, err)
70+
}
71+
if creds != nil {
72+
return creds.Token()
73+
}
74+
return ""
75+
}
76+
77+
// GetClient encapsulates the logic for configuring a go-tfe client instance for
78+
// the provider, including fallback to values from environment variables. This
79+
// is useful because we're muxing multiple provider servers together and each
80+
// one needs an identically configured client.
81+
//
82+
// Internally, this function caches configured clients using the specified
83+
// parameters
84+
func GetClient(tfeHost, token string, insecure bool) (*tfe.Client, error) {
85+
config, err := configure(tfeHost, token, insecure)
86+
if err != nil {
87+
return nil, err
88+
}
89+
90+
clientCache.Lock()
91+
defer clientCache.Unlock()
92+
93+
// Try to retrieve the client from cache
94+
cached := clientCache.GetByConfig(config)
95+
if cached != nil {
96+
return cached, nil
97+
}
98+
99+
// Discover the Terraform Enterprise address.
100+
host, err := config.Services.Discover(config.TFEHost)
101+
if err != nil {
102+
return nil, err
103+
}
104+
105+
// Get the full Terraform Enterprise service address.
106+
var address *url.URL
107+
var discoErr error
108+
for _, tfeServiceID := range tfeServiceIDs {
109+
service, err := host.ServiceURL(tfeServiceID)
110+
if _, ok := err.(*disco.ErrVersionNotSupported); !ok && err != nil {
111+
return nil, err
112+
}
113+
// If discoErr is nil we save the first error. When multiple services
114+
// are checked and we found one that didn't give an error we need to
115+
// reset the discoErr. So if err is nil, we assign it as well.
116+
if discoErr == nil || err == nil {
117+
discoErr = err
118+
}
119+
if service != nil {
120+
address = service
121+
break
122+
}
123+
}
124+
125+
if providerVersion.ProviderVersion != "dev" {
126+
// We purposefully ignore the error and return the previous error, as
127+
// checking for version constraints is considered optional.
128+
constraints, _ := host.VersionConstraints(tfeServiceIDs[0], "tfe-provider")
129+
130+
// First check any constraints we might have received.
131+
if constraints != nil {
132+
if err := CheckConstraints(constraints); err != nil {
133+
return nil, err
134+
}
135+
}
136+
}
137+
138+
// When we don't have any constraints errors, also check for discovery
139+
// errors before we continue.
140+
if discoErr != nil {
141+
return nil, discoErr
142+
}
143+
144+
// Create a new TFE client.
145+
client, err := tfe.NewClient(&tfe.Config{
146+
Address: address.String(),
147+
Token: token,
148+
HTTPClient: config.HTTPClient,
149+
})
150+
if err != nil {
151+
return nil, err
152+
}
153+
154+
client.RetryServerErrors(true)
155+
clientCache.Set(client, config)
156+
157+
return client, nil
158+
}
159+
160+
// CheckConstraints checks service version constrains against our own
161+
// version and returns rich and informational diagnostics in case any
162+
// incompatibilities are detected.
163+
func CheckConstraints(c *disco.Constraints) error {
164+
if c == nil || c.Minimum == "" || c.Maximum == "" {
165+
return nil
166+
}
167+
168+
// Generate a parsable constraints string.
169+
excluding := ""
170+
if len(c.Excluding) > 0 {
171+
excluding = fmt.Sprintf(", != %s", strings.Join(c.Excluding, ", != "))
172+
}
173+
constStr := fmt.Sprintf(">= %s%s, <= %s", c.Minimum, excluding, c.Maximum)
174+
175+
// Create the constraints to check against.
176+
constraints, err := version.NewConstraint(constStr)
177+
if err != nil {
178+
return checkConstraintsWarning(err)
179+
}
180+
181+
// Create the version to check.
182+
v, err := version.NewVersion(providerVersion.ProviderVersion)
183+
if err != nil {
184+
return checkConstraintsWarning(err)
185+
}
186+
187+
// Return if we satisfy all constraints.
188+
if constraints.Check(v) {
189+
return nil
190+
}
191+
192+
// Find out what action (upgrade/downgrade) we should advice.
193+
minimum, err := version.NewVersion(c.Minimum)
194+
if err != nil {
195+
return checkConstraintsWarning(err)
196+
}
197+
198+
maximum, err := version.NewVersion(c.Maximum)
199+
if err != nil {
200+
return checkConstraintsWarning(err)
201+
}
202+
203+
var excludes []*version.Version
204+
for _, exclude := range c.Excluding {
205+
v, err := version.NewVersion(exclude)
206+
if err != nil {
207+
return checkConstraintsWarning(err)
208+
}
209+
excludes = append(excludes, v)
210+
}
211+
212+
// Sort all the excludes.
213+
sort.Sort(version.Collection(excludes))
214+
215+
var action, toVersion string
216+
switch {
217+
case minimum.GreaterThan(v):
218+
action = "upgrade"
219+
toVersion = ">= " + minimum.String()
220+
case maximum.LessThan(v):
221+
action = "downgrade"
222+
toVersion = "<= " + maximum.String()
223+
case len(excludes) > 0:
224+
// Get the latest excluded version.
225+
action = "upgrade"
226+
toVersion = "> " + excludes[len(excludes)-1].String()
227+
}
228+
229+
switch {
230+
case len(excludes) == 1:
231+
excluding = fmt.Sprintf(", excluding version %s", excludes[0].String())
232+
case len(excludes) > 1:
233+
var vs []string
234+
for _, v := range excludes {
235+
vs = append(vs, v.String())
236+
}
237+
excluding = fmt.Sprintf(", excluding versions %s", strings.Join(vs, ", "))
238+
default:
239+
excluding = ""
240+
}
241+
242+
summary := fmt.Sprintf("Incompatible TFE provider version v%s", v.String())
243+
details := fmt.Sprintf(
244+
"The configured Terraform Enterprise backend is compatible with TFE provider\n"+
245+
"versions >= %s, <= %s%s.", c.Minimum, c.Maximum, excluding,
246+
)
247+
248+
if action != "" && toVersion != "" {
249+
summary = fmt.Sprintf("Please %s the TFE provider to %s", action, toVersion)
250+
}
251+
252+
// Return the customized and informational error message.
253+
return fmt.Errorf("%s\n\n%s", summary, details)
254+
}
255+
256+
func checkConstraintsWarning(err error) error {
257+
return fmt.Errorf(
258+
"failed to check version constraints: %v\n\n"+
259+
"checking version constraints is considered optional, but this is an\n"+
260+
"unexpected error which should be reported",
261+
err,
262+
)
263+
}

0 commit comments

Comments
 (0)