-
Notifications
You must be signed in to change notification settings - Fork 123
WIP: Add framework provider #234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ import ( | |||||
| "fmt" | ||||||
| "net/http" | ||||||
| "os" | ||||||
| "strconv" | ||||||
| "strings" | ||||||
|
|
||||||
| "github.com/disaster37/go-kibana-rest/v8" | ||||||
|
|
@@ -16,6 +17,9 @@ import ( | |||||
| "github.com/elastic/terraform-provider-elasticstack/internal/models" | ||||||
| "github.com/elastic/terraform-provider-elasticstack/internal/utils" | ||||||
| "github.com/hashicorp/go-version" | ||||||
| fwdiag "github.com/hashicorp/terraform-plugin-framework/diag" | ||||||
| "github.com/hashicorp/terraform-plugin-framework/tfsdk" | ||||||
| "github.com/hashicorp/terraform-plugin-framework/types" | ||||||
| "github.com/hashicorp/terraform-plugin-log/tflog" | ||||||
| "github.com/hashicorp/terraform-plugin-sdk/v2/diag" | ||||||
| "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" | ||||||
|
|
@@ -132,6 +136,35 @@ func NewAcceptanceTestingClient() (*ApiClient, error) { | |||||
|
|
||||||
| const esConnectionKey string = "elasticsearch_connection" | ||||||
|
|
||||||
| type ElasticSearchConnection struct { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||
| Username types.String `tfsdk:"username"` | ||||||
| Password types.String `tfsdk:"password"` | ||||||
| APIKey types.String `tfsdk:"api_key"` | ||||||
| Endpoints types.List `tfsdk:"endpoints"` | ||||||
| Insecure types.Bool `tfsdk:"insecure"` | ||||||
| CAFile types.String `tfsdk:"ca_file"` | ||||||
| CAData types.String `tfsdk:"ca_data"` | ||||||
| CertFile types.String `tfsdk:"cert_file"` | ||||||
| KeyFile types.String `tfsdk:"key_file"` | ||||||
| CertData types.String `tfsdk:"cert_data"` | ||||||
| KeyData types.String `tfsdk:"key_data"` | ||||||
| } | ||||||
|
|
||||||
| func NewFWApiClientFromState(ctx context.Context, state tfsdk.State, defaultClient *ApiClient) (*ApiClient, fwdiag.Diagnostics) { | ||||||
| var es struct { | ||||||
| Connection []*ElasticSearchConnection `tfsdk:"elasticsearch_connection"` | ||||||
| } | ||||||
| diags := state.Get(ctx, &es) | ||||||
| if diags.HasError() { | ||||||
| return nil, diags | ||||||
| } | ||||||
| if len(es.Connection) > 0 { | ||||||
| return NewFWEsApiClient(ctx, es.Connection[0], defaultClient.version, false) | ||||||
| } | ||||||
|
|
||||||
| return defaultClient, nil | ||||||
| } | ||||||
|
|
||||||
| func NewApiClient(d *schema.ResourceData, meta interface{}) (*ApiClient, diag.Diagnostics) { | ||||||
| defaultClient := meta.(*ApiClient) | ||||||
|
|
||||||
|
|
@@ -269,6 +302,117 @@ func (a *ApiClient) ClusterID(ctx context.Context) (*string, diag.Diagnostics) { | |||||
| return nil, diags | ||||||
| } | ||||||
|
|
||||||
| func NewFWEsApiClient(ctx context.Context, esConn *ElasticSearchConnection, version string, useEnvAsDefault bool) (*ApiClient, fwdiag.Diagnostics) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We're working on adding Kibana support here too |
||||||
| var diags fwdiag.Diagnostics | ||||||
| config := elasticsearch.Config{} | ||||||
| config.Username = getStringValue(esConn.Username, "ELASTICSEARCH_USERNAME", true) | ||||||
| config.Password = getStringValue(esConn.Password, "ELASTICSEARCH_PASSWORD", true) | ||||||
| config.APIKey = getStringValue(esConn.APIKey, "ELASTICSEARCH_API_KEY", true) | ||||||
|
|
||||||
| var addrs []string | ||||||
| diags.Append(esConn.Endpoints.ElementsAs(ctx, &addrs, true)...) | ||||||
| if diags.HasError() { | ||||||
| return nil, diags | ||||||
| } | ||||||
| if len(addrs) == 0 && useEnvAsDefault { | ||||||
| if endpoints := os.Getenv("ELASTICSEARCH_ENDPOINTS"); endpoints != "" { | ||||||
| for _, e := range strings.Split(endpoints, ",") { | ||||||
| addrs = append(addrs, strings.TrimSpace(e)) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| config.Addresses = addrs | ||||||
|
|
||||||
| envInsecure, _ := strconv.ParseBool(os.Getenv("ELASTICSEARCH_INSECURE")) | ||||||
| if esConn.Insecure.ValueBool() || envInsecure { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this is inconsistent with the other settings, where the environment value is checked even when the value is specified in the provider configuration. I think we should be consistent here. |
||||||
| tlsClientConfig := ensureTLSClientConfig(&config) | ||||||
| tlsClientConfig.InsecureSkipVerify = true | ||||||
| } | ||||||
|
|
||||||
| if esConn.CAFile.ValueString() != "" { | ||||||
| caCert, err := os.ReadFile(esConn.CAFile.ValueString()) | ||||||
| if err != nil { | ||||||
| diags.Append(fwdiag.NewErrorDiagnostic( | ||||||
| "Unable to read CA File", | ||||||
| err.Error(), | ||||||
| )) | ||||||
| return nil, diags | ||||||
| } | ||||||
| config.CACert = caCert | ||||||
| } | ||||||
| if esConn.CAData.ValueString() != "" { | ||||||
| config.CACert = []byte(esConn.CAData.ValueString()) | ||||||
| } | ||||||
|
|
||||||
| if certFile := esConn.CertFile.ValueString(); certFile != "" { | ||||||
| if keyFile := esConn.KeyFile.ValueString(); keyFile != "" { | ||||||
| cert, err := tls.LoadX509KeyPair(certFile, keyFile) | ||||||
| if err != nil { | ||||||
| diags.Append(fwdiag.NewErrorDiagnostic( | ||||||
| "Unable to read certificate or key file", | ||||||
| err.Error(), | ||||||
| )) | ||||||
| return nil, diags | ||||||
| } | ||||||
| tlsClientConfig := ensureTLSClientConfig(&config) | ||||||
| tlsClientConfig.Certificates = []tls.Certificate{cert} | ||||||
| } else { | ||||||
| diags.Append(fwdiag.NewErrorDiagnostic( | ||||||
| "Unable to read key file", | ||||||
| "Path to key file has not been configured or is empty", | ||||||
| )) | ||||||
| return nil, diags | ||||||
| } | ||||||
| } | ||||||
| if certData := esConn.CertData.ValueString(); certData != "" { | ||||||
| if keyData := esConn.KeyData.ValueString(); keyData != "" { | ||||||
| cert, err := tls.X509KeyPair([]byte(certData), []byte(keyData)) | ||||||
| if err != nil { | ||||||
| diags.Append(fwdiag.NewErrorDiagnostic( | ||||||
| "Unable to parse certificate or key", | ||||||
| err.Error(), | ||||||
| )) | ||||||
| return nil, diags | ||||||
| } | ||||||
| tlsClientConfig := ensureTLSClientConfig(&config) | ||||||
| tlsClientConfig.Certificates = []tls.Certificate{cert} | ||||||
| } else { | ||||||
| diags.Append(fwdiag.NewErrorDiagnostic( | ||||||
| "Unable to parse key", | ||||||
| "Key data has not been configured or is empty", | ||||||
| )) | ||||||
| return nil, diags | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| es, err := elasticsearch.NewClient(config) | ||||||
| if err != nil { | ||||||
| diags.Append(fwdiag.NewErrorDiagnostic( | ||||||
| "Unable to create Elasticsearch client", | ||||||
| err.Error(), | ||||||
| )) | ||||||
| return nil, diags | ||||||
| } | ||||||
| if logging.IsDebugOrHigher() { | ||||||
| config.EnableDebugLogger = true | ||||||
| config.Logger = &debugLogger{Name: "elasticsearch"} | ||||||
| } | ||||||
|
|
||||||
| return &ApiClient{ | ||||||
| elasticsearch: es, | ||||||
| version: version, | ||||||
| }, diags | ||||||
| } | ||||||
|
|
||||||
| func getStringValue(s types.String, envKey string, useEnvAsDefault bool) string { | ||||||
| if s.IsNull() { | ||||||
| if useEnvAsDefault { | ||||||
| return os.Getenv(envKey) | ||||||
| } | ||||||
| } | ||||||
| return s.ValueString() | ||||||
| } | ||||||
|
|
||||||
| type BaseConfig struct { | ||||||
| Username string | ||||||
| Password string | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,9 +3,128 @@ package schema | |||||
| import ( | ||||||
| "fmt" | ||||||
|
|
||||||
| "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" | ||||||
| "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" | ||||||
| "github.com/hashicorp/terraform-plugin-framework/path" | ||||||
| fwschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" | ||||||
| "github.com/hashicorp/terraform-plugin-framework/schema/validator" | ||||||
| "github.com/hashicorp/terraform-plugin-framework/types" | ||||||
| "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||||||
| ) | ||||||
|
|
||||||
| func GetEsFWConnectionBlock(keyName string, isProviderConfiguration bool) fwschema.Block { | ||||||
| usernamePath := makePathRef(keyName, "username") | ||||||
| passwordPath := makePathRef(keyName, "password") | ||||||
| caFilePath := makePathRef(keyName, "ca_file") | ||||||
| caDataPath := makePathRef(keyName, "ca_data") | ||||||
| certFilePath := makePathRef(keyName, "cert_file") | ||||||
| certDataPath := makePathRef(keyName, "cert_data") | ||||||
| keyFilePath := makePathRef(keyName, "key_file") | ||||||
| keyDataPath := makePathRef(keyName, "key_data") | ||||||
|
|
||||||
| usernameValidators := []validator.String{stringvalidator.AlsoRequires(path.MatchRoot(passwordPath))} | ||||||
| passwordValidators := []validator.String{stringvalidator.AlsoRequires(path.MatchRoot(usernamePath))} | ||||||
|
|
||||||
| if isProviderConfiguration { | ||||||
| // RequireWith validation isn't compatible when used in conjunction with DefaultFunc | ||||||
| usernameValidators = nil | ||||||
| passwordValidators = nil | ||||||
| } | ||||||
|
|
||||||
| return fwschema.ListNestedBlock{ | ||||||
| MarkdownDescription: fmt.Sprintf("Elasticsearch connection configuration block. %s", getDeprecationMessage(isProviderConfiguration)), | ||||||
| DeprecationMessage: getDeprecationMessage(isProviderConfiguration), | ||||||
| NestedObject: fwschema.NestedBlockObject{ | ||||||
| Attributes: map[string]fwschema.Attribute{ | ||||||
| "username": fwschema.StringAttribute{ | ||||||
| MarkdownDescription: "Username to use for API authentication to Elasticsearch.", | ||||||
| Optional: true, | ||||||
| Validators: usernameValidators, | ||||||
| }, | ||||||
| "password": fwschema.StringAttribute{ | ||||||
| MarkdownDescription: "Password to use for API authentication to Elasticsearch.", | ||||||
| Optional: true, | ||||||
| Sensitive: true, | ||||||
| Validators: passwordValidators, | ||||||
| }, | ||||||
| "api_key": fwschema.StringAttribute{ | ||||||
| MarkdownDescription: "API Key to use for authentication to Elasticsearch", | ||||||
| Optional: true, | ||||||
| Sensitive: true, | ||||||
| Validators: []validator.String{ | ||||||
| stringvalidator.ConflictsWith(path.MatchRoot(usernamePath)), | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we instead use a relative path matcher in the new schema?
Suggested change
|
||||||
| stringvalidator.ConflictsWith(path.MatchRoot(passwordPath)), | ||||||
| }, | ||||||
| }, | ||||||
| "endpoints": fwschema.ListAttribute{ | ||||||
| MarkdownDescription: "A comma-separated list of endpoints where the terraform provider will point to, this must include the http(s) schema and port number.", | ||||||
| Optional: true, | ||||||
| Sensitive: true, | ||||||
| ElementType: types.StringType, | ||||||
| }, | ||||||
| "insecure": fwschema.BoolAttribute{ | ||||||
| MarkdownDescription: "Disable TLS certificate validation", | ||||||
| Optional: true, | ||||||
| }, | ||||||
| "ca_file": fwschema.StringAttribute{ | ||||||
| MarkdownDescription: "Path to a custom Certificate Authority certificate", | ||||||
| Optional: true, | ||||||
| Validators: []validator.String{ | ||||||
| stringvalidator.ConflictsWith(path.MatchRoot(caDataPath)), | ||||||
| }, | ||||||
| }, | ||||||
| "ca_data": fwschema.StringAttribute{ | ||||||
| MarkdownDescription: "PEM-encoded custom Certificate Authority certificate", | ||||||
| Optional: true, | ||||||
| Validators: []validator.String{ | ||||||
| stringvalidator.ConflictsWith(path.MatchRoot(caFilePath)), | ||||||
| }, | ||||||
| }, | ||||||
| "cert_file": fwschema.StringAttribute{ | ||||||
| MarkdownDescription: "Path to a file containing the PEM encoded certificate for client auth", | ||||||
| Optional: true, | ||||||
| Validators: []validator.String{ | ||||||
| stringvalidator.AlsoRequires(path.MatchRoot(keyFilePath)), | ||||||
| stringvalidator.ConflictsWith(path.MatchRoot(certDataPath)), | ||||||
| stringvalidator.ConflictsWith(path.MatchRoot(keyDataPath)), | ||||||
| }, | ||||||
| }, | ||||||
| "key_file": fwschema.StringAttribute{ | ||||||
| MarkdownDescription: "Path to a file containing the PEM encoded private key for client auth", | ||||||
| Optional: true, | ||||||
| Validators: []validator.String{ | ||||||
| stringvalidator.AlsoRequires(path.MatchRoot(certFilePath)), | ||||||
| stringvalidator.ConflictsWith(path.MatchRoot(certDataPath)), | ||||||
| stringvalidator.ConflictsWith(path.MatchRoot(keyDataPath)), | ||||||
| }, | ||||||
| }, | ||||||
| "cert_data": fwschema.StringAttribute{ | ||||||
| MarkdownDescription: "PEM encoded certificate for client auth", | ||||||
| Optional: true, | ||||||
| Validators: []validator.String{ | ||||||
| stringvalidator.AlsoRequires(path.MatchRoot(keyDataPath)), | ||||||
| stringvalidator.ConflictsWith(path.MatchRoot(certFilePath)), | ||||||
| stringvalidator.ConflictsWith(path.MatchRoot(keyFilePath)), | ||||||
| }, | ||||||
| }, | ||||||
| "key_data": fwschema.StringAttribute{ | ||||||
| MarkdownDescription: "PEM encoded private key for client auth", | ||||||
| Optional: true, | ||||||
| Sensitive: true, | ||||||
| Validators: []validator.String{ | ||||||
| stringvalidator.AlsoRequires(path.MatchRoot(certDataPath)), | ||||||
| stringvalidator.ConflictsWith(path.MatchRoot(certFilePath)), | ||||||
| stringvalidator.ConflictsWith(path.MatchRoot(keyFilePath)), | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| Validators: []validator.List{ | ||||||
| listvalidator.SizeAtMost(1), | ||||||
| }, | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| func GetEsConnectionSchema(keyName string, isProviderConfiguration bool) *schema.Schema { | ||||||
| usernamePath := makePathRef(keyName, "username") | ||||||
| passwordPath := makePathRef(keyName, "password") | ||||||
|
|
@@ -20,20 +139,18 @@ func GetEsConnectionSchema(keyName string, isProviderConfiguration bool) *schema | |||||
| passwordRequiredWithValidation := []string{usernamePath} | ||||||
|
|
||||||
| withEnvDefault := func(key string, dv interface{}) schema.SchemaDefaultFunc { return nil } | ||||||
| deprecationMessage := "This property will be removed in a future provider version. Configure the Elasticsearch connection via the provider configuration instead." | ||||||
|
|
||||||
| if isProviderConfiguration { | ||||||
| withEnvDefault = func(key string, dv interface{}) schema.SchemaDefaultFunc { return schema.EnvDefaultFunc(key, dv) } | ||||||
| deprecationMessage = "" | ||||||
|
|
||||||
| // RequireWith validation isn't compatible when used in conjunction with DefaultFunc | ||||||
| usernameRequiredWithValidation = nil | ||||||
| passwordRequiredWithValidation = nil | ||||||
| } | ||||||
|
|
||||||
| return &schema.Schema{ | ||||||
| Description: fmt.Sprintf("Elasticsearch connection configuration block. %s", deprecationMessage), | ||||||
| Deprecated: deprecationMessage, | ||||||
| Description: fmt.Sprintf("Elasticsearch connection configuration block. %s", getDeprecationMessage(isProviderConfiguration)), | ||||||
| Deprecated: getDeprecationMessage(isProviderConfiguration), | ||||||
| Type: schema.TypeList, | ||||||
| MaxItems: 1, | ||||||
| Optional: true, | ||||||
|
|
@@ -168,3 +285,10 @@ func GetKibanaConnectionSchema() *schema.Schema { | |||||
| func makePathRef(keyName string, keyValue string) string { | ||||||
| return fmt.Sprintf("%s.0.%s", keyName, keyValue) | ||||||
| } | ||||||
|
|
||||||
| func getDeprecationMessage(isProviderConfiguration bool) string { | ||||||
| if isProviderConfiguration { | ||||||
| return "" | ||||||
| } | ||||||
| return "This property will be removed in a future provider version. Configure the Elasticsearch connection via the provider configuration instead." | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO it would be cleaner to have all functions which build a new client parse the relevant config into this struct and then call a single function which builds an
elasticsearch.Config{}instance from this struct.It would mean consolidating all the CA, TLS and debugging code. Given the different diagnostic types we'd have to return a plain error but I think that's ok.
WDYT?