Skip to content

Commit 3c57def

Browse files
Set-up muxing with new provider framework (#1206)
* Set-up muxing with new provider framework This PR sets up the migration from the old provider SDK (what we're using) to the new plugin framework The migration process is outlined here: https://developer.hashicorp.com/terraform/plugin/framework/migrating Essentially, it consists of running two providers side by side with the exact same config and migrate resources one by one The client configuration is now done using the new plugin framework config (go struct) object, and the legacy provider config (d.Get(...)) is translated into the plugin framework config This PR is a no-op in the sense that no current resources are modified This PR is also the first step to use TF code generation (https://developer.hashicorp.com/terraform/plugin/code-generation) since it generates TF plugin framework code (not old SDK code) * Apply suggestions from code review Co-authored-by: Selene <[email protected]> --------- Co-authored-by: Selene <[email protected]>
1 parent b76a5d4 commit 3c57def

File tree

10 files changed

+1047
-654
lines changed

10 files changed

+1047
-654
lines changed

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ require (
1919
github.com/hashicorp/go-retryablehttp v0.7.5
2020
github.com/hashicorp/hcl/v2 v2.19.1
2121
github.com/hashicorp/terraform-plugin-docs v0.16.0
22+
github.com/hashicorp/terraform-plugin-framework v1.4.2
23+
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
24+
github.com/hashicorp/terraform-plugin-go v0.19.0
25+
github.com/hashicorp/terraform-plugin-mux v0.12.0
2226
github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0
2327
github.com/prometheus/common v0.45.0
2428
golang.org/x/text v0.14.0
@@ -61,7 +65,6 @@ require (
6165
github.com/hashicorp/logutils v1.0.0 // indirect
6266
github.com/hashicorp/terraform-exec v0.19.0 // indirect
6367
github.com/hashicorp/terraform-json v0.17.1 // indirect
64-
github.com/hashicorp/terraform-plugin-go v0.19.0 // indirect
6568
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
6669
github.com/hashicorp/terraform-registry-address v0.2.2 // indirect
6770
github.com/hashicorp/terraform-svchost v0.1.1 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,16 @@ github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQH
163163
github.com/hashicorp/terraform-json v0.17.1/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o=
164164
github.com/hashicorp/terraform-plugin-docs v0.16.0 h1:UmxFr3AScl6Wged84jndJIfFccGyBZn52KtMNsS12dI=
165165
github.com/hashicorp/terraform-plugin-docs v0.16.0/go.mod h1:M3ZrlKBJAbPMtNOPwHicGi1c+hZUh7/g0ifT/z7TVfA=
166+
github.com/hashicorp/terraform-plugin-framework v1.4.2 h1:P7a7VP1GZbjc4rv921Xy5OckzhoiO3ig6SGxwelD2sI=
167+
github.com/hashicorp/terraform-plugin-framework v1.4.2/go.mod h1:GWl3InPFZi2wVQmdVnINPKys09s9mLmTZr95/ngLnbY=
168+
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc=
169+
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg=
166170
github.com/hashicorp/terraform-plugin-go v0.19.0 h1:BuZx/6Cp+lkmiG0cOBk6Zps0Cb2tmqQpDM3iAtnhDQU=
167171
github.com/hashicorp/terraform-plugin-go v0.19.0/go.mod h1:EhRSkEPNoylLQntYsk5KrDHTZJh9HQoumZXbOGOXmec=
168172
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
169173
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
174+
github.com/hashicorp/terraform-plugin-mux v0.12.0 h1:TJlmeslQ11WlQtIFAfth0vXx+gSNgvMEng2Rn9z3WZY=
175+
github.com/hashicorp/terraform-plugin-mux v0.12.0/go.mod h1:8MR0AgmV+Q03DIjyrAKxXyYlq2EUnYBQP8gxAAA0zeM=
170176
github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0 h1:X7vB6vn5tON2b49ILa4W7mFAsndeqJ7bZFOGbVO+0Cc=
171177
github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0/go.mod h1:ydFcxbdj6klCqYEPkPvdvFKiNGKZLUs+896ODUXCyao=
172178
github.com/hashicorp/terraform-registry-address v0.2.2 h1:lPQBg403El8PPicg/qONZJDC6YlgCVbWDtNmmZKtBno=
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
package provider
2+
3+
import (
4+
"crypto/tls"
5+
"crypto/x509"
6+
"errors"
7+
"fmt"
8+
"net/url"
9+
"os"
10+
"strings"
11+
"time"
12+
13+
onCallAPI "github.com/grafana/amixr-api-go-client"
14+
gapi "github.com/grafana/grafana-api-golang-client"
15+
goapi "github.com/grafana/grafana-openapi-client-go/client"
16+
"github.com/grafana/machine-learning-go-client/mlapi"
17+
SMAPI "github.com/grafana/synthetic-monitoring-api-go-client"
18+
19+
"github.com/go-openapi/strfmt"
20+
"github.com/grafana/terraform-provider-grafana/internal/common"
21+
"github.com/grafana/terraform-provider-grafana/internal/resources/grafana"
22+
"github.com/hashicorp/go-cleanhttp"
23+
"github.com/hashicorp/go-retryablehttp"
24+
"github.com/hashicorp/terraform-plugin-framework/attr"
25+
"github.com/hashicorp/terraform-plugin-framework/types"
26+
)
27+
28+
func createClients(providerConfig frameworkProviderConfig) (*common.Client, error) {
29+
var err error
30+
c := &common.Client{}
31+
if !providerConfig.Auth.IsNull() {
32+
c.GrafanaAPIURL, c.GrafanaAPIConfig, c.GrafanaAPI, err = createGrafanaClient(providerConfig)
33+
if err != nil {
34+
return nil, err
35+
}
36+
if c.GrafanaAPIURLParsed, err = url.Parse(c.GrafanaAPIURL); err != nil {
37+
return nil, err
38+
}
39+
c.GrafanaOAPI, err = createGrafanaOAPIClient(providerConfig)
40+
if err != nil {
41+
return nil, err
42+
}
43+
c.MLAPI, err = createMLClient(c.GrafanaAPIURL, c.GrafanaAPIConfig)
44+
if err != nil {
45+
return nil, err
46+
}
47+
}
48+
if !providerConfig.CloudAPIKey.IsNull() {
49+
c.GrafanaCloudAPI, err = createCloudClient(providerConfig)
50+
if err != nil {
51+
return nil, err
52+
}
53+
}
54+
if !providerConfig.SMAccessToken.IsNull() {
55+
retryClient := retryablehttp.NewClient()
56+
retryClient.RetryMax = int(providerConfig.Retries.ValueInt64())
57+
if wait := providerConfig.RetryWait.ValueInt64(); wait > 0 {
58+
retryClient.RetryWaitMin = time.Second * time.Duration(wait)
59+
retryClient.RetryWaitMax = time.Second * time.Duration(wait)
60+
}
61+
62+
c.SMAPI = SMAPI.NewClient(providerConfig.SMURL.ValueString(), providerConfig.SMAccessToken.ValueString(), retryClient.StandardClient())
63+
}
64+
if !providerConfig.OncallAccessToken.IsNull() {
65+
var onCallClient *onCallAPI.Client
66+
onCallClient, err = createOnCallClient(providerConfig)
67+
if err != nil {
68+
return nil, err
69+
}
70+
onCallClient.UserAgent = providerConfig.UserAgent.ValueString()
71+
c.OnCallClient = onCallClient
72+
}
73+
74+
grafana.StoreDashboardSHA256 = providerConfig.StoreDashboardSha256.ValueBool()
75+
76+
return c, nil
77+
}
78+
79+
func createGrafanaClient(providerConfig frameworkProviderConfig) (string, *gapi.Config, *gapi.Client, error) {
80+
cli := cleanhttp.DefaultClient()
81+
transport := cleanhttp.DefaultTransport()
82+
// limiting the amount of concurrent HTTP connections from the provider
83+
// makes it not overload the API and DB
84+
transport.MaxConnsPerHost = 2
85+
86+
tlsClientConfig, err := parseTLSconfig(providerConfig)
87+
if err != nil {
88+
return "", nil, nil, err
89+
}
90+
transport.TLSClientConfig = tlsClientConfig
91+
cli.Transport = transport
92+
93+
apiURL := providerConfig.URL.ValueString()
94+
95+
userInfo, orgID, apiKey, err := parseAuth(providerConfig)
96+
if err != nil {
97+
return "", nil, nil, err
98+
}
99+
100+
cfg := gapi.Config{
101+
Client: cli,
102+
NumRetries: int(providerConfig.Retries.ValueInt64()),
103+
RetryTimeout: time.Second * time.Duration(providerConfig.RetryWait.ValueInt64()),
104+
RetryStatusCodes: setToStringArray(providerConfig.RetryStatusCodes.Elements()),
105+
BasicAuth: userInfo,
106+
OrgID: orgID,
107+
APIKey: apiKey,
108+
}
109+
110+
if cfg.HTTPHeaders, err = getHTTPHeadersMap(providerConfig); err != nil {
111+
return "", nil, nil, err
112+
}
113+
114+
gclient, err := gapi.New(apiURL, cfg)
115+
if err != nil {
116+
return "", nil, nil, err
117+
}
118+
return apiURL, &cfg, gclient, nil
119+
}
120+
121+
func createGrafanaOAPIClient(providerConfig frameworkProviderConfig) (*goapi.GrafanaHTTPAPI, error) {
122+
tlsClientConfig, err := parseTLSconfig(providerConfig)
123+
if err != nil {
124+
return nil, err
125+
}
126+
127+
u, err := url.Parse(providerConfig.URL.ValueString())
128+
if err != nil {
129+
return nil, fmt.Errorf("failed to parse API url: %v", err.Error())
130+
}
131+
apiPath, err := url.JoinPath(u.Path, "api")
132+
if err != nil {
133+
return nil, fmt.Errorf("failed to join API path: %v", err.Error())
134+
}
135+
136+
userInfo, orgID, apiKey, err := parseAuth(providerConfig)
137+
if err != nil {
138+
return nil, err
139+
}
140+
141+
cfg := goapi.TransportConfig{
142+
Host: u.Host,
143+
BasePath: apiPath,
144+
Schemes: []string{u.Scheme},
145+
NumRetries: int(providerConfig.Retries.ValueInt64()),
146+
RetryTimeout: time.Second * time.Duration(providerConfig.RetryWait.ValueInt64()),
147+
RetryStatusCodes: setToStringArray(providerConfig.RetryStatusCodes.Elements()),
148+
TLSConfig: tlsClientConfig,
149+
BasicAuth: userInfo,
150+
OrgID: orgID,
151+
APIKey: apiKey,
152+
}
153+
154+
if cfg.HTTPHeaders, err = getHTTPHeadersMap(providerConfig); err != nil {
155+
return nil, err
156+
}
157+
158+
return goapi.NewHTTPClientWithConfig(strfmt.Default, &cfg), nil
159+
}
160+
161+
func createMLClient(url string, grafanaCfg *gapi.Config) (*mlapi.Client, error) {
162+
mlcfg := mlapi.Config{
163+
BasicAuth: grafanaCfg.BasicAuth,
164+
BearerToken: grafanaCfg.APIKey,
165+
Client: grafanaCfg.Client,
166+
NumRetries: grafanaCfg.NumRetries,
167+
}
168+
mlURL := url
169+
if !strings.HasSuffix(mlURL, "/") {
170+
mlURL += "/"
171+
}
172+
mlURL += "api/plugins/grafana-ml-app/resources"
173+
mlclient, err := mlapi.New(mlURL, mlcfg)
174+
if err != nil {
175+
return nil, err
176+
}
177+
return mlclient, nil
178+
}
179+
180+
func createCloudClient(providerConfig frameworkProviderConfig) (*gapi.Client, error) {
181+
cfg := gapi.Config{
182+
APIKey: providerConfig.CloudAPIKey.ValueString(),
183+
NumRetries: int(providerConfig.Retries.ValueInt64()),
184+
RetryTimeout: time.Second * time.Duration(providerConfig.RetryWait.ValueInt64()),
185+
}
186+
187+
var err error
188+
if cfg.HTTPHeaders, err = getHTTPHeadersMap(providerConfig); err != nil {
189+
return nil, err
190+
}
191+
192+
return gapi.New(providerConfig.CloudAPIURL.ValueString(), cfg)
193+
}
194+
195+
func createOnCallClient(providerConfig frameworkProviderConfig) (*onCallAPI.Client, error) {
196+
return onCallAPI.New(providerConfig.OncallURL.ValueString(), providerConfig.OncallAccessToken.ValueString())
197+
}
198+
199+
// Sets a custom HTTP Header on all requests coming from the Grafana Terraform Provider to Grafana-Terraform-Provider: true
200+
// in addition to any headers set within the `http_headers` field or the `GRAFANA_HTTP_HEADERS` environment variable
201+
func getHTTPHeadersMap(providerConfig frameworkProviderConfig) (map[string]string, error) {
202+
headers := map[string]string{"Grafana-Terraform-Provider": "true"}
203+
for k, v := range providerConfig.HTTPHeaders.Elements() {
204+
if vString, ok := v.(types.String); ok {
205+
headers[k] = vString.ValueString()
206+
} else {
207+
return nil, fmt.Errorf("invalid header value for %s: %v", k, v)
208+
}
209+
}
210+
211+
return headers, nil
212+
}
213+
214+
func createTempFileIfLiteral(value string) (path string, tempFile bool, err error) {
215+
if value == "" {
216+
return "", false, nil
217+
}
218+
219+
if _, err := os.Stat(value); errors.Is(err, os.ErrNotExist) {
220+
// value is not a file path, assume it's a literal
221+
f, err := os.CreateTemp("", "grafana-provider-tls")
222+
if err != nil {
223+
return "", false, err
224+
}
225+
if _, err := f.WriteString(value); err != nil {
226+
return "", false, err
227+
}
228+
if err := f.Close(); err != nil {
229+
return "", false, err
230+
}
231+
return f.Name(), true, nil
232+
}
233+
234+
return value, false, nil
235+
}
236+
237+
func parseAuth(providerConfig frameworkProviderConfig) (*url.Userinfo, int64, string, error) {
238+
auth := strings.SplitN(providerConfig.Auth.ValueString(), ":", 2)
239+
var orgID int64 = 1
240+
if !providerConfig.OrgID.IsNull() {
241+
orgID = providerConfig.OrgID.ValueInt64()
242+
}
243+
244+
if len(auth) == 2 {
245+
return url.UserPassword(auth[0], auth[1]), orgID, "", nil
246+
} else if auth[0] != "anonymous" {
247+
if orgID > 1 {
248+
return nil, 0, "", fmt.Errorf("org_id is only supported with basic auth. API keys are already org-scoped")
249+
}
250+
return nil, 0, auth[0], nil
251+
}
252+
return nil, 0, "", nil
253+
}
254+
255+
func parseTLSconfig(providerConfig frameworkProviderConfig) (*tls.Config, error) {
256+
tlsClientConfig := &tls.Config{}
257+
258+
tlsKeyFile, tempFile, err := createTempFileIfLiteral(providerConfig.TLSKey.ValueString())
259+
if err != nil {
260+
return nil, err
261+
}
262+
if tempFile {
263+
defer os.Remove(tlsKeyFile)
264+
}
265+
tlsCertFile, tempFile, err := createTempFileIfLiteral(providerConfig.TLSCert.ValueString())
266+
if err != nil {
267+
return nil, err
268+
}
269+
if tempFile {
270+
defer os.Remove(tlsCertFile)
271+
}
272+
caCertFile, tempFile, err := createTempFileIfLiteral(providerConfig.CACert.ValueString())
273+
if err != nil {
274+
return nil, err
275+
}
276+
if tempFile {
277+
defer os.Remove(caCertFile)
278+
}
279+
280+
insecure := providerConfig.InsecureSkipVerify.ValueBool()
281+
if caCertFile != "" {
282+
ca, err := os.ReadFile(caCertFile)
283+
if err != nil {
284+
return nil, err
285+
}
286+
pool := x509.NewCertPool()
287+
pool.AppendCertsFromPEM(ca)
288+
tlsClientConfig.RootCAs = pool
289+
}
290+
if tlsKeyFile != "" && tlsCertFile != "" {
291+
cert, err := tls.LoadX509KeyPair(tlsCertFile, tlsKeyFile)
292+
if err != nil {
293+
return nil, err
294+
}
295+
tlsClientConfig.Certificates = []tls.Certificate{cert}
296+
}
297+
if insecure {
298+
tlsClientConfig.InsecureSkipVerify = true
299+
}
300+
301+
return tlsClientConfig, nil
302+
}
303+
304+
func setToStringArray(set []attr.Value) []string {
305+
var result []string
306+
for _, v := range set {
307+
result = append(result, v.(types.String).ValueString())
308+
}
309+
return result
310+
}

0 commit comments

Comments
 (0)