Skip to content

Commit 00f9ba0

Browse files
committed
add client caching by config, extract client and logging packages
It doesn't make sense for each provider (sdk v2 + framework) to initialize its own client because it has to go through ping and service discovery requests. This may also slightly speed up testing since each test begins by initializing a new client.
1 parent 7b9abd9 commit 00f9ba0

File tree

11 files changed

+710
-567
lines changed

11 files changed

+710
-567
lines changed

internal/client/client.go

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

0 commit comments

Comments
 (0)