ec/provider.go (290 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package ec import ( "context" "fmt" "github.com/elastic/terraform-provider-ec/ec/ecresource/organizationresource" "net/http" "time" "github.com/elastic/terraform-provider-ec/ec/ecdatasource/deploymenttemplates" "github.com/elastic/terraform-provider-ec/ec/internal" "github.com/elastic/terraform-provider-ec/ec/internal/gen/serverless" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/elastic/cloud-sdk-go/pkg/api" "github.com/elastic/terraform-provider-ec/ec/ecdatasource/deploymentdatasource" "github.com/elastic/terraform-provider-ec/ec/ecdatasource/deploymentsdatasource" "github.com/elastic/terraform-provider-ec/ec/ecdatasource/privatelinkdatasource" "github.com/elastic/terraform-provider-ec/ec/ecdatasource/stackdatasource" "github.com/elastic/terraform-provider-ec/ec/ecdatasource/trafficfilterdatasource" "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource" "github.com/elastic/terraform-provider-ec/ec/ecresource/elasticsearchkeystoreresource" "github.com/elastic/terraform-provider-ec/ec/ecresource/extensionresource" "github.com/elastic/terraform-provider-ec/ec/ecresource/projectresource" "github.com/elastic/terraform-provider-ec/ec/ecresource/snapshotrepositoryresource" "github.com/elastic/terraform-provider-ec/ec/ecresource/trafficfilterassocresource" "github.com/elastic/terraform-provider-ec/ec/ecresource/trafficfilterresource" "github.com/elastic/terraform-provider-ec/ec/internal/util" "github.com/elastic/terraform-provider-ec/ec/internal/validators" ) const ( eceOnlyText = "Available only when targeting ECE Installations or Elasticsearch Service Private" saasRequiredText = "The only valid authentication mechanism for the Elasticsearch Service" endpointDesc = "Endpoint where the terraform provider will point to. Defaults to \"%s\"." insecureDesc = "Allow the provider to skip TLS validation on its outgoing HTTP calls." timeoutDesc = "Timeout used for individual HTTP calls. Defaults to \"1m\"." verboseDesc = "When set, a \"request.log\" file will be written with all outgoing HTTP requests. Defaults to \"false\"." verboseCredsDesc = "When set with verbose, the contents of the Authorization header will not be redacted. Defaults to \"false\"." ) var ( apikeyDesc = fmt.Sprint("API Key to use for API authentication. ", saasRequiredText, ".") usernameDesc = fmt.Sprint("Username to use for API authentication. ", eceOnlyText, ".") passwordDesc = fmt.Sprint("Password to use for API authentication. ", eceOnlyText, ".") validURLSchemes = []string{"http", "https"} // defaultTimeout used for all outgoing HTTP requests, keeping it low-ish // since any requests which timeout due to network factors are retried // automatically by the SDK 2 times. defaultTimeout = 40 * time.Second ) func New(version string) provider.Provider { return &Provider{version: version} } func ProviderWithClient(client *api.API, version string) provider.Provider { return &Provider{client: client, version: version} } var _ provider.Provider = (*Provider)(nil) type Provider struct { version string client *api.API slsClient serverless.ClientWithResponsesInterface } func (p *Provider) Metadata(ctx context.Context, request provider.MetadataRequest, response *provider.MetadataResponse) { response.TypeName = "ec" } func (p *Provider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ func() datasource.DataSource { return &deploymentdatasource.DataSource{} }, func() datasource.DataSource { return &deploymentsdatasource.DataSource{} }, func() datasource.DataSource { return &stackdatasource.DataSource{} }, func() datasource.DataSource { return &trafficfilterdatasource.DataSource{} }, privatelinkdatasource.AwsDataSource, privatelinkdatasource.GcpDataSource, privatelinkdatasource.AzureDataSource, func() datasource.DataSource { return &deploymenttemplates.DataSource{} }, } } func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ func() resource.Resource { return &elasticsearchkeystoreresource.Resource{} }, func() resource.Resource { return &extensionresource.Resource{} }, func() resource.Resource { return &deploymentresource.Resource{} }, func() resource.Resource { return &snapshotrepositoryresource.Resource{} }, func() resource.Resource { return &trafficfilterresource.Resource{} }, func() resource.Resource { return &trafficfilterassocresource.Resource{} }, func() resource.Resource { return projectresource.NewElasticsearchProjectResource() }, func() resource.Resource { return projectresource.NewObservabilityProjectResource() }, func() resource.Resource { return projectresource.NewSecurityProjectResource() }, func() resource.Resource { return &organizationresource.Resource{} }, } } func (p *Provider) Schema(_ context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "endpoint": schema.StringAttribute{ Description: fmt.Sprintf(endpointDesc, api.ESSEndpoint), Optional: true, Validators: []validator.String{ validators.IsURLWithSchemeValidator(validURLSchemes), }, }, "apikey": schema.StringAttribute{ Description: apikeyDesc, Optional: true, Sensitive: true, }, "username": schema.StringAttribute{ Description: usernameDesc, Optional: true, }, "password": schema.StringAttribute{ Description: passwordDesc, Optional: true, Sensitive: true, }, "insecure": schema.BoolAttribute{ Description: insecureDesc, Optional: true, }, "timeout": schema.StringAttribute{ Description: timeoutDesc, Optional: true, }, "verbose": schema.BoolAttribute{ Description: verboseDesc, Optional: true, }, "verbose_credentials": schema.BoolAttribute{ Description: verboseCredsDesc, Optional: true, }, "verbose_file": schema.StringAttribute{ Description: timeoutDesc, Optional: true, }, }, } } // Retrieve provider data from configuration type providerConfig struct { Endpoint types.String `tfsdk:"endpoint"` ApiKey types.String `tfsdk:"apikey"` Username types.String `tfsdk:"username"` Password types.String `tfsdk:"password"` Insecure types.Bool `tfsdk:"insecure"` Timeout types.String `tfsdk:"timeout"` Verbose types.Bool `tfsdk:"verbose"` VerboseCredentials types.Bool `tfsdk:"verbose_credentials"` VerboseFile types.String `tfsdk:"verbose_file"` } func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { if p.client != nil { data := internal.ProviderClients{ Stateful: p.client, Serverless: p.slsClient, } // Required for unit tests, because a mock client is pre-created there. resp.DataSourceData = data resp.ResourceData = data return } var config providerConfig diags := req.Config.Get(ctx, &config) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } endpoint := config.Endpoint.ValueString() if config.Endpoint.ValueString() == "" { endpoint = util.MultiGetenvOrDefault([]string{"EC_ENDPOINT", "EC_HOST"}, api.ESSEndpoint) diags := validateEndpoint(ctx, endpoint) resp.Diagnostics.Append(diags...) if diags.HasError() { return } } apiKey := config.ApiKey.ValueString() if config.ApiKey.ValueString() == "" { apiKey = util.MultiGetenvOrDefault([]string{"EC_API_KEY"}, "") } username := config.Username.ValueString() if config.Username.ValueString() == "" { username = util.MultiGetenvOrDefault([]string{"EC_USER", "EC_USERNAME"}, "") } password := config.Password.ValueString() if config.Password.ValueString() == "" { password = util.MultiGetenvOrDefault([]string{"EC_PASS", "EC_PASSWORD"}, "") } timeoutStr := config.Timeout.ValueString() if config.Timeout.ValueString() == "" { timeoutStr = util.MultiGetenvOrDefault([]string{"EC_TIMEOUT"}, defaultTimeout.String()) } timeout, err := time.ParseDuration(timeoutStr) if err != nil { resp.Diagnostics.AddError("Unable to create client", err.Error()) return } insecure := config.Insecure.ValueBool() if config.Insecure.IsNull() { insecureStr := util.MultiGetenvOrDefault([]string{"EC_INSECURE", "EC_SKIP_TLS_VALIDATION"}, "") if insecure, err = util.StringToBool(insecureStr); err != nil { resp.Diagnostics.AddError( "Unable to create client", fmt.Sprintf("Invalid value '%v' in 'EC_INSECURE' or 'EC_SKIP_TLS_VALIDATION'", insecureStr), ) return } } verbose := config.Verbose.ValueBool() if config.Verbose.IsNull() { verboseStr := util.MultiGetenvOrDefault([]string{"EC_VERBOSE"}, "") if verbose, err = util.StringToBool(verboseStr); err != nil { resp.Diagnostics.AddError( "Unable to create client", fmt.Sprintf("Invalid value '%v' in 'EC_VERBOSE'", verboseStr), ) return } } verboseCredentials := config.VerboseCredentials.ValueBool() if config.VerboseCredentials.IsNull() { verboseCredentialsStr := util.MultiGetenvOrDefault([]string{"EC_VERBOSE_CREDENTIALS"}, "") if verboseCredentials, err = util.StringToBool(verboseCredentialsStr); err != nil { resp.Diagnostics.AddError( "Unable to create client", fmt.Sprintf("Invalid value '%v' in 'EC_VERBOSE_CREDENTIALS'", verboseCredentialsStr), ) return } } verboseFile := config.VerboseFile.ValueString() if config.VerboseFile.IsNull() { verboseFile = util.MultiGetenvOrDefault([]string{"EC_VERBOSE_FILE"}, "request.log") } cfg, err := newAPIConfig(apiSetup{ endpoint: endpoint, apikey: apiKey, username: username, password: password, insecure: insecure, timeout: timeout, verbose: verbose, verboseCredentials: verboseCredentials, verboseFile: verboseFile, }) if err != nil { resp.Diagnostics.AddError( "Unable to create api Client config", err.Error(), ) return } client, err := api.NewAPI(cfg) if err != nil { resp.Diagnostics.AddError( "Unable to create api Client", err.Error(), ) return } serverlessClient, err := serverless.NewClientWithResponses( cfg.Host, serverless.WithHTTPClient(cfg.Client), serverless.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error { cfg.AuthWriter.AuthRequest(req) return nil }), ) if err != nil { resp.Diagnostics.AddError( "Unable to create serverless Client", err.Error(), ) } p.client = client p.slsClient = serverlessClient data := internal.ProviderClients{ Stateful: client, Serverless: serverlessClient, } resp.DataSourceData = data resp.ResourceData = data } func validateEndpoint(ctx context.Context, endpoint string) diag.Diagnostics { validateReq := validator.StringRequest{ Path: path.Root("endpoint"), ConfigValue: types.StringValue(endpoint), } validateResp := validator.StringResponse{} validators.IsURLWithSchemeValidator(validURLSchemes).ValidateString(ctx, validateReq, &validateResp) return validateResp.Diagnostics }