diff --git a/docs/resources/kibana_synthetics_monitor.md b/docs/resources/kibana_synthetics_monitor.md index e19aaca92..37dfe83a8 100644 --- a/docs/resources/kibana_synthetics_monitor.md +++ b/docs/resources/kibana_synthetics_monitor.md @@ -72,7 +72,7 @@ resource "elasticstack_kibana_synthetics_monitor" "my_monitor" { ### Required -- `name` (String) The monitor’s name. +- `name` (String) The monitor's name. ### Optional @@ -85,14 +85,14 @@ resource "elasticstack_kibana_synthetics_monitor" "my_monitor" { - `locations` (List of String) Where to deploy the monitor. Monitors can be deployed in multiple locations so that you can detect differences in availability and response times across those locations. - `namespace` (String) The data stream namespace. Note: if you change its value, kibana creates new datastream. A user needs permissions for new/old datastream in update case to be able to see full monitor history. The `namespace` field should be lowercase and not contain spaces. The namespace must not include any of the following characters: *, \, /, ?, ", <, >, |, whitespace, ,, #, :, or -. Default: `default` - `params` (String) Monitor parameters. Raw JSON object, use `jsonencode` function to represent JSON -- `private_locations` (List of String) These Private Locations refer to locations hosted and managed by you, whereas locations are hosted by Elastic. You can specify a Private Location using the location’s name. +- `private_locations` (List of String) These Private Locations refer to locations hosted and managed by you, whereas locations are hosted by Elastic. You can specify a Private Location using the location's name. - `retest_on_failure` (Boolean) Enable or disable retesting when a monitor fails. By default, monitors are automatically retested if the monitor goes from "up" to "down". If the result of the retest is also "down", an error will be created, and if configured, an alert sent. Then the monitor will resume running according to the defined schedule. Using retest_on_failure can reduce noise related to transient problems. Default: `true`. -- `schedule` (Number) The monitor’s schedule in minutes. Supported values are 1, 3, 5, 10, 15, 30, 60, 120 and 240. +- `schedule` (Number) The monitor's schedule in minutes. Supported values are 1, 3, 5, 10, 15, 30, 60, 120 and 240. - `service_name` (String) The APM service name. - `space_id` (String) Kibana space. The space ID that is part of the Kibana URL when inside the space. Space IDs are limited to lowercase alphanumeric, underscore, and hyphen characters (a-z, 0-9, _, and -). You are cannot change the ID with the update operation. - `tags` (List of String) An array of tags. - `tcp` (Attributes) TCP Monitor specific fields (see [below for nested schema](#nestedatt--tcp)) -- `timeout` (Number) The monitor timeout in seconds, monitor will fail if it doesn’t complete within this time. Default: `16` +- `timeout` (Number) The monitor timeout in seconds, monitor will fail if it doesn't complete within this time. Default: `16` ### Read-Only @@ -151,7 +151,7 @@ Optional: - `ipv4` (Boolean) Whether to ping using the ipv4 protocol. - `ipv6` (Boolean) Whether to ping using the ipv6 protocol. - `max_redirects` (Number) The maximum number of redirects to follow. Default: `0` -- `mode` (String) The mode of the monitor. Can be "all" or "any". If you’re using a DNS-load balancer and want to ping every IP address for the specified hostname, you should use all. +- `mode` (String) The mode of the monitor. Can be "all" or "any". If you're using a DNS-load balancer and want to ping every IP address for the specified hostname, you should use all. - `password` (String) The password for authenticating with the server. The credentials are passed with the request. - `proxy_header` (String) Additional headers to send to proxies during CONNECT requests.. Raw JSON object, use `jsonencode` function to represent JSON - `proxy_url` (String) The URL of the proxy to use for this monitor. diff --git a/internal/apm/agent_configuration/resource_test.go b/internal/apm/agent_configuration/acc_test.go similarity index 100% rename from internal/apm/agent_configuration/resource_test.go rename to internal/apm/agent_configuration/acc_test.go diff --git a/internal/fleet/agent_policy/resource_test.go b/internal/fleet/agent_policy/acc_test.go similarity index 100% rename from internal/fleet/agent_policy/resource_test.go rename to internal/fleet/agent_policy/acc_test.go diff --git a/internal/fleet/integration/resource_test.go b/internal/fleet/integration/acc_test.go similarity index 100% rename from internal/fleet/integration/resource_test.go rename to internal/fleet/integration/acc_test.go diff --git a/internal/fleet/integration_policy/resource_test.go b/internal/fleet/integration_policy/acc_test.go similarity index 100% rename from internal/fleet/integration_policy/resource_test.go rename to internal/fleet/integration_policy/acc_test.go diff --git a/internal/fleet/server_host/resource_test.go b/internal/fleet/server_host/acc_test.go similarity index 100% rename from internal/fleet/server_host/resource_test.go rename to internal/fleet/server_host/acc_test.go diff --git a/internal/kibana/synthetics/api_client.go b/internal/kibana/synthetics/api_client.go new file mode 100644 index 000000000..3be47c300 --- /dev/null +++ b/internal/kibana/synthetics/api_client.go @@ -0,0 +1,51 @@ +package synthetics + +import ( + "github.com/disaster37/go-kibana-rest/v8" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +// ESApiClient interface provides access to the underlying API client +type ESApiClient interface { + GetClient() *clients.ApiClient +} + +// GetKibanaClient returns a configured Kibana client for the given ESApiClient +func GetKibanaClient(c ESApiClient, dg diag.Diagnostics) *kibana.Client { + client := c.GetClient() + if client == nil { + dg.AddError( + "Unconfigured Client", + "Expected configured client. Please report this issue to the provider developers.", + ) + return nil + } + + kibanaClient, err := client.GetKibanaClient() + if err != nil { + dg.AddError("unable to get kibana client", err.Error()) + return nil + } + return kibanaClient +} + +// GetKibanaOAPIClient returns a configured Kibana OpenAPI client for the given ESApiClient +func GetKibanaOAPIClient(c ESApiClient, dg diag.Diagnostics) *kibana_oapi.Client { + client := c.GetClient() + if client == nil { + dg.AddError( + "Unconfigured Client", + "Expected configured client. Please report this issue to the provider developers.", + ) + return nil + } + + kibanaClient, err := client.GetKibanaOapiClient() + if err != nil { + dg.AddError("unable to get kibana oapi client", err.Error()) + return nil + } + return kibanaClient +} diff --git a/internal/kibana/synthetics/acc_test.go b/internal/kibana/synthetics/monitor/acc_test.go similarity index 99% rename from internal/kibana/synthetics/acc_test.go rename to internal/kibana/synthetics/monitor/acc_test.go index bb6da968c..e1507d336 100644 --- a/internal/kibana/synthetics/acc_test.go +++ b/internal/kibana/synthetics/monitor/acc_test.go @@ -1,11 +1,11 @@ -package synthetics_test +package monitor_test import ( "fmt" "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" - "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics/monitor" "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" "github.com/hashicorp/go-version" sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" @@ -882,7 +882,7 @@ func TestSyntheticMonitorLabelsResource(t *testing.T) { Steps: []resource.TestStep{ // Create and Read monitor with labels { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(synthetics.MinLabelsVersion), + SkipFunc: versionutils.CheckIfVersionIsUnsupported(monitor.MinLabelsVersion), Config: labelsConfig, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet(labelsMonitorId, "id"), @@ -896,7 +896,7 @@ func TestSyntheticMonitorLabelsResource(t *testing.T) { }, // ImportState testing { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(synthetics.MinLabelsVersion), + SkipFunc: versionutils.CheckIfVersionIsUnsupported(monitor.MinLabelsVersion), ResourceName: labelsMonitorId, ImportState: true, ImportStateVerify: true, @@ -904,7 +904,7 @@ func TestSyntheticMonitorLabelsResource(t *testing.T) { }, // Update labels - change values but keep same keys { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(synthetics.MinLabelsVersion), + SkipFunc: versionutils.CheckIfVersionIsUnsupported(monitor.MinLabelsVersion), Config: labelsConfigUpdated, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet(labelsMonitorId, "id"), @@ -917,7 +917,7 @@ func TestSyntheticMonitorLabelsResource(t *testing.T) { }, // Remove all labels - this tests the round-trip consistency fix { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(synthetics.MinLabelsVersion), + SkipFunc: versionutils.CheckIfVersionIsUnsupported(monitor.MinLabelsVersion), Config: labelsConfigRemoved, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet(labelsMonitorId, "id"), diff --git a/internal/kibana/synthetics/create.go b/internal/kibana/synthetics/monitor/create.go similarity index 89% rename from internal/kibana/synthetics/create.go rename to internal/kibana/synthetics/monitor/create.go index 5fbc80aa0..c92df530b 100644 --- a/internal/kibana/synthetics/create.go +++ b/internal/kibana/synthetics/monitor/create.go @@ -1,18 +1,18 @@ -package synthetics +package monitor import ( "context" "fmt" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-plugin-framework/resource" ) var MinLabelsVersion = version.Must(version.NewVersion("8.16.0")) func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { - kibanaClient := GetKibanaClient(r, response.Diagnostics) + kibanaClient := synthetics.GetKibanaClient(r, response.Diagnostics) if kibanaClient == nil { return } diff --git a/internal/kibana/synthetics/delete.go b/internal/kibana/synthetics/monitor/delete.go similarity index 78% rename from internal/kibana/synthetics/delete.go rename to internal/kibana/synthetics/monitor/delete.go index fc93d57a2..9c0e25a6e 100644 --- a/internal/kibana/synthetics/delete.go +++ b/internal/kibana/synthetics/monitor/delete.go @@ -1,15 +1,16 @@ -package synthetics +package monitor import ( "context" "fmt" + "github.com/disaster37/go-kibana-rest/v8/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" "github.com/hashicorp/terraform-plugin-framework/resource" ) func (r *Resource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { - - kibanaClient := GetKibanaClient(r, response.Diagnostics) + kibanaClient := synthetics.GetKibanaClient(r, response.Diagnostics) if kibanaClient == nil { return } @@ -21,7 +22,7 @@ func (r *Resource) Delete(ctx context.Context, request resource.DeleteRequest, r return } - compositeId, dg := GetCompositeId(plan.ID.ValueString()) + compositeId, dg := synthetics.GetCompositeId(plan.ID.ValueString()) response.Diagnostics.Append(dg...) if response.Diagnostics.HasError() { return diff --git a/internal/kibana/synthetics/read.go b/internal/kibana/synthetics/monitor/read.go similarity index 84% rename from internal/kibana/synthetics/read.go rename to internal/kibana/synthetics/monitor/read.go index 8d13887fd..5c8f945bc 100644 --- a/internal/kibana/synthetics/read.go +++ b/internal/kibana/synthetics/monitor/read.go @@ -1,4 +1,4 @@ -package synthetics +package monitor import ( "context" @@ -6,12 +6,12 @@ import ( "fmt" "github.com/disaster37/go-kibana-rest/v8/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" "github.com/hashicorp/terraform-plugin-framework/resource" ) func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { - - kibanaClient := GetKibanaClient(r, response.Diagnostics) + kibanaClient := synthetics.GetKibanaClient(r, response.Diagnostics) if kibanaClient == nil { return } @@ -23,7 +23,7 @@ func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, respo return } - compositeId, dg := GetCompositeId(state.ID.ValueString()) + compositeId, dg := synthetics.GetCompositeId(state.ID.ValueString()) response.Diagnostics.Append(dg...) if response.Diagnostics.HasError() { return diff --git a/internal/kibana/synthetics/resource-description.md b/internal/kibana/synthetics/monitor/resource-description.md similarity index 100% rename from internal/kibana/synthetics/resource-description.md rename to internal/kibana/synthetics/monitor/resource-description.md diff --git a/internal/kibana/synthetics/resource.go b/internal/kibana/synthetics/monitor/resource.go similarity index 60% rename from internal/kibana/synthetics/resource.go rename to internal/kibana/synthetics/monitor/resource.go index 899a8b9bc..36238317e 100644 --- a/internal/kibana/synthetics/resource.go +++ b/internal/kibana/synthetics/monitor/resource.go @@ -1,76 +1,38 @@ -package synthetics +package monitor import ( "context" - "github.com/disaster37/go-kibana-rest/v8" "github.com/elastic/terraform-provider-elasticstack/internal/clients" - "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" ) -const resourceName = MetadataPrefix + "monitor" +const resourceName = synthetics.MetadataPrefix + "monitor" + +// NewResource creates a new synthetics monitor resource +func NewResource() resource.Resource { + return &Resource{} +} // Ensure provider defined types fully satisfy framework interfaces var _ resource.Resource = &Resource{} var _ resource.ResourceWithConfigure = &Resource{} var _ resource.ResourceWithImportState = &Resource{} var _ resource.ResourceWithConfigValidators = &Resource{} -var _ ESApiClient = &Resource{} - -type ESApiClient interface { - GetClient() *clients.ApiClient -} - -func GetKibanaClient(c ESApiClient, dg diag.Diagnostics) *kibana.Client { - - client := c.GetClient() - if client == nil { - dg.AddError( - "Unconfigured Client", - "Expected configured client. Please report this issue to the provider developers.", - ) - return nil - } - - kibanaClient, err := client.GetKibanaClient() - if err != nil { - dg.AddError("unable to get kibana client", err.Error()) - return nil - } - return kibanaClient -} - -func GetKibanaOAPIClient(c ESApiClient, dg diag.Diagnostics) *kibana_oapi.Client { - - client := c.GetClient() - if client == nil { - dg.AddError( - "Unconfigured Client", - "Expected configured client. Please report this issue to the provider developers.", - ) - return nil - } - - kibanaClient, err := client.GetKibanaOapiClient() - if err != nil { - dg.AddError("unable to get kibana oapi client", err.Error()) - return nil - } - return kibanaClient -} +var _ synthetics.ESApiClient = &Resource{} +// Resource represents a synthetics monitor resource type Resource struct { client *clients.ApiClient - ESApiClient } func (r *Resource) GetClient() *clients.ApiClient { return r.client } + func (r *Resource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { return []resource.ConfigValidator{ resourcevalidator.ExactlyOneOf( @@ -102,5 +64,4 @@ func (r *Resource) Metadata(ctx context.Context, request resource.MetadataReques func (r *Resource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { response.Schema = monitorConfigSchema() - } diff --git a/internal/kibana/synthetics/monitor/schema.go b/internal/kibana/synthetics/monitor/schema.go new file mode 100644 index 000000000..f722cc466 --- /dev/null +++ b/internal/kibana/synthetics/monitor/schema.go @@ -0,0 +1,1039 @@ +package monitor + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "regexp" + "strconv" + + "github.com/disaster37/go-kibana-rest/v8/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/diagutil" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +type kibanaAPIRequest struct { + fields kbapi.MonitorFields + config kbapi.SyntheticsMonitorConfig +} + +type tfStatusConfigV0 struct { + Enabled types.Bool `tfsdk:"enabled"` +} + +type tfAlertConfigV0 struct { + Status *tfStatusConfigV0 `tfsdk:"status"` + TLS *tfStatusConfigV0 `tfsdk:"tls"` +} + +type tfSSLConfig struct { + SslVerificationMode types.String `tfsdk:"ssl_verification_mode"` + SslSupportedProtocols types.List `tfsdk:"ssl_supported_protocols"` + SslCertificateAuthorities []types.String `tfsdk:"ssl_certificate_authorities"` + SslCertificate types.String `tfsdk:"ssl_certificate"` + SslKey types.String `tfsdk:"ssl_key"` + SslKeyPassphrase types.String `tfsdk:"ssl_key_passphrase"` +} + +type tfHTTPMonitorFieldsV0 struct { + URL types.String `tfsdk:"url"` + MaxRedirects types.Int64 `tfsdk:"max_redirects"` + Mode types.String `tfsdk:"mode"` + IPv4 types.Bool `tfsdk:"ipv4"` + IPv6 types.Bool `tfsdk:"ipv6"` + ProxyURL types.String `tfsdk:"proxy_url"` + ProxyHeader jsontypes.Normalized `tfsdk:"proxy_header"` + Username types.String `tfsdk:"username"` + Password types.String `tfsdk:"password"` + Response jsontypes.Normalized `tfsdk:"response"` + Check jsontypes.Normalized `tfsdk:"check"` + + tfSSLConfig +} + +type tfTCPMonitorFieldsV0 struct { + Host types.String `tfsdk:"host"` + CheckSend types.String `tfsdk:"check_send"` + CheckReceive types.String `tfsdk:"check_receive"` + ProxyURL types.String `tfsdk:"proxy_url"` + ProxyUseLocalResolver types.Bool `tfsdk:"proxy_use_local_resolver"` + + tfSSLConfig +} + +type tfICMPMonitorFieldsV0 struct { + Host types.String `tfsdk:"host"` + Wait types.Int64 `tfsdk:"wait"` +} + +type tfBrowserMonitorFieldsV0 struct { + InlineScript types.String `tfsdk:"inline_script"` + Screenshots types.String `tfsdk:"screenshots"` + SyntheticsArgs []types.String `tfsdk:"synthetics_args"` + IgnoreHttpsErrors types.Bool `tfsdk:"ignore_https_errors"` + PlaywrightOptions jsontypes.Normalized `tfsdk:"playwright_options"` +} + +type tfModelV0 struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + SpaceID types.String `tfsdk:"space_id"` + Namespace types.String `tfsdk:"namespace"` + Schedule types.Int64 `tfsdk:"schedule"` + Locations []types.String `tfsdk:"locations"` + PrivateLocations []types.String `tfsdk:"private_locations"` + Enabled types.Bool `tfsdk:"enabled"` + Tags []types.String `tfsdk:"tags"` + Labels types.Map `tfsdk:"labels"` + Alert types.Object `tfsdk:"alert"` //tfAlertConfigV0 + APMServiceName types.String `tfsdk:"service_name"` + TimeoutSeconds types.Int64 `tfsdk:"timeout"` + HTTP *tfHTTPMonitorFieldsV0 `tfsdk:"http"` + TCP *tfTCPMonitorFieldsV0 `tfsdk:"tcp"` + ICMP *tfICMPMonitorFieldsV0 `tfsdk:"icmp"` + Browser *tfBrowserMonitorFieldsV0 `tfsdk:"browser"` + Params jsontypes.Normalized `tfsdk:"params"` + RetestOnFailure types.Bool `tfsdk:"retest_on_failure"` +} + +//go:embed resource-description.md +var monitorDescription string + +func monitorConfigSchema() schema.Schema { + return schema.Schema{ + MarkdownDescription: monitorDescription, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Generated identifier for the monitor", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + Optional: false, + Required: true, + MarkdownDescription: "The monitor's name.", + }, + "space_id": schema.StringAttribute{ + MarkdownDescription: "Kibana space. The space ID that is part of the Kibana URL when inside the space. Space IDs are limited to lowercase alphanumeric, underscore, and hyphen characters (a-z, 0-9, _, and -). You are cannot change the ID with the update operation.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + Computed: true, + }, + "namespace": schema.StringAttribute{ + MarkdownDescription: "The data stream namespace. Note: if you change its value, kibana creates new datastream. A user needs permissions for new/old datastream in update case to be able to see full monitor history. The `namespace` field should be lowercase and not contain spaces. The namespace must not include any of the following characters: *, \\, /, ?, \", <, >, |, whitespace, ,, #, :, or -. Default: `default`", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Computed: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[^*\\/?\"<>|\s,#:-]*$`), + "namespace must not contain any of the following characters: *, \\, /, ?, \", <, >, |, whitespace, ,, #, :, or -", + ), + }, + }, + "schedule": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "The monitor's schedule in minutes. Supported values are 1, 3, 5, 10, 15, 30, 60, 120 and 240.", + Validators: []validator.Int64{ + int64validator.OneOf(1, 3, 5, 10, 15, 30, 60, 120, 240), + }, + Computed: true, + PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, + }, + "locations": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Where to deploy the monitor. Monitors can be deployed in multiple locations so that you can detect differences in availability and response times across those locations.", + Validators: []validator.List{ + listvalidator.ValueStringsAre( + stringvalidator.OneOf( + "japan", + "india", + "singapore", + "australia_east", + "united_kingdom", + "germany", + "canada_east", + "brazil", + "us_east", + "us_west", + ), + ), + }, + }, + "private_locations": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "These Private Locations refer to locations hosted and managed by you, whereas locations are hosted by Elastic. You can specify a Private Location using the location's name.", + }, + "enabled": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether the monitor is enabled. Default: `true`", + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + }, + "tags": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "An array of tags.", + }, + "labels": schema.MapAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Key-value pairs of labels to associate with the monitor. Labels can be used for filtering and grouping monitors.", + }, + "alert": monitorAlertConfigSchema(), + "service_name": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The APM service name.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "timeout": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "The monitor timeout in seconds, monitor will fail if it doesn't complete within this time. Default: `16`", + Computed: true, + PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, + }, + "params": jsonObjectSchema("Monitor parameters"), + "http": httpMonitorFieldsSchema(), + "tcp": tcpMonitorFieldsSchema(), + "icmp": icmpMonitorFieldsSchema(), + "browser": browserMonitorFieldsSchema(), + "retest_on_failure": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Enable or disable retesting when a monitor fails. By default, monitors are automatically retested if the monitor goes from \"up\" to \"down\". If the result of the retest is also \"down\", an error will be created, and if configured, an alert sent. Then the monitor will resume running according to the defined schedule. Using retest_on_failure can reduce noise related to transient problems. Default: `true`.", + }, + }, + } +} + +func browserMonitorFieldsSchema() schema.Attribute { + return schema.SingleNestedAttribute{ + Optional: true, + MarkdownDescription: "Browser Monitor specific fields", + Attributes: map[string]schema.Attribute{ + "inline_script": schema.StringAttribute{ + Optional: false, + Required: true, + MarkdownDescription: "The inline script.", + }, + "screenshots": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Controls the behavior of the screenshots feature.", + Validators: []validator.String{ + stringvalidator.OneOf("on", "off", "only-on-failure"), + }, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "synthetics_args": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Synthetics agent CLI arguments.", + }, + "ignore_https_errors": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to ignore HTTPS errors.", + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + }, + "playwright_options": jsonObjectSchema("Playwright options."), + }, + } +} + +func icmpMonitorFieldsSchema() schema.Attribute { + return schema.SingleNestedAttribute{ + Optional: true, + MarkdownDescription: "ICMP Monitor specific fields", + Attributes: map[string]schema.Attribute{ + "host": schema.StringAttribute{ + Optional: false, + Required: true, + MarkdownDescription: "Host to ping; it can be an IP address or a hostname.", + }, + "wait": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: " Wait time in seconds. Default: `1`", + PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, + Computed: true, + }, + }, + } +} + +func jsonObjectSchema(doc string) schema.Attribute { + return schema.StringAttribute{ + Optional: true, + MarkdownDescription: fmt.Sprintf("%s. Raw JSON object, use `jsonencode` function to represent JSON", doc), + CustomType: jsontypes.NormalizedType{}, + } +} + +func statusConfigSchema() schema.Attribute { + return schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + }, + }, + } +} + +func monitorAlertConfigSchema() schema.Attribute { + return schema.SingleNestedAttribute{ + Optional: true, + MarkdownDescription: "Alert configuration. Default: `{ status: { enabled: true }, tls: { enabled: true } }`.", + Attributes: map[string]schema.Attribute{ + "status": statusConfigSchema(), + "tls": statusConfigSchema(), + }, + Computed: true, + PlanModifiers: []planmodifier.Object{objectplanmodifier.UseStateForUnknown()}, + } +} + +func httpMonitorFieldsSchema() schema.Attribute { + return schema.SingleNestedAttribute{ + Optional: true, + MarkdownDescription: "HTTP Monitor specific fields", + Attributes: map[string]schema.Attribute{ + "url": schema.StringAttribute{ + Optional: false, + Required: true, + MarkdownDescription: "URL to monitor.", + }, + "ssl_verification_mode": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Controls the verification of server certificates. ", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "ssl_supported_protocols": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "List of allowed SSL/TLS versions.", + Computed: true, + PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, + }, + "ssl_certificate_authorities": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "The list of root certificates for verifications is required.", + }, + "ssl_certificate": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Certificate.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "ssl_key": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Certificate key.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Sensitive: true, + }, + "ssl_key_passphrase": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Key passphrase.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Sensitive: true, + }, + "max_redirects": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "The maximum number of redirects to follow. Default: `0`", + PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, + Computed: true, + }, + "mode": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The mode of the monitor. Can be \"all\" or \"any\". If you're using a DNS-load balancer and want to ping every IP address for the specified hostname, you should use all.", + Validators: []validator.String{ + stringvalidator.OneOf("any", "all"), + }, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "ipv4": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to ping using the ipv4 protocol.", + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + }, + "ipv6": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to ping using the ipv6 protocol.", + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + }, + "username": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The username for authenticating with the server. The credentials are passed with the request.", + }, + "password": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The password for authenticating with the server. The credentials are passed with the request.", + }, + "proxy_header": jsonObjectSchema("Additional headers to send to proxies during CONNECT requests."), + "proxy_url": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The URL of the proxy to use for this monitor.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "response": jsonObjectSchema("Controls the indexing of the HTTP response body contents to the `http.response.body.contents` field."), + "check": jsonObjectSchema("The check request settings."), + }, + } +} + +func tcpMonitorFieldsSchema() schema.Attribute { + return schema.SingleNestedAttribute{ + Optional: true, + MarkdownDescription: "TCP Monitor specific fields", + Attributes: map[string]schema.Attribute{ + "host": schema.StringAttribute{ + Optional: false, + Required: true, + MarkdownDescription: "The host to monitor; it can be an IP address or a hostname. The host can include the port using a colon (e.g., \"example.com:9200\").", + }, + "ssl_verification_mode": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Controls the verification of server certificates. ", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "ssl_supported_protocols": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "List of allowed SSL/TLS versions.", + Computed: true, + PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, + }, + "ssl_certificate_authorities": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "The list of root certificates for verifications is required.", + }, + "ssl_certificate": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Certificate.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "ssl_key": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Certificate key.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Sensitive: true, + }, + "ssl_key_passphrase": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Key passphrase.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Sensitive: true, + }, + "check_send": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "An optional payload string to send to the remote host.", + }, + "check_receive": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The expected answer. ", + }, + "proxy_url": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The URL of the SOCKS5 proxy to use when connecting to the server. The value must be a URL with a scheme of `socks5://`. If the SOCKS5 proxy server requires client authentication, then a username and password can be embedded in the URL. When using a proxy, hostnames are resolved on the proxy server instead of on the client. You can change this behavior by setting the `proxy_use_local_resolver` option.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "proxy_use_local_resolver": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: " A Boolean value that determines whether hostnames are resolved locally instead of being resolved on the proxy server. The default value is false, which means that name resolution occurs on the proxy server.", + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + }, + }, + } +} + +func toNormalizedValue(jsObj kbapi.JsonObject) (jsontypes.Normalized, error) { + res, err := json.Marshal(jsObj) + if err != nil { + return jsontypes.NewNormalizedUnknown(), err + } + return jsontypes.NewNormalizedValue(string(res)), nil +} + +func toJsonObject(v jsontypes.Normalized) (kbapi.JsonObject, diag.Diagnostics) { + if v.IsNull() { + return nil, diag.Diagnostics{} + } + var res kbapi.JsonObject + dg := v.Unmarshal(&res) + if dg.HasError() { + return nil, dg + } + return res, diag.Diagnostics{} +} + +func stringToInt64(v string) (int64, error) { + var res int64 + var err error + if v != "" { + res, err = strconv.ParseInt(v, 10, 64) + } + return res, err +} + +func (v *tfModelV0) toModelV0(ctx context.Context, api *kbapi.SyntheticsMonitor, space string) (*tfModelV0, diag.Diagnostics) { + var schedule int64 + var err error + dg := diag.Diagnostics{} + if api.Schedule != nil { + schedule, err = stringToInt64(api.Schedule.Number) + if err != nil { + dg.AddError("Failed to convert schedule to int64", err.Error()) + return nil, dg + } + } + + var privateLocLabels []string + for _, l := range api.Locations { + if !l.IsServiceManaged { + privateLocLabels = append(privateLocLabels, l.Label) + } + } + + timeout, err := stringToInt64(string(api.Timeout)) + if err != nil { + dg.AddError("Failed to convert timeout to int64", err.Error()) + return nil, dg + } + + var http *tfHTTPMonitorFieldsV0 + var tcp *tfTCPMonitorFieldsV0 + var icmp *tfICMPMonitorFieldsV0 + var browser *tfBrowserMonitorFieldsV0 + + switch mType := api.Type; mType { + case kbapi.Http: + http = &tfHTTPMonitorFieldsV0{} + if v.HTTP != nil { + http = v.HTTP + } + http = http.toTfHTTPMonitorFieldsV0(ctx, dg, api) + case kbapi.Tcp: + tcp = &tfTCPMonitorFieldsV0{} + if v.TCP != nil { + tcp = v.TCP + } + tcp = tcp.toTfTCPMonitorFieldsV0(ctx, dg, api) + case kbapi.Icmp: + icmp = &tfICMPMonitorFieldsV0{} + if v.ICMP != nil { + icmp = v.ICMP + } + icmp, err = icmp.toTfICMPMonitorFieldsV0(api) + case kbapi.Browser: + browser = &tfBrowserMonitorFieldsV0{} + if v.Browser != nil { + browser = v.Browser + } + browser, err = browser.toTfBrowserMonitorFieldsV0(api) + default: + err = fmt.Errorf("unsupported monitor type: %s", mType) + } + + if err != nil { + dg.AddError("Failed to convert monitor fields", err.Error()) + return nil, dg + } + + params := v.Params + if api.Params != nil { + params, err = toNormalizedValue(api.Params) + if err != nil { + dg.AddError("Failed to parse params", err.Error()) + return nil, dg + } + } + + resourceID := clients.CompositeId{ + ClusterId: space, + ResourceId: string(api.Id), + } + + alertV0, dg := toTfAlertConfigV0(ctx, api.Alert) + if dg.HasError() { + return nil, dg + } + + return &tfModelV0{ + ID: types.StringValue(resourceID.String()), + Name: types.StringValue(api.Name), + SpaceID: types.StringValue(space), + Namespace: types.StringValue(api.Namespace), + Schedule: types.Int64Value(schedule), + Locations: v.Locations, + PrivateLocations: synthetics.StringSliceValue(privateLocLabels), + Enabled: types.BoolPointerValue(api.Enabled), + Tags: synthetics.StringSliceValue(api.Tags), + Labels: utils.MapValueFrom(ctx, api.Labels, types.StringType, path.Root("labels"), &dg), + Alert: alertV0, + APMServiceName: types.StringValue(api.APMServiceName), + TimeoutSeconds: types.Int64Value(timeout), + Params: params, + HTTP: http, + TCP: tcp, + ICMP: icmp, + Browser: browser, + RetestOnFailure: v.RetestOnFailure, + }, dg +} + +func (v *tfTCPMonitorFieldsV0) toTfTCPMonitorFieldsV0(ctx context.Context, dg diag.Diagnostics, api *kbapi.SyntheticsMonitor) *tfTCPMonitorFieldsV0 { + checkSend := v.CheckSend + if api.CheckSend != "" { + checkSend = types.StringValue(api.CheckSend) + } + checkReceive := v.CheckReceive + if api.CheckReceive != "" { + checkReceive = types.StringValue(api.CheckReceive) + } + sslCfg, dg := toTFSSLConfig(ctx, dg, api, "tcp") + + if dg.HasError() { + return nil + } + return &tfTCPMonitorFieldsV0{ + Host: types.StringValue(api.Host), + CheckSend: checkSend, + CheckReceive: checkReceive, + ProxyURL: types.StringValue(api.ProxyUrl), + ProxyUseLocalResolver: types.BoolPointerValue(api.ProxyUseLocalResolver), + tfSSLConfig: sslCfg, + } +} + +func (v *tfICMPMonitorFieldsV0) toTfICMPMonitorFieldsV0(api *kbapi.SyntheticsMonitor) (*tfICMPMonitorFieldsV0, error) { + wait, err := stringToInt64(string(api.Wait)) + if err != nil { + return nil, err + } + return &tfICMPMonitorFieldsV0{ + Host: types.StringValue(api.Host), + Wait: types.Int64Value(wait), + }, nil +} + +func (v *tfBrowserMonitorFieldsV0) toTfBrowserMonitorFieldsV0(api *kbapi.SyntheticsMonitor) (*tfBrowserMonitorFieldsV0, error) { + + var err error + playwrightOptions := v.PlaywrightOptions + if api.PlaywrightOptions != nil { + playwrightOptions, err = toNormalizedValue(api.PlaywrightOptions) + if err != nil { + return nil, err + } + } + + syntheticsArgs := v.SyntheticsArgs + if api.SyntheticsArgs != nil { + syntheticsArgs = synthetics.StringSliceValue(api.SyntheticsArgs) + } + + inlineScript := v.InlineScript + if api.InlineScript != "" { + inlineScript = types.StringValue(api.InlineScript) + } + + return &tfBrowserMonitorFieldsV0{ + InlineScript: inlineScript, + Screenshots: types.StringValue(api.Screenshots), + SyntheticsArgs: syntheticsArgs, + IgnoreHttpsErrors: types.BoolPointerValue(api.IgnoreHttpsErrors), + PlaywrightOptions: playwrightOptions, + }, nil +} + +func (v *tfHTTPMonitorFieldsV0) toTfHTTPMonitorFieldsV0(ctx context.Context, dg diag.Diagnostics, api *kbapi.SyntheticsMonitor) *tfHTTPMonitorFieldsV0 { + + var err error + proxyHeaders := v.ProxyHeader + if api.ProxyHeaders != nil { + proxyHeaders, err = toNormalizedValue(api.ProxyHeaders) + if err != nil { + dg.AddError("Failed to parse proxy_headers", err.Error()) + return nil + } + } + + username := v.Username + if api.Username != "" { + username = types.StringValue(api.Username) + } + password := v.Password + if api.Password != "" { + password = types.StringValue(api.Password) + } + + maxRedirects, err := stringToInt64(api.MaxRedirects) + if err != nil { + dg.AddError("Failed to parse max_redirects", err.Error()) + return nil + } + + sslCfg, dg := toTFSSLConfig(ctx, dg, api, "http") + if dg.HasError() { + return nil + } + return &tfHTTPMonitorFieldsV0{ + URL: types.StringValue(api.Url), + MaxRedirects: types.Int64Value(maxRedirects), + Mode: types.StringValue(string(api.Mode)), + IPv4: types.BoolPointerValue(api.Ipv4), + IPv6: types.BoolPointerValue(api.Ipv6), + Username: username, + Password: password, + ProxyHeader: proxyHeaders, + ProxyURL: types.StringValue(api.ProxyUrl), + Check: v.Check, + Response: v.Response, + tfSSLConfig: sslCfg, + } +} + +func toTFSSLConfig(ctx context.Context, dg diag.Diagnostics, api *kbapi.SyntheticsMonitor, p string) (tfSSLConfig, diag.Diagnostics) { + sslSupportedProtocols := utils.SliceToListType_String(ctx, api.SslSupportedProtocols, path.Root(p).AtName("ssl_supported_protocols"), &dg) + return tfSSLConfig{ + SslVerificationMode: types.StringValue(api.SslVerificationMode), + SslSupportedProtocols: sslSupportedProtocols, + SslCertificateAuthorities: synthetics.StringSliceValue(api.SslCertificateAuthorities), + SslCertificate: types.StringValue(api.SslCertificate), + SslKey: types.StringValue(api.SslKey), + SslKeyPassphrase: types.StringValue(api.SslKeyPassphrase), + }, dg +} + +func toTfAlertConfigV0(ctx context.Context, alert *kbapi.MonitorAlertConfig) (basetypes.ObjectValue, diag.Diagnostics) { + + dg := diag.Diagnostics{} + + alertAttributes := monitorAlertConfigSchema().GetType().(attr.TypeWithAttributeTypes).AttributeTypes() + + var emptyAttr = map[string]attr.Type(nil) + + if alert == nil { + return basetypes.NewObjectNull(emptyAttr), dg + } + + tfAlertConfig := tfAlertConfigV0{ + Status: toTfStatusConfigV0(alert.Status), + TLS: toTfStatusConfigV0(alert.Tls), + } + + return types.ObjectValueFrom(ctx, alertAttributes, &tfAlertConfig) +} + +func toTfStatusConfigV0(status *kbapi.SyntheticsStatusConfig) *tfStatusConfigV0 { + if status == nil { + return nil + } + return &tfStatusConfigV0{ + Enabled: types.BoolPointerValue(status.Enabled), + } +} + +func (v *tfModelV0) toKibanaAPIRequest(ctx context.Context) (*kibanaAPIRequest, diag.Diagnostics) { + + fields, dg := v.toMonitorFields(ctx) + if dg.HasError() { + return nil, dg + } + config, dg := v.toSyntheticsMonitorConfig(ctx) + if dg.HasError() { + return nil, dg + } + return &kibanaAPIRequest{ + fields: fields, + config: *config, + }, dg +} + +func (v *tfModelV0) toMonitorFields(ctx context.Context) (kbapi.MonitorFields, diag.Diagnostics) { + dg := diag.Diagnostics{} + + if v.HTTP != nil { + return v.toHttpMonitorFields(ctx) + } else if v.TCP != nil { + return v.toTCPMonitorFields(ctx) + } else if v.ICMP != nil { + return v.toICMPMonitorFields(), dg + } else if v.Browser != nil { + return v.toBrowserMonitorFields() + } + + dg.AddError("Unsupported monitor type config", "one of http,tcp monitor fields is required") + return nil, dg +} + +func toTFAlertConfig(ctx context.Context, v basetypes.ObjectValue) *kbapi.MonitorAlertConfig { + var alert *kbapi.MonitorAlertConfig + if !v.IsNull() && !v.IsUnknown() { + tfAlert := tfAlertConfigV0{} + tfsdk.ValueAs(ctx, v, &tfAlert) + alert = tfAlert.toTfAlertConfigV0() + } + return alert +} + +func (v *tfModelV0) toSyntheticsMonitorConfig(ctx context.Context) (*kbapi.SyntheticsMonitorConfig, diag.Diagnostics) { + locations := Map[types.String, kbapi.MonitorLocation](v.Locations, func(s types.String) kbapi.MonitorLocation { return kbapi.MonitorLocation(s.ValueString()) }) + params, dg := toJsonObject(v.Params) + if dg.HasError() { + return nil, dg + } + + labels := utils.MapTypeAs[string](ctx, v.Labels, path.Root("labels"), &dg) + if dg.HasError() { + return nil, dg + } + + if labels == nil { + labels = map[string]string{} + } + + return &kbapi.SyntheticsMonitorConfig{ + Name: v.Name.ValueString(), + Schedule: kbapi.MonitorSchedule(v.Schedule.ValueInt64()), + Locations: locations, + PrivateLocations: synthetics.ValueStringSlice(v.PrivateLocations), + Enabled: v.Enabled.ValueBoolPointer(), + Tags: synthetics.ValueStringSlice(v.Tags), + Labels: labels, + Alert: toTFAlertConfig(ctx, v.Alert), + APMServiceName: v.APMServiceName.ValueString(), + TimeoutSeconds: int(v.TimeoutSeconds.ValueInt64()), + Namespace: v.Namespace.ValueString(), + Params: params, + RetestOnFailure: v.RetestOnFailure.ValueBoolPointer(), + }, dg +} + +func tfInt64ToString(v types.Int64) string { + res := "" + if !v.IsUnknown() && !v.IsNull() { // handle omitempty case + return strconv.FormatInt(v.ValueInt64(), 10) + } + return res +} + +func toSSLConfig(ctx context.Context, dg diag.Diagnostics, v tfSSLConfig, p string) (*kbapi.SSLConfig, diag.Diagnostics) { + + var ssl *kbapi.SSLConfig + if !v.SslSupportedProtocols.IsNull() && !v.SslSupportedProtocols.IsUnknown() { + sslSupportedProtocols := utils.ListTypeToSlice_String(ctx, v.SslSupportedProtocols, path.Root(p).AtName("ssl_supported_protocols"), &dg) + if dg.HasError() { + return nil, dg + } + ssl = &kbapi.SSLConfig{} + ssl.SupportedProtocols = sslSupportedProtocols + } + + if !v.SslVerificationMode.IsNull() && !v.SslVerificationMode.IsUnknown() { + if ssl == nil { + ssl = &kbapi.SSLConfig{} + } + ssl.VerificationMode = v.SslVerificationMode.ValueString() + } + + certAuths := synthetics.ValueStringSlice(v.SslCertificateAuthorities) + if len(certAuths) > 0 { + if ssl == nil { + ssl = &kbapi.SSLConfig{} + } + ssl.CertificateAuthorities = certAuths + } + + if !v.SslCertificate.IsUnknown() && !v.SslCertificate.IsNull() { + if ssl == nil { + ssl = &kbapi.SSLConfig{} + } + ssl.Certificate = v.SslCertificate.ValueString() + } + + if !v.SslKey.IsUnknown() && !v.SslKey.IsNull() { + if ssl == nil { + ssl = &kbapi.SSLConfig{} + } + ssl.Key = v.SslKey.ValueString() + } + + if !v.SslKeyPassphrase.IsUnknown() && !v.SslKeyPassphrase.IsNull() { + if ssl == nil { + ssl = &kbapi.SSLConfig{} + } + ssl.KeyPassphrase = v.SslKeyPassphrase.ValueString() + } + return ssl, dg +} + +func (v *tfModelV0) toHttpMonitorFields(ctx context.Context) (kbapi.MonitorFields, diag.Diagnostics) { + http := v.HTTP + proxyHeaders, dg := toJsonObject(http.ProxyHeader) + if dg.HasError() { + return nil, dg + } + response, dg := toJsonObject(http.Response) + if dg.HasError() { + return nil, dg + } + check, dg := toJsonObject(http.Check) + if dg.HasError() { + return nil, dg + } + + ssl, dg := toSSLConfig(ctx, dg, http.tfSSLConfig, "http") + + maxRedirects := tfInt64ToString(http.MaxRedirects) + return kbapi.HTTPMonitorFields{ + Url: http.URL.ValueString(), + Ssl: ssl, + MaxRedirects: maxRedirects, + Mode: kbapi.HttpMonitorMode(http.Mode.ValueString()), + Ipv4: http.IPv4.ValueBoolPointer(), + Ipv6: http.IPv6.ValueBoolPointer(), + Username: http.Username.ValueString(), + Password: http.Password.ValueString(), + ProxyHeader: proxyHeaders, + ProxyUrl: http.ProxyURL.ValueString(), + Response: response, + Check: check, + }, dg +} + +func (v *tfModelV0) toTCPMonitorFields(ctx context.Context) (kbapi.MonitorFields, diag.Diagnostics) { + + tcp := v.TCP + + dg := diag.Diagnostics{} + ssl, dg := toSSLConfig(ctx, dg, tcp.tfSSLConfig, "tcp") + + return kbapi.TCPMonitorFields{ + Host: tcp.Host.ValueString(), + CheckSend: tcp.CheckSend.ValueString(), + CheckReceive: tcp.CheckReceive.ValueString(), + ProxyUrl: tcp.ProxyURL.ValueString(), + ProxyUseLocalResolver: tcp.ProxyUseLocalResolver.ValueBoolPointer(), + Ssl: ssl, + }, dg +} + +func (v *tfModelV0) toICMPMonitorFields() kbapi.MonitorFields { + return kbapi.ICMPMonitorFields{ + Host: v.ICMP.Host.ValueString(), + Wait: tfInt64ToString(v.ICMP.Wait), + } +} + +func (v *tfModelV0) toBrowserMonitorFields() (kbapi.MonitorFields, diag.Diagnostics) { + playwrightOptions, dg := toJsonObject(v.Browser.PlaywrightOptions) + if dg.HasError() { + return nil, dg + } + return kbapi.BrowserMonitorFields{ + InlineScript: v.Browser.InlineScript.ValueString(), + Screenshots: kbapi.ScreenshotOption(v.Browser.Screenshots.ValueString()), + SyntheticsArgs: synthetics.ValueStringSlice(v.Browser.SyntheticsArgs), + IgnoreHttpsErrors: v.Browser.IgnoreHttpsErrors.ValueBoolPointer(), + PlaywrightOptions: playwrightOptions, + }, diag.Diagnostics{} //dg +} + +func Map[T, U any](ts []T, f func(T) U) []U { + var us []U + for _, v := range ts { + us = append(us, f(v)) + } + return us +} + +func (v tfAlertConfigV0) toTfAlertConfigV0() *kbapi.MonitorAlertConfig { + var status *kbapi.SyntheticsStatusConfig + if v.Status != nil { + status = v.Status.toTfStatusConfigV0() + } + var tls *kbapi.SyntheticsStatusConfig + if v.TLS != nil { + tls = v.TLS.toTfStatusConfigV0() + } + return &kbapi.MonitorAlertConfig{ + Status: status, + Tls: tls, + } +} + +func (v tfStatusConfigV0) toTfStatusConfigV0() *kbapi.SyntheticsStatusConfig { + return &kbapi.SyntheticsStatusConfig{ + Enabled: v.Enabled.ValueBoolPointer(), + } +} + +func (v tfModelV0) enforceVersionConstraints(ctx context.Context, client *clients.ApiClient) diag.Diagnostics { + if utils.IsKnown(v.Labels) { + isSupported, sdkDiags := client.EnforceMinVersion(ctx, MinLabelsVersion) + diags := diagutil.FrameworkDiagsFromSDK(sdkDiags) + if diags.HasError() { + return diags + } + + if !isSupported { + diags.AddAttributeError( + path.Root("labels"), + "Unsupported version for `labels` attribute", + fmt.Sprintf("The `labels` attribute requires server version %s or higher. Either remove the `labels` attribute or upgrade your Elastic Stack installation.", MinLabelsVersion.String()), + ) + return diags + } + } + + return nil +} diff --git a/internal/kibana/synthetics/schema_test.go b/internal/kibana/synthetics/monitor/schema_test.go similarity index 93% rename from internal/kibana/synthetics/schema_test.go rename to internal/kibana/synthetics/monitor/schema_test.go index 2b0ce8d69..f8162aa06 100644 --- a/internal/kibana/synthetics/schema_test.go +++ b/internal/kibana/synthetics/monitor/schema_test.go @@ -1,4 +1,4 @@ -package synthetics +package monitor import ( "context" @@ -25,96 +25,6 @@ func boolPointer(v bool) *bool { return res } -func TestMapStringValue(t *testing.T) { - testcases := []struct { - name string - input map[string]string - expected types.Map - }{ - { - name: "nil map", - input: nil, - expected: types.MapNull(types.StringType), - }, - { - name: "empty map", - input: map[string]string{}, - expected: types.MapNull(types.StringType), - }, - { - name: "map with values", - input: map[string]string{ - "environment": "production", - "team": "platform", - }, - expected: func() types.Map { - elements := map[string]attr.Value{ - "environment": types.StringValue("production"), - "team": types.StringValue("platform"), - } - mapValue, _ := types.MapValue(types.StringType, elements) - return mapValue - }(), - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - result := MapStringValue(tc.input) - assert.Equal(t, tc.expected, result) - }) - } -} - -func TestValueStringMap(t *testing.T) { - testcases := []struct { - name string - input types.Map - expected map[string]string - }{ - { - name: "null map", - input: types.MapNull(types.StringType), - expected: map[string]string{}, - }, - { - name: "unknown map", - input: types.MapUnknown(types.StringType), - expected: map[string]string{}, - }, - { - name: "empty map", - input: func() types.Map { - mapValue, _ := types.MapValue(types.StringType, map[string]attr.Value{}) - return mapValue - }(), - expected: map[string]string{}, - }, - { - name: "map with values", - input: func() types.Map { - elements := map[string]attr.Value{ - "environment": types.StringValue("production"), - "team": types.StringValue("platform"), - } - mapValue, _ := types.MapValue(types.StringType, elements) - return mapValue - }(), - expected: map[string]string{ - "environment": "production", - "team": "platform", - }, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - result := ValueStringMap(tc.input) - assert.Equal(t, tc.expected, result) - }) - } -} - func TestLabelsFieldConversion(t *testing.T) { testcases := []struct { name string @@ -135,7 +45,7 @@ func TestLabelsFieldConversion(t *testing.T) { Type: kbapi.Http, Labels: map[string]string{}, }, - expected: types.MapNull(types.StringType), + expected: types.MapValueMust(types.StringType, map[string]attr.Value{}), }, { name: "monitor with labels", @@ -147,15 +57,11 @@ func TestLabelsFieldConversion(t *testing.T) { "service": "web-app", }, }, - expected: func() types.Map { - elements := map[string]attr.Value{ - "environment": types.StringValue("production"), - "team": types.StringValue("platform"), - "service": types.StringValue("web-app"), - } - mapValue, _ := types.MapValue(types.StringType, elements) - return mapValue - }(), + expected: types.MapValueMust(types.StringType, map[string]attr.Value{ + "environment": types.StringValue("production"), + "team": types.StringValue("platform"), + "service": types.StringValue("web-app"), + }), }, } diff --git a/internal/kibana/synthetics/update.go b/internal/kibana/synthetics/monitor/update.go similarity index 85% rename from internal/kibana/synthetics/update.go rename to internal/kibana/synthetics/monitor/update.go index 02063bb4b..167c0b005 100644 --- a/internal/kibana/synthetics/update.go +++ b/internal/kibana/synthetics/monitor/update.go @@ -1,16 +1,16 @@ -package synthetics +package monitor import ( "context" "fmt" "github.com/disaster37/go-kibana-rest/v8/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" "github.com/hashicorp/terraform-plugin-framework/resource" ) func (r *Resource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { - - kibanaClient := GetKibanaClient(r, response.Diagnostics) + kibanaClient := synthetics.GetKibanaClient(r, response.Diagnostics) if kibanaClient == nil { return } @@ -33,7 +33,7 @@ func (r *Resource) Update(ctx context.Context, request resource.UpdateRequest, r return } - monitorId, dg := GetCompositeId(plan.ID.ValueString()) + monitorId, dg := synthetics.GetCompositeId(plan.ID.ValueString()) response.Diagnostics.Append(dg...) if response.Diagnostics.HasError() { return diff --git a/internal/kibana/synthetics/parameter/resource_test.go b/internal/kibana/synthetics/parameter/acc_test.go similarity index 100% rename from internal/kibana/synthetics/parameter/resource_test.go rename to internal/kibana/synthetics/parameter/acc_test.go diff --git a/internal/kibana/synthetics/acc_pl_test.go b/internal/kibana/synthetics/private_location/acc_test.go similarity index 99% rename from internal/kibana/synthetics/acc_pl_test.go rename to internal/kibana/synthetics/private_location/acc_test.go index bdc607841..a48e1d693 100644 --- a/internal/kibana/synthetics/acc_pl_test.go +++ b/internal/kibana/synthetics/private_location/acc_test.go @@ -1,4 +1,4 @@ -package synthetics_test +package private_location_test // this test is in synthetics_test package, because of https://github.com/elastic/kibana/issues/190801 // having both tests in same package allows to use mutex in kibana API client and workaround the issue diff --git a/internal/kibana/synthetics/private_location/schema.go b/internal/kibana/synthetics/private_location/schema.go index ad55cc2f9..c745c932a 100644 --- a/internal/kibana/synthetics/private_location/schema.go +++ b/internal/kibana/synthetics/private_location/schema.go @@ -16,11 +16,11 @@ import ( ) type tfModelV0 struct { - ID types.String `tfsdk:"id"` - Label types.String `tfsdk:"label"` - AgentPolicyId types.String `tfsdk:"agent_policy_id"` - Tags []types.String `tfsdk:"tags"` //> string - Geo *synthetics.TFGeoConfigV0 `tfsdk:"geo"` + ID types.String `tfsdk:"id"` + Label types.String `tfsdk:"label"` + AgentPolicyId types.String `tfsdk:"agent_policy_id"` + Tags []types.String `tfsdk:"tags"` //> string + Geo *tfGeoConfigV0 `tfsdk:"geo"` } func privateLocationSchema() schema.Schema { @@ -62,7 +62,7 @@ func privateLocationSchema() schema.Schema { listplanmodifier.RequiresReplace(), }, }, - "geo": synthetics.GeoConfigSchema(), + "geo": geoConfigSchema(), }, } } @@ -70,7 +70,7 @@ func privateLocationSchema() schema.Schema { func (m *tfModelV0) toPrivateLocationConfig() kbapi.PrivateLocationConfig { var geoConfig *kbapi.SyntheticGeoConfig if m.Geo != nil { - geoConfig = m.Geo.ToSyntheticGeoConfig() + geoConfig = m.Geo.toSyntheticGeoConfig() } return kbapi.PrivateLocationConfig{ @@ -96,9 +96,51 @@ func toModelV0(pLoc kbapi.PrivateLocation) tfModelV0 { Label: types.StringValue(pLoc.Label), AgentPolicyId: types.StringValue(pLoc.AgentPolicyId), Tags: synthetics.StringSliceValue(pLoc.Tags), - Geo: synthetics.FromSyntheticGeoConfig(pLoc.Geo), + Geo: fromSyntheticGeoConfig(pLoc.Geo), } } //go:embed resource-description.md var syntheticsPrivateLocationDescription string + +// Geographic configuration schema and types +func geoConfigSchema() schema.Attribute { + return schema.SingleNestedAttribute{ + Optional: true, + Description: "Geographic coordinates (WGS84) for the location", + Attributes: map[string]schema.Attribute{ + "lat": schema.Float64Attribute{ + Optional: false, + Required: true, + MarkdownDescription: "The latitude of the location.", + }, + "lon": schema.Float64Attribute{ + Optional: false, + Required: true, + MarkdownDescription: "The longitude of the location.", + }, + }, + } +} + +type tfGeoConfigV0 struct { + Lat types.Float64 `tfsdk:"lat"` + Lon types.Float64 `tfsdk:"lon"` +} + +func (m *tfGeoConfigV0) toSyntheticGeoConfig() *kbapi.SyntheticGeoConfig { + return &kbapi.SyntheticGeoConfig{ + Lat: m.Lat.ValueFloat64(), + Lon: m.Lon.ValueFloat64(), + } +} + +func fromSyntheticGeoConfig(v *kbapi.SyntheticGeoConfig) *tfGeoConfigV0 { + if v == nil { + return nil + } + return &tfGeoConfigV0{ + Lat: types.Float64Value(v.Lat), + Lon: types.Float64Value(v.Lon), + } +} diff --git a/internal/kibana/synthetics/schema.go b/internal/kibana/synthetics/schema.go index 942e7b2b2..721e1275e 100644 --- a/internal/kibana/synthetics/schema.go +++ b/internal/kibana/synthetics/schema.go @@ -1,125 +1,18 @@ package synthetics import ( - "context" - _ "embed" - "encoding/json" "fmt" - "regexp" - "strconv" - "github.com/disaster37/go-kibana-rest/v8/kbapi" "github.com/elastic/terraform-provider-elasticstack/internal/clients" - "github.com/elastic/terraform-provider-elasticstack/internal/diagutil" - "github.com/elastic/terraform-provider-elasticstack/internal/utils" - "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) const ( MetadataPrefix = "_kibana_synthetics_" ) -type kibanaAPIRequest struct { - fields kbapi.MonitorFields - config kbapi.SyntheticsMonitorConfig -} - -type tfStatusConfigV0 struct { - Enabled types.Bool `tfsdk:"enabled"` -} - -type tfAlertConfigV0 struct { - Status *tfStatusConfigV0 `tfsdk:"status"` - TLS *tfStatusConfigV0 `tfsdk:"tls"` -} - -type tfSSLConfig struct { - SslVerificationMode types.String `tfsdk:"ssl_verification_mode"` - SslSupportedProtocols types.List `tfsdk:"ssl_supported_protocols"` - SslCertificateAuthorities []types.String `tfsdk:"ssl_certificate_authorities"` - SslCertificate types.String `tfsdk:"ssl_certificate"` - SslKey types.String `tfsdk:"ssl_key"` - SslKeyPassphrase types.String `tfsdk:"ssl_key_passphrase"` -} - -type tfHTTPMonitorFieldsV0 struct { - URL types.String `tfsdk:"url"` - MaxRedirects types.Int64 `tfsdk:"max_redirects"` - Mode types.String `tfsdk:"mode"` - IPv4 types.Bool `tfsdk:"ipv4"` - IPv6 types.Bool `tfsdk:"ipv6"` - ProxyURL types.String `tfsdk:"proxy_url"` - ProxyHeader jsontypes.Normalized `tfsdk:"proxy_header"` - Username types.String `tfsdk:"username"` - Password types.String `tfsdk:"password"` - Response jsontypes.Normalized `tfsdk:"response"` - Check jsontypes.Normalized `tfsdk:"check"` - - tfSSLConfig -} - -type tfTCPMonitorFieldsV0 struct { - Host types.String `tfsdk:"host"` - CheckSend types.String `tfsdk:"check_send"` - CheckReceive types.String `tfsdk:"check_receive"` - ProxyURL types.String `tfsdk:"proxy_url"` - ProxyUseLocalResolver types.Bool `tfsdk:"proxy_use_local_resolver"` - - tfSSLConfig -} - -type tfICMPMonitorFieldsV0 struct { - Host types.String `tfsdk:"host"` - Wait types.Int64 `tfsdk:"wait"` -} - -type tfBrowserMonitorFieldsV0 struct { - InlineScript types.String `tfsdk:"inline_script"` - Screenshots types.String `tfsdk:"screenshots"` - SyntheticsArgs []types.String `tfsdk:"synthetics_args"` - IgnoreHttpsErrors types.Bool `tfsdk:"ignore_https_errors"` - PlaywrightOptions jsontypes.Normalized `tfsdk:"playwright_options"` -} - -type tfModelV0 struct { - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - SpaceID types.String `tfsdk:"space_id"` - Namespace types.String `tfsdk:"namespace"` - Schedule types.Int64 `tfsdk:"schedule"` - Locations []types.String `tfsdk:"locations"` - PrivateLocations []types.String `tfsdk:"private_locations"` - Enabled types.Bool `tfsdk:"enabled"` - Tags []types.String `tfsdk:"tags"` - Labels types.Map `tfsdk:"labels"` - Alert types.Object `tfsdk:"alert"` //tfAlertConfigV0 - APMServiceName types.String `tfsdk:"service_name"` - TimeoutSeconds types.Int64 `tfsdk:"timeout"` - HTTP *tfHTTPMonitorFieldsV0 `tfsdk:"http"` - TCP *tfTCPMonitorFieldsV0 `tfsdk:"tcp"` - ICMP *tfICMPMonitorFieldsV0 `tfsdk:"icmp"` - Browser *tfBrowserMonitorFieldsV0 `tfsdk:"browser"` - Params jsontypes.Normalized `tfsdk:"params"` - RetestOnFailure types.Bool `tfsdk:"retest_on_failure"` -} - +// GetCompositeId parses a composite ID and returns the parsed components func GetCompositeId(id string) (*clients.CompositeId, diag.Diagnostics) { compositeID, sdkDiag := clients.CompositeIdFromStr(id) dg := diag.Diagnostics{} @@ -130,424 +23,7 @@ func GetCompositeId(id string) (*clients.CompositeId, diag.Diagnostics) { return compositeID, dg } -//go:embed resource-description.md -var monitorDescription string - -func monitorConfigSchema() schema.Schema { - return schema.Schema{ - MarkdownDescription: monitorDescription, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Computed: true, - MarkdownDescription: "Generated identifier for the monitor", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - }, - "name": schema.StringAttribute{ - Optional: false, - Required: true, - MarkdownDescription: "The monitor’s name.", - }, - "space_id": schema.StringAttribute{ - MarkdownDescription: "Kibana space. The space ID that is part of the Kibana URL when inside the space. Space IDs are limited to lowercase alphanumeric, underscore, and hyphen characters (a-z, 0-9, _, and -). You are cannot change the ID with the update operation.", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - Computed: true, - }, - "namespace": schema.StringAttribute{ - MarkdownDescription: "The data stream namespace. Note: if you change its value, kibana creates new datastream. A user needs permissions for new/old datastream in update case to be able to see full monitor history. The `namespace` field should be lowercase and not contain spaces. The namespace must not include any of the following characters: *, \\, /, ?, \", <, >, |, whitespace, ,, #, :, or -. Default: `default`", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Computed: true, - Validators: []validator.String{ - stringvalidator.RegexMatches( - regexp.MustCompile(`^[^*\\/?\"<>|\s,#:-]*$`), - "namespace must not contain any of the following characters: *, \\, /, ?, \", <, >, |, whitespace, ,, #, :, or -", - ), - }, - }, - "schedule": schema.Int64Attribute{ - Optional: true, - MarkdownDescription: "The monitor’s schedule in minutes. Supported values are 1, 3, 5, 10, 15, 30, 60, 120 and 240.", - Validators: []validator.Int64{ - int64validator.OneOf(1, 3, 5, 10, 15, 30, 60, 120, 240), - }, - Computed: true, - PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, - }, - "locations": schema.ListAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "Where to deploy the monitor. Monitors can be deployed in multiple locations so that you can detect differences in availability and response times across those locations.", - Validators: []validator.List{ - listvalidator.ValueStringsAre( - stringvalidator.OneOf( - "japan", - "india", - "singapore", - "australia_east", - "united_kingdom", - "germany", - "canada_east", - "brazil", - "us_east", - "us_west", - ), - ), - }, - }, - "private_locations": schema.ListAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "These Private Locations refer to locations hosted and managed by you, whereas locations are hosted by Elastic. You can specify a Private Location using the location’s name.", - }, - "enabled": schema.BoolAttribute{ - Optional: true, - MarkdownDescription: "Whether the monitor is enabled. Default: `true`", - Computed: true, - PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, - }, - "tags": schema.ListAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "An array of tags.", - }, - "labels": schema.MapAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "Key-value pairs of labels to associate with the monitor. Labels can be used for filtering and grouping monitors.", - }, - "alert": monitorAlertConfigSchema(), - "service_name": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "The APM service name.", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - }, - "timeout": schema.Int64Attribute{ - Optional: true, - MarkdownDescription: "The monitor timeout in seconds, monitor will fail if it doesn’t complete within this time. Default: `16`", - Computed: true, - PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, - }, - "params": jsonObjectSchema("Monitor parameters"), - "http": httpMonitorFieldsSchema(), - "tcp": tcpMonitorFieldsSchema(), - "icmp": icmpMonitorFieldsSchema(), - "browser": browserMonitorFieldsSchema(), - "retest_on_failure": schema.BoolAttribute{ - Optional: true, - MarkdownDescription: "Enable or disable retesting when a monitor fails. By default, monitors are automatically retested if the monitor goes from \"up\" to \"down\". If the result of the retest is also \"down\", an error will be created, and if configured, an alert sent. Then the monitor will resume running according to the defined schedule. Using retest_on_failure can reduce noise related to transient problems. Default: `true`.", - }, - }, - } -} - -func browserMonitorFieldsSchema() schema.Attribute { - return schema.SingleNestedAttribute{ - Optional: true, - MarkdownDescription: "Browser Monitor specific fields", - Attributes: map[string]schema.Attribute{ - "inline_script": schema.StringAttribute{ - Optional: false, - Required: true, - MarkdownDescription: "The inline script.", - }, - "screenshots": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "Controls the behavior of the screenshots feature.", - Validators: []validator.String{ - stringvalidator.OneOf("on", "off", "only-on-failure"), - }, - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - }, - "synthetics_args": schema.ListAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "Synthetics agent CLI arguments.", - }, - "ignore_https_errors": schema.BoolAttribute{ - Optional: true, - MarkdownDescription: "Whether to ignore HTTPS errors.", - Computed: true, - PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, - }, - "playwright_options": jsonObjectSchema("Playwright options."), - }, - } -} - -func icmpMonitorFieldsSchema() schema.Attribute { - return schema.SingleNestedAttribute{ - Optional: true, - MarkdownDescription: "ICMP Monitor specific fields", - Attributes: map[string]schema.Attribute{ - "host": schema.StringAttribute{ - Optional: false, - Required: true, - MarkdownDescription: "Host to ping; it can be an IP address or a hostname.", - }, - "wait": schema.Int64Attribute{ - Optional: true, - MarkdownDescription: " Wait time in seconds. Default: `1`", - PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, - Computed: true, - }, - }, - } -} - -func jsonObjectSchema(doc string) schema.Attribute { - return schema.StringAttribute{ - Optional: true, - MarkdownDescription: fmt.Sprintf("%s. Raw JSON object, use `jsonencode` function to represent JSON", doc), - CustomType: jsontypes.NormalizedType{}, - } -} - -func statusConfigSchema() schema.Attribute { - return schema.SingleNestedAttribute{ - Optional: true, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, - }, - }, - } -} - -func monitorAlertConfigSchema() schema.Attribute { - return schema.SingleNestedAttribute{ - Optional: true, - MarkdownDescription: "Alert configuration. Default: `{ status: { enabled: true }, tls: { enabled: true } }`.", - Attributes: map[string]schema.Attribute{ - "status": statusConfigSchema(), - "tls": statusConfigSchema(), - }, - Computed: true, - PlanModifiers: []planmodifier.Object{objectplanmodifier.UseStateForUnknown()}, - } -} - -func httpMonitorFieldsSchema() schema.Attribute { - return schema.SingleNestedAttribute{ - Optional: true, - MarkdownDescription: "HTTP Monitor specific fields", - Attributes: map[string]schema.Attribute{ - "url": schema.StringAttribute{ - Optional: false, - Required: true, - MarkdownDescription: "URL to monitor.", - }, - "ssl_verification_mode": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "Controls the verification of server certificates. ", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - }, - "ssl_supported_protocols": schema.ListAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "List of allowed SSL/TLS versions.", - Computed: true, - PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, - }, - "ssl_certificate_authorities": schema.ListAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "The list of root certificates for verifications is required.", - }, - "ssl_certificate": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "Certificate.", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - }, - "ssl_key": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "Certificate key.", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - Sensitive: true, - }, - "ssl_key_passphrase": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "Key passphrase.", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - Sensitive: true, - }, - "max_redirects": schema.Int64Attribute{ - Optional: true, - MarkdownDescription: "The maximum number of redirects to follow. Default: `0`", - PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, - Computed: true, - }, - "mode": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "The mode of the monitor. Can be \"all\" or \"any\". If you’re using a DNS-load balancer and want to ping every IP address for the specified hostname, you should use all.", - Validators: []validator.String{ - stringvalidator.OneOf("any", "all"), - }, - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - }, - "ipv4": schema.BoolAttribute{ - Optional: true, - MarkdownDescription: "Whether to ping using the ipv4 protocol.", - Computed: true, - PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, - }, - "ipv6": schema.BoolAttribute{ - Optional: true, - MarkdownDescription: "Whether to ping using the ipv6 protocol.", - Computed: true, - PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, - }, - "username": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "The username for authenticating with the server. The credentials are passed with the request.", - }, - "password": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "The password for authenticating with the server. The credentials are passed with the request.", - }, - "proxy_header": jsonObjectSchema("Additional headers to send to proxies during CONNECT requests."), - "proxy_url": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "The URL of the proxy to use for this monitor.", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - }, - "response": jsonObjectSchema("Controls the indexing of the HTTP response body contents to the `http.response.body.contents` field."), - "check": jsonObjectSchema("The check request settings."), - }, - } -} - -func tcpMonitorFieldsSchema() schema.Attribute { - return schema.SingleNestedAttribute{ - Optional: true, - MarkdownDescription: "TCP Monitor specific fields", - Attributes: map[string]schema.Attribute{ - "host": schema.StringAttribute{ - Optional: false, - Required: true, - MarkdownDescription: "The host to monitor; it can be an IP address or a hostname. The host can include the port using a colon (e.g., \"example.com:9200\").", - }, - "ssl_verification_mode": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "Controls the verification of server certificates. ", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - }, - "ssl_supported_protocols": schema.ListAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "List of allowed SSL/TLS versions.", - Computed: true, - PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, - }, - "ssl_certificate_authorities": schema.ListAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "The list of root certificates for verifications is required.", - }, - "ssl_certificate": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "Certificate.", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - }, - "ssl_key": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "Certificate key.", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - Sensitive: true, - }, - "ssl_key_passphrase": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "Key passphrase.", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - Sensitive: true, - }, - "check_send": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "An optional payload string to send to the remote host.", - }, - "check_receive": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "The expected answer. ", - }, - "proxy_url": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "The URL of the SOCKS5 proxy to use when connecting to the server. The value must be a URL with a scheme of `socks5://`. If the SOCKS5 proxy server requires client authentication, then a username and password can be embedded in the URL. When using a proxy, hostnames are resolved on the proxy server instead of on the client. You can change this behavior by setting the `proxy_use_local_resolver` option.", - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - }, - "proxy_use_local_resolver": schema.BoolAttribute{ - Optional: true, - MarkdownDescription: " A Boolean value that determines whether hostnames are resolved locally instead of being resolved on the proxy server. The default value is false, which means that name resolution occurs on the proxy server.", - Computed: true, - PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, - }, - }, - } -} - -func GeoConfigSchema() schema.Attribute { - return schema.SingleNestedAttribute{ - Optional: true, - Description: "Geographic coordinates (WGS84) for the location", - Attributes: map[string]schema.Attribute{ - "lat": schema.Float64Attribute{ - Optional: false, - Required: true, - MarkdownDescription: "The latitude of the location.", - }, - "lon": schema.Float64Attribute{ - Optional: false, - Required: true, - MarkdownDescription: "The longitude of the location.", - }, - }, - } -} - -type TFGeoConfigV0 struct { - Lat types.Float64 `tfsdk:"lat"` - Lon types.Float64 `tfsdk:"lon"` -} - -func (m *TFGeoConfigV0) ToSyntheticGeoConfig() *kbapi.SyntheticGeoConfig { - return &kbapi.SyntheticGeoConfig{ - Lat: m.Lat.ValueFloat64(), - Lon: m.Lon.ValueFloat64(), - } -} - -func FromSyntheticGeoConfig(v *kbapi.SyntheticGeoConfig) *TFGeoConfigV0 { - if v == nil { - return nil - } - return &TFGeoConfigV0{ - Lat: types.Float64Value(v.Lat), - Lon: types.Float64Value(v.Lon), - } -} - +// Shared utility functions for type conversions func ValueStringSlice(v []types.String) []string { var res []string for _, s := range v { @@ -563,563 +39,3 @@ func StringSliceValue(v []string) []types.String { } return res } - -func MapStringValue(v map[string]string) types.Map { - if len(v) == 0 { - return types.MapNull(types.StringType) - } - elements := make(map[string]attr.Value) - for k, val := range v { - elements[k] = types.StringValue(val) - } - mapValue, _ := types.MapValue(types.StringType, elements) - return mapValue -} - -func ValueStringMap(v types.Map) map[string]string { - if v.IsNull() || v.IsUnknown() { - return make(map[string]string) - } - result := make(map[string]string) - for k, val := range v.Elements() { - if strVal, ok := val.(types.String); ok { - result[k] = strVal.ValueString() - } - } - return result -} - -func toNormalizedValue(jsObj kbapi.JsonObject) (jsontypes.Normalized, error) { - res, err := json.Marshal(jsObj) - if err != nil { - return jsontypes.NewNormalizedUnknown(), err - } - return jsontypes.NewNormalizedValue(string(res)), nil -} - -func toJsonObject(v jsontypes.Normalized) (kbapi.JsonObject, diag.Diagnostics) { - if v.IsNull() { - return nil, diag.Diagnostics{} - } - var res kbapi.JsonObject - dg := v.Unmarshal(&res) - if dg.HasError() { - return nil, dg - } - return res, diag.Diagnostics{} -} - -func stringToInt64(v string) (int64, error) { - var res int64 - var err error - if v != "" { - res, err = strconv.ParseInt(v, 10, 64) - } - return res, err -} - -func (v *tfModelV0) toModelV0(ctx context.Context, api *kbapi.SyntheticsMonitor, space string) (*tfModelV0, diag.Diagnostics) { - var schedule int64 - var err error - dg := diag.Diagnostics{} - if api.Schedule != nil { - schedule, err = stringToInt64(api.Schedule.Number) - if err != nil { - dg.AddError("Failed to convert schedule to int64", err.Error()) - return nil, dg - } - } - - var privateLocLabels []string - for _, l := range api.Locations { - if !l.IsServiceManaged { - privateLocLabels = append(privateLocLabels, l.Label) - } - } - - timeout, err := stringToInt64(string(api.Timeout)) - if err != nil { - dg.AddError("Failed to convert timeout to int64", err.Error()) - return nil, dg - } - - var http *tfHTTPMonitorFieldsV0 - var tcp *tfTCPMonitorFieldsV0 - var icmp *tfICMPMonitorFieldsV0 - var browser *tfBrowserMonitorFieldsV0 - - switch mType := api.Type; mType { - case kbapi.Http: - http = &tfHTTPMonitorFieldsV0{} - if v.HTTP != nil { - http = v.HTTP - } - http = http.toTfHTTPMonitorFieldsV0(ctx, dg, api) - case kbapi.Tcp: - tcp = &tfTCPMonitorFieldsV0{} - if v.TCP != nil { - tcp = v.TCP - } - tcp = tcp.toTfTCPMonitorFieldsV0(ctx, dg, api) - case kbapi.Icmp: - icmp = &tfICMPMonitorFieldsV0{} - if v.ICMP != nil { - icmp = v.ICMP - } - icmp, err = icmp.toTfICMPMonitorFieldsV0(api) - case kbapi.Browser: - browser = &tfBrowserMonitorFieldsV0{} - if v.Browser != nil { - browser = v.Browser - } - browser, err = browser.toTfBrowserMonitorFieldsV0(api) - default: - err = fmt.Errorf("unsupported monitor type: %s", mType) - } - - if err != nil { - dg.AddError("Failed to convert monitor fields", err.Error()) - return nil, dg - } - - params := v.Params - if api.Params != nil { - params, err = toNormalizedValue(api.Params) - if err != nil { - dg.AddError("Failed to parse params", err.Error()) - return nil, dg - } - } - - resourceID := clients.CompositeId{ - ClusterId: space, - ResourceId: string(api.Id), - } - - alertV0, dg := toTfAlertConfigV0(ctx, api.Alert) - if dg.HasError() { - return nil, dg - } - - return &tfModelV0{ - ID: types.StringValue(resourceID.String()), - Name: types.StringValue(api.Name), - SpaceID: types.StringValue(space), - Namespace: types.StringValue(api.Namespace), - Schedule: types.Int64Value(schedule), - Locations: v.Locations, - PrivateLocations: StringSliceValue(privateLocLabels), - Enabled: types.BoolPointerValue(api.Enabled), - Tags: StringSliceValue(api.Tags), - Labels: MapStringValue(api.Labels), - Alert: alertV0, - APMServiceName: types.StringValue(api.APMServiceName), - TimeoutSeconds: types.Int64Value(timeout), - Params: params, - HTTP: http, - TCP: tcp, - ICMP: icmp, - Browser: browser, - RetestOnFailure: v.RetestOnFailure, - }, dg -} - -func (v *tfTCPMonitorFieldsV0) toTfTCPMonitorFieldsV0(ctx context.Context, dg diag.Diagnostics, api *kbapi.SyntheticsMonitor) *tfTCPMonitorFieldsV0 { - checkSend := v.CheckSend - if api.CheckSend != "" { - checkSend = types.StringValue(api.CheckSend) - } - checkReceive := v.CheckReceive - if api.CheckReceive != "" { - checkReceive = types.StringValue(api.CheckReceive) - } - sslCfg, dg := toTFSSLConfig(ctx, dg, api, "tcp") - - if dg.HasError() { - return nil - } - return &tfTCPMonitorFieldsV0{ - Host: types.StringValue(api.Host), - CheckSend: checkSend, - CheckReceive: checkReceive, - ProxyURL: types.StringValue(api.ProxyUrl), - ProxyUseLocalResolver: types.BoolPointerValue(api.ProxyUseLocalResolver), - tfSSLConfig: sslCfg, - } -} - -func (v *tfICMPMonitorFieldsV0) toTfICMPMonitorFieldsV0(api *kbapi.SyntheticsMonitor) (*tfICMPMonitorFieldsV0, error) { - wait, err := stringToInt64(string(api.Wait)) - if err != nil { - return nil, err - } - return &tfICMPMonitorFieldsV0{ - Host: types.StringValue(api.Host), - Wait: types.Int64Value(wait), - }, nil -} - -func (v *tfBrowserMonitorFieldsV0) toTfBrowserMonitorFieldsV0(api *kbapi.SyntheticsMonitor) (*tfBrowserMonitorFieldsV0, error) { - - var err error - playwrightOptions := v.PlaywrightOptions - if api.PlaywrightOptions != nil { - playwrightOptions, err = toNormalizedValue(api.PlaywrightOptions) - if err != nil { - return nil, err - } - } - - syntheticsArgs := v.SyntheticsArgs - if api.SyntheticsArgs != nil { - syntheticsArgs = StringSliceValue(api.SyntheticsArgs) - } - - inlineScript := v.InlineScript - if api.InlineScript != "" { - inlineScript = types.StringValue(api.InlineScript) - } - - return &tfBrowserMonitorFieldsV0{ - InlineScript: inlineScript, - Screenshots: types.StringValue(api.Screenshots), - SyntheticsArgs: syntheticsArgs, - IgnoreHttpsErrors: types.BoolPointerValue(api.IgnoreHttpsErrors), - PlaywrightOptions: playwrightOptions, - }, nil -} - -func (v *tfHTTPMonitorFieldsV0) toTfHTTPMonitorFieldsV0(ctx context.Context, dg diag.Diagnostics, api *kbapi.SyntheticsMonitor) *tfHTTPMonitorFieldsV0 { - - var err error - proxyHeaders := v.ProxyHeader - if api.ProxyHeaders != nil { - proxyHeaders, err = toNormalizedValue(api.ProxyHeaders) - if err != nil { - dg.AddError("Failed to parse proxy_headers", err.Error()) - return nil - } - } - - username := v.Username - if api.Username != "" { - username = types.StringValue(api.Username) - } - password := v.Password - if api.Password != "" { - password = types.StringValue(api.Password) - } - - maxRedirects, err := stringToInt64(api.MaxRedirects) - if err != nil { - dg.AddError("Failed to parse max_redirects", err.Error()) - return nil - } - - sslCfg, dg := toTFSSLConfig(ctx, dg, api, "http") - if dg.HasError() { - return nil - } - return &tfHTTPMonitorFieldsV0{ - URL: types.StringValue(api.Url), - MaxRedirects: types.Int64Value(maxRedirects), - Mode: types.StringValue(string(api.Mode)), - IPv4: types.BoolPointerValue(api.Ipv4), - IPv6: types.BoolPointerValue(api.Ipv6), - Username: username, - Password: password, - ProxyHeader: proxyHeaders, - ProxyURL: types.StringValue(api.ProxyUrl), - Check: v.Check, - Response: v.Response, - tfSSLConfig: sslCfg, - } -} - -func toTFSSLConfig(ctx context.Context, dg diag.Diagnostics, api *kbapi.SyntheticsMonitor, p string) (tfSSLConfig, diag.Diagnostics) { - sslSupportedProtocols := utils.SliceToListType_String(ctx, api.SslSupportedProtocols, path.Root(p).AtName("ssl_supported_protocols"), &dg) - return tfSSLConfig{ - SslVerificationMode: types.StringValue(api.SslVerificationMode), - SslSupportedProtocols: sslSupportedProtocols, - SslCertificateAuthorities: StringSliceValue(api.SslCertificateAuthorities), - SslCertificate: types.StringValue(api.SslCertificate), - SslKey: types.StringValue(api.SslKey), - SslKeyPassphrase: types.StringValue(api.SslKeyPassphrase), - }, dg -} - -func toTfAlertConfigV0(ctx context.Context, alert *kbapi.MonitorAlertConfig) (basetypes.ObjectValue, diag.Diagnostics) { - - dg := diag.Diagnostics{} - - alertAttributes := monitorAlertConfigSchema().GetType().(attr.TypeWithAttributeTypes).AttributeTypes() - - var emptyAttr = map[string]attr.Type(nil) - - if alert == nil { - return basetypes.NewObjectNull(emptyAttr), dg - } - - tfAlertConfig := tfAlertConfigV0{ - Status: toTfStatusConfigV0(alert.Status), - TLS: toTfStatusConfigV0(alert.Tls), - } - - return types.ObjectValueFrom(ctx, alertAttributes, &tfAlertConfig) -} - -func toTfStatusConfigV0(status *kbapi.SyntheticsStatusConfig) *tfStatusConfigV0 { - if status == nil { - return nil - } - return &tfStatusConfigV0{ - Enabled: types.BoolPointerValue(status.Enabled), - } -} - -func (v *tfModelV0) toKibanaAPIRequest(ctx context.Context) (*kibanaAPIRequest, diag.Diagnostics) { - - fields, dg := v.toMonitorFields(ctx) - if dg.HasError() { - return nil, dg - } - config, dg := v.toSyntheticsMonitorConfig(ctx) - if dg.HasError() { - return nil, dg - } - return &kibanaAPIRequest{ - fields: fields, - config: *config, - }, dg -} - -func (v *tfModelV0) toMonitorFields(ctx context.Context) (kbapi.MonitorFields, diag.Diagnostics) { - dg := diag.Diagnostics{} - - if v.HTTP != nil { - return v.toHttpMonitorFields(ctx) - } else if v.TCP != nil { - return v.toTCPMonitorFields(ctx) - } else if v.ICMP != nil { - return v.toICMPMonitorFields(), dg - } else if v.Browser != nil { - return v.toBrowserMonitorFields() - } - - dg.AddError("Unsupported monitor type config", "one of http,tcp monitor fields is required") - return nil, dg -} - -func toTFAlertConfig(ctx context.Context, v basetypes.ObjectValue) *kbapi.MonitorAlertConfig { - var alert *kbapi.MonitorAlertConfig - if !v.IsNull() && !v.IsUnknown() { - tfAlert := tfAlertConfigV0{} - tfsdk.ValueAs(ctx, v, &tfAlert) - alert = tfAlert.toTfAlertConfigV0() - } - return alert -} - -func (v *tfModelV0) toSyntheticsMonitorConfig(ctx context.Context) (*kbapi.SyntheticsMonitorConfig, diag.Diagnostics) { - locations := Map[types.String, kbapi.MonitorLocation](v.Locations, func(s types.String) kbapi.MonitorLocation { return kbapi.MonitorLocation(s.ValueString()) }) - params, dg := toJsonObject(v.Params) - if dg.HasError() { - return nil, dg - } - - return &kbapi.SyntheticsMonitorConfig{ - Name: v.Name.ValueString(), - Schedule: kbapi.MonitorSchedule(v.Schedule.ValueInt64()), - Locations: locations, - PrivateLocations: ValueStringSlice(v.PrivateLocations), - Enabled: v.Enabled.ValueBoolPointer(), - Tags: ValueStringSlice(v.Tags), - Labels: ValueStringMap(v.Labels), - Alert: toTFAlertConfig(ctx, v.Alert), - APMServiceName: v.APMServiceName.ValueString(), - TimeoutSeconds: int(v.TimeoutSeconds.ValueInt64()), - Namespace: v.Namespace.ValueString(), - Params: params, - RetestOnFailure: v.RetestOnFailure.ValueBoolPointer(), - }, diag.Diagnostics{} //dg -} - -func tfInt64ToString(v types.Int64) string { - res := "" - if !v.IsUnknown() && !v.IsNull() { // handle omitempty case - return strconv.FormatInt(v.ValueInt64(), 10) - } - return res -} - -func toSSLConfig(ctx context.Context, dg diag.Diagnostics, v tfSSLConfig, p string) (*kbapi.SSLConfig, diag.Diagnostics) { - - var ssl *kbapi.SSLConfig - if !v.SslSupportedProtocols.IsNull() && !v.SslSupportedProtocols.IsUnknown() { - sslSupportedProtocols := utils.ListTypeToSlice_String(ctx, v.SslSupportedProtocols, path.Root(p).AtName("ssl_supported_protocols"), &dg) - if dg.HasError() { - return nil, dg - } - ssl = &kbapi.SSLConfig{} - ssl.SupportedProtocols = sslSupportedProtocols - } - - if !v.SslVerificationMode.IsNull() && !v.SslVerificationMode.IsUnknown() { - if ssl == nil { - ssl = &kbapi.SSLConfig{} - } - ssl.VerificationMode = v.SslVerificationMode.ValueString() - } - - certAuths := ValueStringSlice(v.SslCertificateAuthorities) - if len(certAuths) > 0 { - if ssl == nil { - ssl = &kbapi.SSLConfig{} - } - ssl.CertificateAuthorities = certAuths - } - - if !v.SslCertificate.IsUnknown() && !v.SslCertificate.IsNull() { - if ssl == nil { - ssl = &kbapi.SSLConfig{} - } - ssl.Certificate = v.SslCertificate.ValueString() - } - - if !v.SslKey.IsUnknown() && !v.SslKey.IsNull() { - if ssl == nil { - ssl = &kbapi.SSLConfig{} - } - ssl.Key = v.SslKey.ValueString() - } - - if !v.SslKeyPassphrase.IsUnknown() && !v.SslKeyPassphrase.IsNull() { - if ssl == nil { - ssl = &kbapi.SSLConfig{} - } - ssl.KeyPassphrase = v.SslKeyPassphrase.ValueString() - } - return ssl, dg -} - -func (v *tfModelV0) toHttpMonitorFields(ctx context.Context) (kbapi.MonitorFields, diag.Diagnostics) { - http := v.HTTP - proxyHeaders, dg := toJsonObject(http.ProxyHeader) - if dg.HasError() { - return nil, dg - } - response, dg := toJsonObject(http.Response) - if dg.HasError() { - return nil, dg - } - check, dg := toJsonObject(http.Check) - if dg.HasError() { - return nil, dg - } - - ssl, dg := toSSLConfig(ctx, dg, http.tfSSLConfig, "http") - - maxRedirects := tfInt64ToString(http.MaxRedirects) - return kbapi.HTTPMonitorFields{ - Url: http.URL.ValueString(), - Ssl: ssl, - MaxRedirects: maxRedirects, - Mode: kbapi.HttpMonitorMode(http.Mode.ValueString()), - Ipv4: http.IPv4.ValueBoolPointer(), - Ipv6: http.IPv6.ValueBoolPointer(), - Username: http.Username.ValueString(), - Password: http.Password.ValueString(), - ProxyHeader: proxyHeaders, - ProxyUrl: http.ProxyURL.ValueString(), - Response: response, - Check: check, - }, dg -} - -func (v *tfModelV0) toTCPMonitorFields(ctx context.Context) (kbapi.MonitorFields, diag.Diagnostics) { - - tcp := v.TCP - - dg := diag.Diagnostics{} - ssl, dg := toSSLConfig(ctx, dg, tcp.tfSSLConfig, "tcp") - - return kbapi.TCPMonitorFields{ - Host: tcp.Host.ValueString(), - CheckSend: tcp.CheckSend.ValueString(), - CheckReceive: tcp.CheckReceive.ValueString(), - ProxyUrl: tcp.ProxyURL.ValueString(), - ProxyUseLocalResolver: tcp.ProxyUseLocalResolver.ValueBoolPointer(), - Ssl: ssl, - }, dg -} - -func (v *tfModelV0) toICMPMonitorFields() kbapi.MonitorFields { - return kbapi.ICMPMonitorFields{ - Host: v.ICMP.Host.ValueString(), - Wait: tfInt64ToString(v.ICMP.Wait), - } -} - -func (v *tfModelV0) toBrowserMonitorFields() (kbapi.MonitorFields, diag.Diagnostics) { - playwrightOptions, dg := toJsonObject(v.Browser.PlaywrightOptions) - if dg.HasError() { - return nil, dg - } - return kbapi.BrowserMonitorFields{ - InlineScript: v.Browser.InlineScript.ValueString(), - Screenshots: kbapi.ScreenshotOption(v.Browser.Screenshots.ValueString()), - SyntheticsArgs: ValueStringSlice(v.Browser.SyntheticsArgs), - IgnoreHttpsErrors: v.Browser.IgnoreHttpsErrors.ValueBoolPointer(), - PlaywrightOptions: playwrightOptions, - }, diag.Diagnostics{} //dg -} - -func Map[T, U any](ts []T, f func(T) U) []U { - var us []U - for _, v := range ts { - us = append(us, f(v)) - } - return us -} - -func (v tfAlertConfigV0) toTfAlertConfigV0() *kbapi.MonitorAlertConfig { - var status *kbapi.SyntheticsStatusConfig - if v.Status != nil { - status = v.Status.toTfStatusConfigV0() - } - var tls *kbapi.SyntheticsStatusConfig - if v.TLS != nil { - tls = v.TLS.toTfStatusConfigV0() - } - return &kbapi.MonitorAlertConfig{ - Status: status, - Tls: tls, - } -} - -func (v tfStatusConfigV0) toTfStatusConfigV0() *kbapi.SyntheticsStatusConfig { - return &kbapi.SyntheticsStatusConfig{ - Enabled: v.Enabled.ValueBoolPointer(), - } -} - -func (v tfModelV0) enforceVersionConstraints(ctx context.Context, client *clients.ApiClient) diag.Diagnostics { - if utils.IsKnown(v.Labels) { - isSupported, sdkDiags := client.EnforceMinVersion(ctx, MinLabelsVersion) - diags := diagutil.FrameworkDiagsFromSDK(sdkDiags) - if diags.HasError() { - return diags - } - - if !isSupported { - diags.AddAttributeError( - path.Root("labels"), - "Unsupported version for `labels` attribute", - fmt.Sprintf("The `labels` attribute requires server version %s or higher. Either remove the `labels` attribute or upgrade your Elastic Stack installation.", MinLabelsVersion.String()), - ) - return diags - } - } - - return nil -} diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go index b5c681d33..47a51ea2c 100644 --- a/provider/plugin_framework.go +++ b/provider/plugin_framework.go @@ -28,7 +28,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/kibana/maintenance_window" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/security_detection_rule" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/spaces" - "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics/monitor" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics/parameter" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics/private_location" "github.com/elastic/terraform-provider-elasticstack/internal/schema" @@ -107,7 +107,7 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { func() resource.Resource { return ¶meter.Resource{} }, func() resource.Resource { return &private_location.Resource{} }, func() resource.Resource { return &index.Resource{} }, - func() resource.Resource { return &synthetics.Resource{} }, + monitor.NewResource, func() resource.Resource { return &api_key.Resource{} }, func() resource.Resource { return &data_stream_lifecycle.Resource{} }, func() resource.Resource { return &connectors.Resource{} },