diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2687fb064..088f8d73b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -60,10 +60,12 @@ jobs:
image: docker.elastic.co/elasticsearch/elasticsearch:${{ matrix.version }}
env:
discovery.type: single-node
- xpack.license.self_generated.type: trial
xpack.security.enabled: true
xpack.security.authc.api_key.enabled: true
+ xpack.security.authc.token.enabled: true
+ xpack.security.http.ssl.enabled: false
xpack.watcher.enabled: true
+ xpack.license.self_generated.type: trial
repositories.url.allowed_urls: https://example.com/*
path.repo: /tmp
ELASTIC_PASSWORD: ${{ env.ELASTIC_PASSWORD }}
@@ -78,9 +80,10 @@ jobs:
ELASTICSEARCH_USERNAME: ${{ env.KIBANA_SYSTEM_USERNAME }}
ELASTICSEARCH_PASSWORD: ${{ env.KIBANA_SYSTEM_PASSWORD }}
XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY: a7a6311933d3503b89bc2dbc36572c33a6c10925682e591bffcab6911c06786d
- xpack.fleet.enabled: true
+# LOGGING_ROOT_LEVEL: debug
ports:
- 5601:5601
+ options: --health-cmd="curl http://localhost:5601/api/status" --health-interval=10s --health-timeout=5s --health-retries=10
timeout-minutes: 15
strategy:
@@ -135,6 +138,12 @@ jobs:
ELASTICSEARCH_USERNAME: "elastic"
ELASTICSEARCH_PASSWORD: ${{ env.ELASTIC_PASSWORD }}
+ - id: force-install-synthetics
+ name: Force install synthetics
+ if: matrix.version == '8.14.3' || matrix.version == '8.15.0'
+ run: |-
+ for i in {1..5}; do curl -s -H "Authorization: ApiKey ${{ steps.get-api-key.outputs.apikey }}" --header "Content-Type: application/json" --header "kbn-xsrf: true" --request POST --data '{ "force": true }' http://localhost:5601/api/fleet/epm/packages/synthetics/1.2.2 && break || sleep 15; done
+
- name: TF acceptance tests
timeout-minutes: 10
run: make testacc
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 055fc6d3d..1f2b99897 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,5 @@
## [Unreleased]
+- Add support for Kibana synthetics http and tcp monitors ([#699](https://github.com/elastic/terraform-provider-elasticstack/pull/699))
- Add `elasticstack_kibana_spaces` data source ([#682](https://github.com/elastic/terraform-provider-elasticstack/pull/682))
diff --git a/Makefile b/Makefile
index a139c928a..32ca0af18 100644
--- a/Makefile
+++ b/Makefile
@@ -132,7 +132,7 @@ docker-kibana: docker-network docker-elasticsearch set-kibana-password ## Start
-e ELASTICSEARCH_USERNAME=$(KIBANA_SYSTEM_USERNAME) \
-e ELASTICSEARCH_PASSWORD=$(KIBANA_SYSTEM_PASSWORD) \
-e XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY=a7a6311933d3503b89bc2dbc36572c33a6c10925682e591bffcab6911c06786d \
- -e "logging.root.level=debug" \
+ -e LOGGING_ROOT_LEVEL=debug \
--name $(KIBANA_NAME) \
--network $(ELASTICSEARCH_NETWORK) \
docker.elastic.co/kibana/kibana:$(STACK_VERSION); \
@@ -157,7 +157,7 @@ docker-kibana-with-tls: docker-network docker-elasticsearch set-kibana-password
-e SERVER_SSL_CERTIFICATE=/certs/localhost+1.pem \
-e SERVER_SSL_KEY=/certs/localhost+1-key.pem \
-e SERVER_SSL_ENABLED=true \
- -e "logging.root.level=debug" \
+ -e LOGGING_ROOT_LEVEL=debug \
--name $(KIBANA_NAME) \
--network $(ELASTICSEARCH_NETWORK) \
docker.elastic.co/kibana/kibana:$(STACK_VERSION); \
diff --git a/docs/resources/kibana_synthetics_monitor.md b/docs/resources/kibana_synthetics_monitor.md
new file mode 100644
index 000000000..37ef5de61
--- /dev/null
+++ b/docs/resources/kibana_synthetics_monitor.md
@@ -0,0 +1,160 @@
+---
+subcategory: "Kibana"
+layout: ""
+page_title: "Elasticstack: elasticstack_kibana_synthetics_monitor Resource"
+description: |-
+ Creates or updates a Kibana synthetics monitor.
+---
+
+# Resource: elasticstack_kibana_synthetics_monitor
+
+Creates or updates a Kibana synthetics monitor.
+See [API docs](https://www.elastic.co/guide/en/kibana/current/add-monitor-api.html)
+
+## Supported monitor types
+ * `http`
+ * `tcp`
+
+**NOTE:** Due-to nature of partial update API, reset values to defaults is not supported.
+In case you would like to reset an optional monitor value, please set it explicitly or delete and create new monitor.
+
+
+## Example Usage
+
+```terraform
+provider "elasticstack" {
+ kibana {}
+}
+
+resource "elasticstack_kibana_synthetics_monitor" "my_monitor" {
+ name = "Example http monitor"
+ space_id = "default"
+ schedule = 10
+ locations = ["us_west"]
+ enabled = false
+ tags = ["tag"]
+ alert = {
+ status = {
+ enabled = true
+ }
+ tls = {
+ enabled = false
+ }
+ }
+ service_name = "example apm service"
+ timeout = 30
+ http = {
+ url = "http://localhost:8080"
+ ssl_verification_mode = "full"
+ ssl_supported_protocols = ["TLSv1.2"]
+ max_redirects = "10"
+ mode = "all"
+ ipv4 = true
+ ipv6 = true
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `name` (String) The monitor’s name.
+
+### Optional
+
+- `alert` (Attributes) Alert configuration. Default: `{ status: { enabled: true }, tls: { enabled: true } }`. (see [below for nested schema](#nestedatt--alert))
+- `enabled` (Boolean) Whether the monitor is enabled. Default: `true`
+- `http` (Attributes) HTTP Monitor specific fields (see [below for nested schema](#nestedatt--http))
+- `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.
+- `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.
+- `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.
+- `service_name` (String) The APM service name.
+- `space_id` (String) The namespace field should be lowercase and not contain spaces. The namespace must not include any of the following characters: *, \, /, ?, ", <, >, |, whitespace, ,, #, :, or -. Default: `default`
+- `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`
+
+### Read-Only
+
+- `id` (String) Generated identifier for the monitor
+
+
+### Nested Schema for `alert`
+
+Optional:
+
+- `status` (Attributes) (see [below for nested schema](#nestedatt--alert--status))
+- `tls` (Attributes) (see [below for nested schema](#nestedatt--alert--tls))
+
+
+### Nested Schema for `alert.status`
+
+Optional:
+
+- `enabled` (Boolean)
+
+
+
+### Nested Schema for `alert.tls`
+
+Optional:
+
+- `enabled` (Boolean)
+
+
+
+
+### Nested Schema for `http`
+
+Required:
+
+- `url` (String) URL to monitor.
+
+Optional:
+
+- `check` (String) The check request settings.. Raw JSON object, use `jsonencode` function to represent JSON
+- `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.
+- `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.
+- `response` (String) Controls the indexing of the HTTP response body contents to the `http.response.body.contents` field.. Raw JSON object, use `jsonencode` function to represent JSON
+- `ssl_supported_protocols` (List of String) List of allowed SSL/TLS versions.
+- `ssl_verification_mode` (String) Controls the verification of server certificates.
+- `username` (String) The username for authenticating with the server. The credentials are passed with the request.
+
+
+
+### Nested Schema for `tcp`
+
+Required:
+
+- `host` (String) 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").
+
+Optional:
+
+- `check_receive` (String) The expected answer.
+- `check_send` (String) An optional payload string to send to the remote host.
+- `proxy_url` (String) 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.
+- `proxy_use_local_resolver` (Boolean) 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.
+- `ssl_supported_protocols` (List of String) List of allowed SSL/TLS versions.
+- `ssl_verification_mode` (String) Controls the verification of server certificates.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import elasticstack_kibana_synthetics_monitor.my_monitor /
+```
+
+**NOTE:** Not all monitor fields are supported during the import due-to API limitation.
+Full field support could be implemented after this [kibana issue](https://github.com/elastic/kibana/issues/189906) is resolved.
+
+Currently not supported fields during the import: `params`, `retest_on_failure`, `http.proxy_header`, `http.username`, `http.password`, `http.check`, `http.response`, `tcp.check_send`, `tcp.check_receive`
diff --git a/docs/resources/kibana_synthetics_private_location.md b/docs/resources/kibana_synthetics_private_location.md
index 3ae5272b6..527554a61 100644
--- a/docs/resources/kibana_synthetics_private_location.md
+++ b/docs/resources/kibana_synthetics_private_location.md
@@ -72,5 +72,5 @@ Required:
Import is supported using the following syntax:
```shell
-terraform import elasticstack_kibana_synthetics_private_location.my_location
+terraform import elasticstack_kibana_synthetics_private_location.my_location /
```
\ No newline at end of file
diff --git a/examples/resources/elasticstack_kibana_synthetics_monitor/import.sh b/examples/resources/elasticstack_kibana_synthetics_monitor/import.sh
new file mode 100644
index 000000000..c66f88e8f
--- /dev/null
+++ b/examples/resources/elasticstack_kibana_synthetics_monitor/import.sh
@@ -0,0 +1 @@
+terraform import elasticstack_kibana_synthetics_monitor.my_monitor /
diff --git a/examples/resources/elasticstack_kibana_synthetics_monitor/resource.tf b/examples/resources/elasticstack_kibana_synthetics_monitor/resource.tf
new file mode 100644
index 000000000..b38e70609
--- /dev/null
+++ b/examples/resources/elasticstack_kibana_synthetics_monitor/resource.tf
@@ -0,0 +1,31 @@
+provider "elasticstack" {
+ kibana {}
+}
+
+resource "elasticstack_kibana_synthetics_monitor" "my_monitor" {
+ name = "Example http monitor"
+ space_id = "default"
+ schedule = 10
+ locations = ["us_west"]
+ enabled = false
+ tags = ["tag"]
+ alert = {
+ status = {
+ enabled = true
+ }
+ tls = {
+ enabled = false
+ }
+ }
+ service_name = "example apm service"
+ timeout = 30
+ http = {
+ url = "http://localhost:8080"
+ ssl_verification_mode = "full"
+ ssl_supported_protocols = ["TLSv1.2"]
+ max_redirects = "10"
+ mode = "all"
+ ipv4 = true
+ ipv6 = true
+ }
+}
diff --git a/examples/resources/elasticstack_kibana_synthetics_private_location/import.sh b/examples/resources/elasticstack_kibana_synthetics_private_location/import.sh
index 628bacff4..2a16897dc 100644
--- a/examples/resources/elasticstack_kibana_synthetics_private_location/import.sh
+++ b/examples/resources/elasticstack_kibana_synthetics_private_location/import.sh
@@ -1 +1 @@
-terraform import elasticstack_kibana_synthetics_private_location.my_location
+terraform import elasticstack_kibana_synthetics_private_location.my_location /
diff --git a/go.mod b/go.mod
index 0d472137b..53aba661f 100644
--- a/go.mod
+++ b/go.mod
@@ -49,6 +49,7 @@ require (
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.21.0 // indirect
github.com/hashicorp/terraform-json v0.22.1 // indirect
+ github.com/hashicorp/terraform-plugin-framework-jsontypes v0.1.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
diff --git a/go.sum b/go.sum
index c09e89233..414f5829d 100644
--- a/go.sum
+++ b/go.sum
@@ -90,6 +90,8 @@ github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7
github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
github.com/hashicorp/terraform-plugin-framework v1.10.0 h1:xXhICE2Fns1RYZxEQebwkB2+kXouLC932Li9qelozrc=
github.com/hashicorp/terraform-plugin-framework v1.10.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM=
+github.com/hashicorp/terraform-plugin-framework-jsontypes v0.1.0 h1:b8vZYB/SkXJT4YPbT3trzE6oJ7dPyMy68+9dEDKsJjE=
+github.com/hashicorp/terraform-plugin-framework-jsontypes v0.1.0/go.mod h1:tP9BC3icoXBz72evMS5UTFvi98CiKhPdXF6yLs1wS8A=
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 h1:bxZfGo9DIUoLLtHMElsu+zwqI4IsMZQBRRy4iLzZJ8E=
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0/go.mod h1:wGeI02gEhj9nPANU62F2jCaHjXulejm/X+af4PdZaNo=
github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co=
diff --git a/internal/kibana/synthetics/acc_test.go b/internal/kibana/synthetics/acc_test.go
new file mode 100644
index 000000000..264ba04c4
--- /dev/null
+++ b/internal/kibana/synthetics/acc_test.go
@@ -0,0 +1,389 @@
+package synthetics_test
+
+import (
+ "fmt"
+ sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+ "testing"
+
+ "github.com/elastic/terraform-provider-elasticstack/internal/acctest"
+ "github.com/elastic/terraform-provider-elasticstack/internal/versionutils"
+ "github.com/hashicorp/go-version"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+)
+
+var (
+ minKibanaVersion = version.Must(version.NewVersion("8.14.0"))
+)
+
+const (
+ httpMonitorConfig = `
+
+resource "elasticstack_kibana_synthetics_monitor" "%s" {
+ name = "TestHttpMonitorResource - %s"
+ space_id = "testacc"
+ schedule = 5
+ private_locations = [elasticstack_kibana_synthetics_private_location.%s.label]
+ enabled = true
+ tags = ["a", "b"]
+ alert = {
+ status = {
+ enabled = true
+ }
+ tls = {
+ enabled = true
+ }
+ }
+ service_name = "test apm service"
+ timeout = 30
+ http = {
+ url = "http://localhost:5601"
+ ssl_verification_mode = "full"
+ ssl_supported_protocols = ["TLSv1.0", "TLSv1.1", "TLSv1.2"]
+ max_redirects = 10
+ mode = "any"
+ ipv4 = true
+ ipv6 = false
+ proxy_url = "http://localhost:8080"
+ }
+}
+`
+
+ httpMonitorUpdated = `
+resource "elasticstack_kibana_synthetics_monitor" "%s" {
+ name = "TestHttpMonitorResource Updated - %s"
+ space_id = "testacc"
+ schedule = 10
+ private_locations = [elasticstack_kibana_synthetics_private_location.%s.label]
+ enabled = false
+ tags = ["c", "d", "e"]
+ alert = {
+ status = {
+ enabled = true
+ }
+ tls = {
+ enabled = false
+ }
+ }
+ service_name = "test apm service"
+ timeout = 30
+ http = {
+ url = "http://localhost:8080"
+ ssl_verification_mode = "full"
+ ssl_supported_protocols = ["TLSv1.2"]
+ max_redirects = 10
+ mode = "all"
+ ipv4 = true
+ ipv6 = true
+ proxy_url = "http://localhost"
+ proxy_header = jsonencode({
+ "header-name" = "header-value-updated"
+ })
+ username = "testupdated"
+ password = "testpassword-updated"
+ check = jsonencode({
+ "request": {
+ "method": "POST",
+ "headers": {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ "body": "name=first&email=someemail@someemailprovider.com",
+ },
+ "response": {
+ "status": [200, 201, 301],
+ "body": {
+ "positive": ["foo", "bar"]
+ }
+ }
+ })
+ response = jsonencode({
+ "include_body": "never",
+ "include_body_max_bytes": "1024",
+ })
+ }
+ params = jsonencode({
+ "param-name" = "param-value-updated"
+ })
+ retest_on_failure = false
+}
+
+`
+
+ tcpMonitorConfig = `
+
+resource "elasticstack_kibana_synthetics_monitor" "%s" {
+ name = "TestTcpMonitorResource - %s"
+ space_id = "default"
+ schedule = 5
+ private_locations = [elasticstack_kibana_synthetics_private_location.%s.label]
+ enabled = true
+ tags = ["a", "b"]
+ alert = {
+ status = {
+ enabled = true
+ }
+ tls = {
+ enabled = true
+ }
+ }
+ service_name = "test apm service"
+ timeout = 30
+ tcp = {
+ host = "http://localhost:5601"
+ ssl_verification_mode = "full"
+ ssl_supported_protocols = ["TLSv1.0", "TLSv1.1", "TLSv1.2"]
+ proxy_url = "http://localhost:8080"
+ proxy_use_local_resolver = true
+ }
+}
+`
+
+ tcpMonitorUpdated = `
+resource "elasticstack_kibana_synthetics_monitor" "%s" {
+ name = "TestTcpMonitorResource Updated - %s"
+ space_id = "default"
+ schedule = 10
+ private_locations = [elasticstack_kibana_synthetics_private_location.%s.label]
+ enabled = false
+ tags = ["c", "d", "e"]
+ alert = {
+ status = {
+ enabled = true
+ }
+ tls = {
+ enabled = false
+ }
+ }
+ service_name = "test apm service"
+ timeout = 30
+ tcp = {
+ host = "http://localhost:8080"
+ ssl_verification_mode = "full"
+ ssl_supported_protocols = ["TLSv1.2"]
+ proxy_url = "http://localhost"
+ proxy_use_local_resolver = false
+ check_send = "Hello Updated"
+ check_receive = "World Updated"
+ }
+}
+
+`
+)
+
+func TestSyntheticMonitorHTTPResource(t *testing.T) {
+
+ name := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum)
+ id := "http-monitor"
+ httpMonitorId, config := testMonitorConfig(id, httpMonitorConfig, name)
+ _, configUpdated := testMonitorConfig(id, httpMonitorUpdated, name)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.PreCheck(t) },
+ ProtoV6ProviderFactories: acctest.Providers,
+ Steps: []resource.TestStep{
+ // Create and Read http monitor
+ {
+ SkipFunc: versionutils.CheckIfVersionIsUnsupported(minKibanaVersion),
+ Config: config,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet(httpMonitorId, "id"),
+ resource.TestCheckResourceAttr(httpMonitorId, "name", "TestHttpMonitorResource - "+name),
+ resource.TestCheckResourceAttr(httpMonitorId, "space_id", "testacc"),
+ resource.TestCheckResourceAttr(httpMonitorId, "schedule", "5"),
+ resource.TestCheckResourceAttr(httpMonitorId, "private_locations.#", "1"),
+ resource.TestCheckResourceAttrSet(httpMonitorId, "private_locations.0"),
+ resource.TestCheckResourceAttr(httpMonitorId, "enabled", "true"),
+ resource.TestCheckResourceAttr(httpMonitorId, "tags.#", "2"),
+ resource.TestCheckResourceAttr(httpMonitorId, "tags.0", "a"),
+ resource.TestCheckResourceAttr(httpMonitorId, "tags.1", "b"),
+ resource.TestCheckResourceAttr(httpMonitorId, "alert.status.enabled", "true"),
+ resource.TestCheckResourceAttr(httpMonitorId, "alert.tls.enabled", "true"),
+ resource.TestCheckResourceAttr(httpMonitorId, "service_name", "test apm service"),
+ resource.TestCheckResourceAttr(httpMonitorId, "timeout", "30"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.url", "http://localhost:5601"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.ssl_verification_mode", "full"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.ssl_supported_protocols.#", "3"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.ssl_supported_protocols.0", "TLSv1.0"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.ssl_supported_protocols.1", "TLSv1.1"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.ssl_supported_protocols.2", "TLSv1.2"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.max_redirects", "10"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.mode", "any"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.ipv4", "true"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.ipv6", "false"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.proxy_url", "http://localhost:8080"),
+ ),
+ },
+ // ImportState testing
+ {
+ SkipFunc: versionutils.CheckIfVersionIsUnsupported(minKibanaVersion),
+ ResourceName: httpMonitorId,
+ ImportState: true,
+ ImportStateVerify: true,
+ Config: config,
+ },
+ // Update and Read testing http monitor
+ {
+ SkipFunc: versionutils.CheckIfVersionIsUnsupported(minKibanaVersion),
+ ResourceName: httpMonitorId,
+ Config: configUpdated,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet(httpMonitorId, "id"),
+ resource.TestCheckResourceAttr(httpMonitorId, "name", "TestHttpMonitorResource Updated - "+name),
+ resource.TestCheckResourceAttr(httpMonitorId, "space_id", "testacc"),
+ resource.TestCheckResourceAttr(httpMonitorId, "schedule", "10"),
+ resource.TestCheckResourceAttr(httpMonitorId, "private_locations.#", "1"),
+ resource.TestCheckResourceAttrSet(httpMonitorId, "private_locations.0"),
+ resource.TestCheckResourceAttr(httpMonitorId, "enabled", "false"),
+ resource.TestCheckResourceAttr(httpMonitorId, "tags.#", "3"),
+ resource.TestCheckResourceAttr(httpMonitorId, "tags.0", "c"),
+ resource.TestCheckResourceAttr(httpMonitorId, "tags.1", "d"),
+ resource.TestCheckResourceAttr(httpMonitorId, "tags.2", "e"),
+ resource.TestCheckResourceAttr(httpMonitorId, "alert.status.enabled", "true"),
+ resource.TestCheckResourceAttr(httpMonitorId, "alert.tls.enabled", "false"),
+ resource.TestCheckResourceAttr(httpMonitorId, "service_name", "test apm service"),
+ resource.TestCheckResourceAttr(httpMonitorId, "timeout", "30"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.url", "http://localhost:8080"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.ssl_verification_mode", "full"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.ssl_supported_protocols.#", "1"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.ssl_supported_protocols.0", "TLSv1.2"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.max_redirects", "10"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.mode", "all"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.ipv4", "true"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.ipv6", "true"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.proxy_url", "http://localhost"),
+ resource.TestCheckNoResourceAttr(httpMonitorId, "tcp"),
+ //check for merge attributes
+ resource.TestCheckResourceAttr(httpMonitorId, "http.proxy_header", `{"header-name":"header-value-updated"}`),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.username", "testupdated"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.password", "testpassword-updated"),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.check", `{"request":{"body":"name=first\u0026email=someemail@someemailprovider.com","headers":{"Content-Type":"application/x-www-form-urlencoded"},"method":"POST"},"response":{"body":{"positive":["foo","bar"]},"status":[200,201,301]}}`),
+ resource.TestCheckResourceAttr(httpMonitorId, "http.response", `{"include_body":"never","include_body_max_bytes":"1024"}`),
+ resource.TestCheckResourceAttr(httpMonitorId, "params", `{"param-name":"param-value-updated"}`),
+ resource.TestCheckResourceAttr(httpMonitorId, "retest_on_failure", "false"),
+ ),
+ },
+ // Delete testing automatically occurs in TestCase
+ },
+ })
+}
+
+func TestSyntheticMonitorTCPResource(t *testing.T) {
+
+ name := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum)
+ id := "tcp-monitor"
+ tcpMonitorId, config := testMonitorConfig(id, tcpMonitorConfig, name)
+ _, configUpdated := testMonitorConfig(id, tcpMonitorUpdated, name)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.PreCheck(t) },
+ ProtoV6ProviderFactories: acctest.Providers,
+ Steps: []resource.TestStep{
+
+ // Create and Read tcp monitor
+ {
+ SkipFunc: versionutils.CheckIfVersionIsUnsupported(minKibanaVersion),
+ Config: config,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet(tcpMonitorId, "id"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "name", "TestTcpMonitorResource - "+name),
+ resource.TestCheckResourceAttr(tcpMonitorId, "space_id", "default"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "schedule", "5"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "private_locations.#", "1"),
+ resource.TestCheckResourceAttrSet(tcpMonitorId, "private_locations.0"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "enabled", "true"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tags.#", "2"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tags.0", "a"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tags.1", "b"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "alert.status.enabled", "true"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "alert.tls.enabled", "true"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "service_name", "test apm service"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "timeout", "30"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.host", "http://localhost:5601"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.ssl_verification_mode", "full"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.ssl_supported_protocols.#", "3"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.ssl_supported_protocols.0", "TLSv1.0"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.ssl_supported_protocols.1", "TLSv1.1"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.ssl_supported_protocols.2", "TLSv1.2"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.proxy_url", "http://localhost:8080"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.proxy_use_local_resolver", "true"),
+ ),
+ },
+ // ImportState testing
+ {
+ SkipFunc: versionutils.CheckIfVersionIsUnsupported(minKibanaVersion),
+ ResourceName: tcpMonitorId,
+ ImportState: true,
+ ImportStateVerify: true,
+ Config: config,
+ },
+ // Update and Read tcp monitor
+ {
+ SkipFunc: versionutils.CheckIfVersionIsUnsupported(minKibanaVersion),
+ ResourceName: tcpMonitorId,
+ Config: configUpdated,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet(tcpMonitorId, "id"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "name", "TestTcpMonitorResource Updated - "+name),
+ resource.TestCheckResourceAttr(tcpMonitorId, "space_id", "default"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "schedule", "10"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "private_locations.#", "1"),
+ resource.TestCheckResourceAttrSet(tcpMonitorId, "private_locations.0"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "enabled", "false"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tags.#", "3"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tags.0", "c"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tags.1", "d"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tags.2", "e"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "alert.status.enabled", "true"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "alert.tls.enabled", "false"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "service_name", "test apm service"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "timeout", "30"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.host", "http://localhost:8080"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.ssl_verification_mode", "full"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.ssl_supported_protocols.#", "1"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.ssl_supported_protocols.0", "TLSv1.2"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.proxy_url", "http://localhost"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.proxy_use_local_resolver", "false"),
+ resource.TestCheckNoResourceAttr(tcpMonitorId, "http"),
+ //check for merge attributes
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.check_send", "Hello Updated"),
+ resource.TestCheckResourceAttr(tcpMonitorId, "tcp.check_receive", "World Updated"),
+ ),
+ },
+ // Delete testing automatically occurs in TestCase
+
+ },
+ })
+}
+
+func testMonitorConfig(id, cfg, name string) (string, string) {
+
+ resourceId := "elasticstack_kibana_synthetics_monitor." + id
+ privateLocationId := "pl-" + id
+ agentPolicyId := "apl-" + id
+
+ provider := fmt.Sprintf(`
+provider "elasticstack" {
+ elasticsearch {}
+ kibana {}
+ fleet{}
+}
+
+resource "elasticstack_fleet_agent_policy" "%s" {
+ name = "TestMonitorResource Agent Policy - %s"
+ namespace = "testacc"
+ description = "TestMonitorResource Agent Policy"
+ monitor_logs = true
+ monitor_metrics = true
+ skip_destroy = false
+}
+
+resource "elasticstack_kibana_synthetics_private_location" "%s" {
+ label = "TestMonitorResource-label-%s"
+ space_id = "testacc"
+ agent_policy_id = elasticstack_fleet_agent_policy.%s.policy_id
+}
+`, agentPolicyId, name, privateLocationId, name, agentPolicyId)
+
+ config := fmt.Sprintf(cfg, id, name, privateLocationId)
+
+ return resourceId, provider + config
+}
diff --git a/internal/kibana/synthetics/create.go b/internal/kibana/synthetics/create.go
new file mode 100644
index 000000000..26cc9285f
--- /dev/null
+++ b/internal/kibana/synthetics/create.go
@@ -0,0 +1,47 @@
+package synthetics
+
+import (
+ "context"
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+)
+
+func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
+
+ kibanaClient := GetKibanaClient(r, response.Diagnostics)
+ if kibanaClient == nil {
+ return
+ }
+
+ plan := new(tfModelV0)
+ diags := request.Plan.Get(ctx, plan)
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ input, diags := plan.toKibanaAPIRequest()
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ namespace := plan.SpaceID.ValueString()
+ result, err := kibanaClient.KibanaSynthetics.Monitor.Add(ctx, input.config, input.fields, namespace)
+ if err != nil {
+ response.Diagnostics.AddError(fmt.Sprintf("Failed to create Kibana monitor `%s`, namespace %s", input.config.Name, namespace), err.Error())
+ return
+ }
+
+ plan, err = plan.toModelV0(result)
+ if err != nil {
+ response.Diagnostics.AddError("Failed to convert Kibana monitor API to TF state", err.Error())
+ return
+ }
+
+ diags = response.State.Set(ctx, plan)
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+}
diff --git a/internal/kibana/synthetics/delete.go b/internal/kibana/synthetics/delete.go
new file mode 100644
index 000000000..fc93d57a2
--- /dev/null
+++ b/internal/kibana/synthetics/delete.go
@@ -0,0 +1,37 @@
+package synthetics
+
+import (
+ "context"
+ "fmt"
+ "github.com/disaster37/go-kibana-rest/v8/kbapi"
+ "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)
+ if kibanaClient == nil {
+ return
+ }
+
+ var plan tfModelV0
+ diags := request.State.Get(ctx, &plan)
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ compositeId, dg := GetCompositeId(plan.ID.ValueString())
+ response.Diagnostics.Append(dg...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ namespace := plan.SpaceID.ValueString()
+ _, err := kibanaClient.KibanaSynthetics.Monitor.Delete(ctx, namespace, kbapi.MonitorID(compositeId.ResourceId))
+
+ if err != nil {
+ response.Diagnostics.AddError(fmt.Sprintf("Failed to delete private location `%s`, namespace %s", compositeId, namespace), err.Error())
+ return
+ }
+}
diff --git a/internal/kibana/synthetics/private_location/create.go b/internal/kibana/synthetics/private_location/create.go
index 9f0bf2aa3..7fd4c8a34 100644
--- a/internal/kibana/synthetics/private_location/create.go
+++ b/internal/kibana/synthetics/private_location/create.go
@@ -3,15 +3,13 @@ package private_location
import (
"context"
"fmt"
+ "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics"
"github.com/hashicorp/terraform-plugin-framework/resource"
- "github.com/hashicorp/terraform-plugin-log/tflog"
)
func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
- tflog.Info(ctx, "Create private location")
-
- kibanaClient := r.getKibanaClient(response.Diagnostics)
+ kibanaClient := synthetics.GetKibanaClient(r, response.Diagnostics)
if kibanaClient == nil {
return
}
@@ -23,10 +21,10 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r
return
}
- input := plan.toPrivateLocation()
+ input := plan.toPrivateLocationConfig()
namespace := plan.SpaceID.ValueString()
- result, err := kibanaClient.KibanaSynthetics.PrivateLocation.Create(input.PrivateLocationConfig, namespace)
+ result, err := kibanaClient.KibanaSynthetics.PrivateLocation.Create(ctx, input, namespace)
if err != nil {
response.Diagnostics.AddError(fmt.Sprintf("Failed to create private location `%s`, namespace %s", input.Label, namespace), err.Error())
return
diff --git a/internal/kibana/synthetics/private_location/delete.go b/internal/kibana/synthetics/private_location/delete.go
index 1b7ecb5f1..34cf9a097 100644
--- a/internal/kibana/synthetics/private_location/delete.go
+++ b/internal/kibana/synthetics/private_location/delete.go
@@ -3,15 +3,13 @@ package private_location
import (
"context"
"fmt"
+ "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics"
"github.com/hashicorp/terraform-plugin-framework/resource"
- "github.com/hashicorp/terraform-plugin-log/tflog"
)
func (r *Resource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
- tflog.Info(ctx, "Delete private location")
-
- kibanaClient := r.getKibanaClient(response.Diagnostics)
+ kibanaClient := synthetics.GetKibanaClient(r, response.Diagnostics)
if kibanaClient == nil {
return
}
@@ -23,12 +21,24 @@ func (r *Resource) Delete(ctx context.Context, request resource.DeleteRequest, r
return
}
- id := plan.ID.ValueString()
+ resourceId := plan.ID.ValueString()
namespace := plan.SpaceID.ValueString()
- err := kibanaClient.KibanaSynthetics.PrivateLocation.Delete(id, namespace)
+
+ compositeId, dg := tryReadCompositeId(resourceId)
+ response.Diagnostics.Append(dg...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ if compositeId != nil {
+ resourceId = compositeId.ResourceId
+ namespace = compositeId.ClusterId
+ }
+
+ err := kibanaClient.KibanaSynthetics.PrivateLocation.Delete(ctx, resourceId, namespace)
if err != nil {
- response.Diagnostics.AddError(fmt.Sprintf("Failed to delete private location `%s`, namespace %s", id, namespace), err.Error())
+ response.Diagnostics.AddError(fmt.Sprintf("Failed to delete private location `%s`, namespace %s", resourceId, namespace), err.Error())
return
}
diff --git a/internal/kibana/synthetics/private_location/read.go b/internal/kibana/synthetics/private_location/read.go
index dedd7a8b7..dcd004d2d 100644
--- a/internal/kibana/synthetics/private_location/read.go
+++ b/internal/kibana/synthetics/private_location/read.go
@@ -5,15 +5,13 @@ import (
"errors"
"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"
- "github.com/hashicorp/terraform-plugin-log/tflog"
)
func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
- tflog.Info(ctx, "Read private location")
-
- kibanaClient := r.getKibanaClient(response.Diagnostics)
+ kibanaClient := synthetics.GetKibanaClient(r, response.Diagnostics)
if kibanaClient == nil {
return
}
@@ -25,9 +23,21 @@ func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, respo
return
}
- label := state.Label.ValueString()
+ resourceId := state.ID.ValueString()
namespace := state.SpaceID.ValueString()
- result, err := kibanaClient.KibanaSynthetics.PrivateLocation.Get(label, namespace)
+
+ compositeId, dg := tryReadCompositeId(resourceId)
+ response.Diagnostics.Append(dg...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ if compositeId != nil {
+ resourceId = compositeId.ResourceId
+ namespace = compositeId.ClusterId
+ }
+
+ result, err := kibanaClient.KibanaSynthetics.PrivateLocation.Get(ctx, resourceId, namespace)
if err != nil {
var apiError *kbapi.APIError
if errors.As(err, &apiError) && apiError.Code == 404 {
@@ -35,7 +45,7 @@ func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, respo
return
}
- response.Diagnostics.AddError(fmt.Sprintf("Failed to get private location `%s`, namespace %s", label, namespace), err.Error())
+ response.Diagnostics.AddError(fmt.Sprintf("Failed to get private location `%s`, namespace %s", resourceId, namespace), err.Error())
return
}
diff --git a/internal/kibana/synthetics/private_location/resource.go b/internal/kibana/synthetics/private_location/resource.go
index 0da381cde..c4023e7de 100644
--- a/internal/kibana/synthetics/private_location/resource.go
+++ b/internal/kibana/synthetics/private_location/resource.go
@@ -2,10 +2,8 @@ package private_location
import (
"context"
- "github.com/disaster37/go-kibana-rest/v8"
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
"github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics"
- "github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-log/tflog"
@@ -17,9 +15,15 @@ const resourceName = synthetics.MetadataPrefix + "private_location"
var _ resource.Resource = &Resource{}
var _ resource.ResourceWithConfigure = &Resource{}
var _ resource.ResourceWithImportState = &Resource{}
+var _ synthetics.ESApiClient = &Resource{}
type Resource struct {
client *clients.ApiClient
+ synthetics.ESApiClient
+}
+
+func (r *Resource) GetClient() *clients.ApiClient {
+ return r.client
}
func (r *Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
@@ -27,8 +31,7 @@ func (r *Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *res
}
func (r *Resource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) {
- tflog.Info(ctx, "Import private location")
- resource.ImportStatePassthroughID(ctx, path.Root("label"), request, response)
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response)
}
func (r *Resource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) {
@@ -48,16 +51,3 @@ func (r *Resource) Update(ctx context.Context, _ resource.UpdateRequest, respons
"Synthetics private location could only be replaced. Please, note, that only unused locations could be deleted.",
)
}
-
-func (r *Resource) getKibanaClient(dg diag.Diagnostics) *kibana.Client {
- if !r.resourceReady(&dg) {
- return nil
- }
-
- kibanaClient, err := r.client.GetKibanaClient()
- if err != nil {
- dg.AddError("unable to get kibana client", err.Error())
- return nil
- }
- return kibanaClient
-}
diff --git a/internal/kibana/synthetics/private_location/schema.go b/internal/kibana/synthetics/private_location/schema.go
index 9b7316a5a..6a78942c5 100644
--- a/internal/kibana/synthetics/private_location/schema.go
+++ b/internal/kibana/synthetics/private_location/schema.go
@@ -2,6 +2,7 @@ package private_location
import (
"github.com/disaster37/go-kibana-rest/v8/kbapi"
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients"
"github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -9,6 +10,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
+ "strings"
)
type tfModelV0 struct {
@@ -72,53 +74,41 @@ func privateLocationSchema() schema.Schema {
}
}
-func (r *Resource) resourceReady(dg *diag.Diagnostics) bool {
- if r.client == nil {
- dg.AddError(
- "Unconfigured Client",
- "Expected configured client. Please report this issue to the provider developers.",
- )
-
- return false
- }
- return true
-}
-
-func (m *tfModelV0) toPrivateLocation() kbapi.PrivateLocation {
+func (m *tfModelV0) toPrivateLocationConfig() kbapi.PrivateLocationConfig {
var geoConfig *kbapi.SyntheticGeoConfig
if m.Geo != nil {
geoConfig = m.Geo.ToSyntheticGeoConfig()
}
- var tags []string
- for _, tag := range m.Tags {
- tags = append(tags, tag.ValueString())
- }
- pLoc := kbapi.PrivateLocationConfig{
+ return kbapi.PrivateLocationConfig{
Label: m.Label.ValueString(),
AgentPolicyId: m.AgentPolicyId.ValueString(),
- Tags: tags,
+ Tags: synthetics.ValueStringSlice(m.Tags),
Geo: geoConfig,
}
+}
- return kbapi.PrivateLocation{
- Id: m.ID.ValueString(),
- Namespace: m.SpaceID.ValueString(),
- PrivateLocationConfig: pLoc,
+func tryReadCompositeId(id string) (*clients.CompositeId, diag.Diagnostics) {
+ if strings.Contains(id, "/") {
+ compositeId, diagnostics := synthetics.GetCompositeId(id)
+ return compositeId, diagnostics
}
+ return nil, diag.Diagnostics{}
}
func toModelV0(pLoc kbapi.PrivateLocation) tfModelV0 {
- var tags []types.String
- for _, tag := range pLoc.Tags {
- tags = append(tags, types.StringValue(tag))
+
+ resourceID := clients.CompositeId{
+ ClusterId: pLoc.Namespace,
+ ResourceId: pLoc.Id,
}
+
return tfModelV0{
- ID: types.StringValue(pLoc.Id),
+ ID: types.StringValue(resourceID.String()),
Label: types.StringValue(pLoc.Label),
SpaceID: types.StringValue(pLoc.Namespace),
AgentPolicyId: types.StringValue(pLoc.AgentPolicyId),
- Tags: tags,
+ Tags: synthetics.StringSliceValue(pLoc.Tags),
Geo: synthetics.FromSyntheticGeoConfig(pLoc.Geo),
}
}
diff --git a/internal/kibana/synthetics/private_location/schema_test.go b/internal/kibana/synthetics/private_location/schema_test.go
index c22c6a846..60ba3b047 100644
--- a/internal/kibana/synthetics/private_location/schema_test.go
+++ b/internal/kibana/synthetics/private_location/schema_test.go
@@ -1,6 +1,7 @@
package private_location
import (
+ "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics"
"testing"
"github.com/disaster37/go-kibana-rest/v8/kbapi"
@@ -71,7 +72,14 @@ func Test_roundtrip(t *testing.T) {
PrivateLocationConfig: plc,
}
modelV0 := toModelV0(input)
- actual := modelV0.toPrivateLocation()
+
+ compositeId, _ := synthetics.GetCompositeId(modelV0.ID.ValueString())
+
+ actual := kbapi.PrivateLocation{
+ Id: compositeId.ResourceId,
+ Namespace: modelV0.SpaceID.ValueString(),
+ PrivateLocationConfig: modelV0.toPrivateLocationConfig(),
+ }
assert.Equal(t, input, actual)
})
}
diff --git a/internal/kibana/synthetics/read.go b/internal/kibana/synthetics/read.go
new file mode 100644
index 000000000..d38799baa
--- /dev/null
+++ b/internal/kibana/synthetics/read.go
@@ -0,0 +1,57 @@
+package synthetics
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/disaster37/go-kibana-rest/v8/kbapi"
+ "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)
+ if kibanaClient == nil {
+ return
+ }
+
+ state := new(tfModelV0)
+ diags := request.State.Get(ctx, state)
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ compositeId, dg := GetCompositeId(state.ID.ValueString())
+ response.Diagnostics.Append(dg...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ namespace := compositeId.ClusterId
+ monitorId := kbapi.MonitorID(compositeId.ResourceId)
+ result, err := kibanaClient.KibanaSynthetics.Monitor.Get(ctx, monitorId, namespace)
+ if err != nil {
+ var apiError *kbapi.APIError
+ if errors.As(err, &apiError) && apiError.Code == 404 {
+ response.State.RemoveResource(ctx)
+ return
+ }
+
+ response.Diagnostics.AddError(fmt.Sprintf("Failed to get monitor `%s`, namespace %s", monitorId, namespace), err.Error())
+ return
+ }
+
+ state, err = state.toModelV0(result)
+ if err != nil {
+ response.Diagnostics.AddError("Failed to convert Kibana monitor API to TF state", err.Error())
+ return
+ }
+
+ // Set refreshed state
+ diags = response.State.Set(ctx, state)
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+}
diff --git a/internal/kibana/synthetics/resource.go b/internal/kibana/synthetics/resource.go
new file mode 100644
index 000000000..1925ff521
--- /dev/null
+++ b/internal/kibana/synthetics/resource.go
@@ -0,0 +1,84 @@
+package synthetics
+
+import (
+ "context"
+ "github.com/disaster37/go-kibana-rest/v8"
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients"
+ "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"
+
+// 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
+}
+
+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(
+ path.MatchRoot("http"),
+ path.MatchRoot("tcp"),
+ // other monitor config types: icmp, browser
+ ),
+ resourcevalidator.AtLeastOneOf(
+ path.MatchRoot("locations"),
+ path.MatchRoot("private_locations"),
+ ),
+ }
+}
+
+func (r *Resource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response)
+}
+
+func (r *Resource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) {
+ client, diags := clients.ConvertProviderData(request.ProviderData)
+ response.Diagnostics.Append(diags...)
+ r.client = client
+}
+
+func (r *Resource) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) {
+ response.TypeName = request.ProviderTypeName + resourceName
+}
+
+func (r *Resource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) {
+ response.Schema = monitorConfigSchema()
+
+}
diff --git a/internal/kibana/synthetics/schema.go b/internal/kibana/synthetics/schema.go
index 91f29aaf6..2f8267a4b 100644
--- a/internal/kibana/synthetics/schema.go
+++ b/internal/kibana/synthetics/schema.go
@@ -1,15 +1,307 @@
package synthetics
import (
+ "encoding/json"
+ "fmt"
"github.com/disaster37/go-kibana-rest/v8/kbapi"
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients"
+ "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/diag"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "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/types"
+ "strconv"
)
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 tfHTTPMonitorFieldsV0 struct {
+ URL types.String `tfsdk:"url"`
+ SslVerificationMode types.String `tfsdk:"ssl_verification_mode"`
+ SslSupportedProtocols []types.String `tfsdk:"ssl_supported_protocols"`
+ 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"`
+}
+
+type tfTCPMonitorFieldsV0 struct {
+ Host types.String `tfsdk:"host"`
+ SslVerificationMode types.String `tfsdk:"ssl_verification_mode"`
+ SslSupportedProtocols []types.String `tfsdk:"ssl_supported_protocols"`
+ 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"`
+}
+
+type tfModelV0 struct {
+ ID types.String `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ SpaceID types.String `tfsdk:"space_id"`
+ 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"`
+ Alert *tfAlertConfigV0 `tfsdk:"alert"`
+ APMServiceName types.String `tfsdk:"service_name"`
+ TimeoutSeconds types.Int64 `tfsdk:"timeout"`
+ HTTP *tfHTTPMonitorFieldsV0 `tfsdk:"http"`
+ TCP *tfTCPMonitorFieldsV0 `tfsdk:"tcp"`
+ Params jsontypes.Normalized `tfsdk:"params"`
+ RetestOnFailure types.Bool `tfsdk:"retest_on_failure"`
+}
+
+func GetCompositeId(id string) (*clients.CompositeId, diag.Diagnostics) {
+ compositeID, sdkDiag := clients.CompositeIdFromStr(id)
+ dg := diag.Diagnostics{}
+ if sdkDiag.HasError() {
+ dg.AddError(fmt.Sprintf("Failed to parse monitor ID %s", id), fmt.Sprintf("Resource ID must have following format: /. Current value: %s", id))
+ return nil, dg
+ }
+ return compositeID, dg
+}
+
+func monitorConfigSchema() schema.Schema {
+ return schema.Schema{
+ MarkdownDescription: "Synthetics monitor config, see https://www.elastic.co/guide/en/kibana/current/add-monitor-api.html for more details. The monitor must have one of the following: http, tcp, icmp or browser.",
+ 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: "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.RequiresReplace(),
+ },
+ },
+ "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),
+ },
+ },
+ "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`",
+ },
+ "tags": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ MarkdownDescription: "An array of tags.",
+ },
+ "alert": monitorAlertConfigSchema(),
+ "service_name": schema.StringAttribute{
+ Optional: true,
+ MarkdownDescription: "The APM service name.",
+ },
+ "timeout": schema.Int64Attribute{
+ Optional: true,
+ MarkdownDescription: "The monitor timeout in seconds, monitor will fail if it doesn’t complete within this time. Default: `16`",
+ },
+ "params": jsonObjectSchema("Monitor parameters"),
+ "http": httpMonitorFieldsSchema(),
+ "tcp": tcpMonitorFieldsSchema(),
+ "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 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,
+ },
+ },
+ }
+}
+
+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(),
+ },
+ }
+}
+
+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. ",
+ },
+ "ssl_supported_protocols": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ MarkdownDescription: "List of allowed SSL/TLS versions.",
+ },
+ "max_redirects": schema.Int64Attribute{
+ Optional: true,
+ MarkdownDescription: "The maximum number of redirects to follow. Default: `0`",
+ },
+ "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"),
+ },
+ },
+ "ipv4": schema.BoolAttribute{
+ Optional: true,
+ MarkdownDescription: "Whether to ping using the ipv4 protocol.",
+ },
+ "ipv6": schema.BoolAttribute{
+ Optional: true,
+ MarkdownDescription: "Whether to ping using the ipv6 protocol.",
+ },
+ "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.",
+ },
+ "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. ",
+ },
+ "ssl_supported_protocols": schema.ListAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ MarkdownDescription: "List of allowed SSL/TLS versions.",
+ },
+ "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.",
+ },
+ "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.",
+ },
+ },
+ }
+}
+
func GeoConfigSchema() schema.Attribute {
return schema.SingleNestedAttribute{
Optional: true,
@@ -50,3 +342,342 @@ func FromSyntheticGeoConfig(v *kbapi.SyntheticGeoConfig) *TFGeoConfigV0 {
Lon: types.Float64Value(v.Lon),
}
}
+
+func ValueStringSlice(v []types.String) []string {
+ var res []string
+ for _, s := range v {
+ res = append(res, s.ValueString())
+ }
+ return res
+}
+
+func StringSliceValue(v []string) []types.String {
+ var res []types.String
+ for _, s := range v {
+ res = append(res, types.StringValue(s))
+ }
+ return res
+}
+
+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(api *kbapi.SyntheticsMonitor) (*tfModelV0, error) {
+ var schedule int64
+ var err error
+ if api.Schedule != nil {
+ schedule, err = stringToInt64(api.Schedule.Number)
+ if err != nil {
+ return nil, err
+ }
+ }
+ var locLabels []string
+ var privateLocLabels []string
+ for _, l := range api.Locations {
+ if l.IsServiceManaged {
+ locLabels = append(locLabels, l.Label)
+ } else {
+ privateLocLabels = append(privateLocLabels, l.Label)
+ }
+ }
+
+ timeout, err := stringToInt64(string(api.Timeout))
+ if err != nil {
+ return nil, err
+ }
+
+ var http *tfHTTPMonitorFieldsV0
+ var tcp *tfTCPMonitorFieldsV0
+
+ switch mType := api.Type; mType {
+ case kbapi.Http:
+ http = &tfHTTPMonitorFieldsV0{}
+ if v.HTTP != nil {
+ http = v.HTTP
+ }
+ http, err = http.toTfHTTPMonitorFieldsV0(api)
+ case kbapi.Tcp:
+ tcp = &tfTCPMonitorFieldsV0{}
+ if v.TCP != nil {
+ tcp = v.TCP
+ }
+ tcp, err = tcp.toTfTCPMonitorFieldsV0(api)
+ default:
+ err = fmt.Errorf("unsupported monitor type: %s", mType)
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ params := v.Params
+ if api.Params != nil {
+ params, err = toNormalizedValue(api.Params)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ resourceID := clients.CompositeId{
+ ClusterId: api.Namespace,
+ ResourceId: string(api.Id),
+ }
+
+ return &tfModelV0{
+ ID: types.StringValue(resourceID.String()),
+ Name: types.StringValue(api.Name),
+ SpaceID: types.StringValue(api.Namespace),
+ Schedule: types.Int64Value(schedule),
+ Locations: StringSliceValue(locLabels),
+ PrivateLocations: StringSliceValue(privateLocLabels),
+ Enabled: types.BoolPointerValue(api.Enabled),
+ Tags: StringSliceValue(api.Tags),
+ Alert: toTfAlertConfigV0(api.Alert),
+ APMServiceName: types.StringValue(api.APMServiceName),
+ TimeoutSeconds: types.Int64Value(timeout),
+ Params: params,
+ HTTP: http,
+ TCP: tcp,
+ RetestOnFailure: v.RetestOnFailure,
+ }, nil
+}
+
+func (v *tfTCPMonitorFieldsV0) toTfTCPMonitorFieldsV0(api *kbapi.SyntheticsMonitor) (*tfTCPMonitorFieldsV0, error) {
+ checkSend := v.CheckSend
+ if api.CheckSend != "" {
+ checkSend = types.StringValue(api.CheckSend)
+ }
+ checkReceive := v.CheckReceive
+ if api.CheckReceive != "" {
+ checkReceive = types.StringValue(api.CheckReceive)
+ }
+ return &tfTCPMonitorFieldsV0{
+ Host: types.StringValue(api.Host),
+ SslVerificationMode: types.StringValue(api.SslVerificationMode),
+ SslSupportedProtocols: StringSliceValue(api.SslSupportedProtocols),
+ CheckSend: checkSend,
+ CheckReceive: checkReceive,
+ ProxyURL: types.StringValue(api.ProxyUrl),
+ ProxyUseLocalResolver: types.BoolPointerValue(api.ProxyUseLocalResolver),
+ }, nil
+}
+
+func (v *tfHTTPMonitorFieldsV0) toTfHTTPMonitorFieldsV0(api *kbapi.SyntheticsMonitor) (*tfHTTPMonitorFieldsV0, error) {
+
+ var err error
+ proxyHeaders := v.ProxyHeader
+ if api.ProxyHeaders != nil {
+ proxyHeaders, err = toNormalizedValue(api.ProxyHeaders)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ 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 {
+ return nil, err
+ }
+
+ return &tfHTTPMonitorFieldsV0{
+ URL: types.StringValue(api.Url),
+ SslVerificationMode: types.StringValue(api.SslVerificationMode),
+ SslSupportedProtocols: StringSliceValue(api.SslSupportedProtocols),
+ 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,
+ }, nil
+}
+
+func toTfAlertConfigV0(alert *kbapi.MonitorAlertConfig) *tfAlertConfigV0 {
+ if alert == nil {
+ return nil
+ }
+ return &tfAlertConfigV0{
+ Status: toTfStatusConfigV0(alert.Status),
+ TLS: toTfStatusConfigV0(alert.Tls),
+ }
+}
+
+func toTfStatusConfigV0(status *kbapi.SyntheticsStatusConfig) *tfStatusConfigV0 {
+ if status == nil {
+ return nil
+ }
+ return &tfStatusConfigV0{
+ Enabled: types.BoolPointerValue(status.Enabled),
+ }
+}
+
+func (v *tfModelV0) toKibanaAPIRequest() (*kibanaAPIRequest, diag.Diagnostics) {
+
+ fields, dg := v.toMonitorFields()
+ if dg.HasError() {
+ return nil, dg
+ }
+ config, dg := v.toSyntheticsMonitorConfig()
+ if dg.HasError() {
+ return nil, dg
+ }
+ return &kibanaAPIRequest{
+ fields: fields,
+ config: *config,
+ }, dg
+}
+
+func (v *tfModelV0) toMonitorFields() (kbapi.MonitorFields, diag.Diagnostics) {
+ var dg diag.Diagnostics
+
+ if v.HTTP != nil {
+ return v.toHttpMonitorFields()
+ } else if v.TCP != nil {
+ return v.toTCPMonitorFields(), dg
+ }
+
+ dg.AddError("Unsupported monitor type config", "one of http,tcp monitor fields is required")
+ return nil, dg
+}
+
+func (v *tfModelV0) toSyntheticsMonitorConfig() (*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
+ }
+
+ var alert *kbapi.MonitorAlertConfig
+ if v.Alert != nil {
+ alert = v.Alert.toTfAlertConfigV0()
+ }
+
+ 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),
+ Alert: alert,
+ APMServiceName: v.APMServiceName.ValueString(),
+ TimeoutSeconds: int(v.TimeoutSeconds.ValueInt64()),
+ Namespace: v.SpaceID.ValueString(),
+ Params: params,
+ RetestOnFailure: v.RetestOnFailure.ValueBoolPointer(),
+ }, diag.Diagnostics{} //dg
+}
+
+func (v *tfModelV0) toHttpMonitorFields() (kbapi.MonitorFields, diag.Diagnostics) {
+ proxyHeaders, dg := toJsonObject(v.HTTP.ProxyHeader)
+ if dg.HasError() {
+ return nil, dg
+ }
+ response, dg := toJsonObject(v.HTTP.Response)
+ if dg.HasError() {
+ return nil, dg
+ }
+ check, dg := toJsonObject(v.HTTP.Check)
+ if dg.HasError() {
+ return nil, dg
+ }
+ maxRedirects := ""
+ if !v.HTTP.MaxRedirects.IsUnknown() && !v.HTTP.MaxRedirects.IsNull() { // handle omitempty case
+ maxRedirects = strconv.FormatInt(v.HTTP.MaxRedirects.ValueInt64(), 10)
+
+ }
+ return kbapi.HTTPMonitorFields{
+ Url: v.HTTP.URL.ValueString(),
+ SslVerificationMode: v.HTTP.SslVerificationMode.ValueString(),
+ SslSupportedProtocols: ValueStringSlice(v.HTTP.SslSupportedProtocols),
+ MaxRedirects: maxRedirects,
+ Mode: kbapi.HttpMonitorMode(v.HTTP.Mode.ValueString()),
+ Ipv4: v.HTTP.IPv4.ValueBoolPointer(),
+ Ipv6: v.HTTP.IPv6.ValueBoolPointer(),
+ Username: v.HTTP.Username.ValueString(),
+ Password: v.HTTP.Password.ValueString(),
+ ProxyHeader: proxyHeaders,
+ ProxyUrl: v.HTTP.ProxyURL.ValueString(),
+ Response: response,
+ Check: check,
+ }, diag.Diagnostics{} //dg
+}
+
+func (v *tfModelV0) toTCPMonitorFields() kbapi.MonitorFields {
+ return kbapi.TCPMonitorFields{
+ Host: v.TCP.Host.ValueString(),
+ SslVerificationMode: v.TCP.SslVerificationMode.ValueString(),
+ SslSupportedProtocols: ValueStringSlice(v.TCP.SslSupportedProtocols),
+ CheckSend: v.TCP.CheckSend.ValueString(),
+ CheckReceive: v.TCP.CheckReceive.ValueString(),
+ ProxyUrl: v.TCP.ProxyURL.ValueString(),
+ ProxyUseLocalResolver: v.TCP.ProxyUseLocalResolver.ValueBoolPointer(),
+ }
+}
+
+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(),
+ }
+}
diff --git a/internal/kibana/synthetics/schema_test.go b/internal/kibana/synthetics/schema_test.go
new file mode 100644
index 000000000..a6a160e19
--- /dev/null
+++ b/internal/kibana/synthetics/schema_test.go
@@ -0,0 +1,446 @@
+package synthetics
+
+import (
+ "encoding/json"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "testing"
+
+ "github.com/disaster37/go-kibana-rest/v8/kbapi"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ fBool = boolPointer(false)
+ tBool = boolPointer(true)
+)
+
+func boolPointer(v bool) *bool {
+ var res = new(bool)
+ *res = v
+ return res
+}
+
+func TestToModelV0(t *testing.T) {
+ testcases := []struct {
+ name string
+ input kbapi.SyntheticsMonitor
+ expected tfModelV0
+ }{
+ {
+ name: "HTTP monitor empty data",
+ input: kbapi.SyntheticsMonitor{
+ Type: kbapi.Http,
+ },
+ expected: tfModelV0{
+ ID: types.StringValue("/"),
+ Name: types.StringValue(""),
+ SpaceID: types.StringValue(""),
+ Schedule: types.Int64Value(0),
+ APMServiceName: types.StringValue(""),
+ TimeoutSeconds: types.Int64Value(0),
+ Params: jsontypes.NewNormalizedValue("null"),
+ HTTP: &tfHTTPMonitorFieldsV0{
+ URL: types.StringValue(""),
+ SslVerificationMode: types.StringValue(""),
+ MaxRedirects: types.Int64Value(0),
+ Mode: types.StringValue(""),
+ Username: types.StringValue(""),
+ Password: types.StringValue(""),
+ ProxyHeader: jsontypes.NewNormalizedValue("null"),
+ ProxyURL: types.StringValue(""),
+ Response: jsontypes.NewNormalizedValue("null"),
+ Check: jsontypes.NewNormalizedValue("null"),
+ },
+ },
+ },
+ {
+ name: "TCP monitor empty data",
+ input: kbapi.SyntheticsMonitor{
+ Type: kbapi.Tcp,
+ },
+ expected: tfModelV0{
+ ID: types.StringValue("/"),
+ Name: types.StringValue(""),
+ SpaceID: types.StringValue(""),
+ Schedule: types.Int64Value(0),
+ APMServiceName: types.StringValue(""),
+ TimeoutSeconds: types.Int64Value(0),
+ Params: jsontypes.NewNormalizedValue("null"),
+ TCP: &tfTCPMonitorFieldsV0{
+ Host: types.StringValue(""),
+ SslVerificationMode: types.StringValue(""),
+ CheckSend: types.StringValue(""),
+ CheckReceive: types.StringValue(""),
+ ProxyURL: types.StringValue(""),
+ },
+ },
+ },
+ {
+ name: "HTTP monitor",
+ input: kbapi.SyntheticsMonitor{
+ Id: "test-id-http",
+ Name: "test-name-http",
+ Namespace: "default",
+ Enabled: tBool,
+ Alert: &kbapi.MonitorAlertConfig{Status: &kbapi.SyntheticsStatusConfig{Enabled: tBool}, Tls: &kbapi.SyntheticsStatusConfig{Enabled: fBool}},
+ Schedule: &kbapi.MonitorScheduleConfig{Number: "5", Unit: "m"},
+ Tags: []string{"tag1", "tag2"},
+ APMServiceName: "test-service-http",
+ Timeout: json.Number("30"),
+ Locations: []kbapi.MonitorLocationConfig{
+ {Label: "us_east", IsServiceManaged: true},
+ {Label: "test private location", IsServiceManaged: false},
+ },
+ Origin: "origin",
+ Params: kbapi.JsonObject{"param1": "value1"},
+ MaxAttempts: 3,
+ Revision: 1,
+ Ui: kbapi.JsonObject{"is_tls_enabled": false},
+ Type: kbapi.Http,
+ Url: "https://example.com",
+ Mode: kbapi.HttpMonitorMode("all"),
+ MaxRedirects: "5",
+ Ipv4: tBool,
+ Ipv6: fBool,
+ Username: "user",
+ Password: "pass",
+ ProxyHeaders: kbapi.JsonObject{"header1": "value1"},
+ ProxyUrl: "https://proxy.com",
+ CheckResponseBodyPositive: []string{"foo", "bar"},
+ CheckResponseStatus: []string{"200", "201"},
+ ResponseIncludeBody: "always",
+ ResponseIncludeHeaders: true,
+ ResponseIncludeBodyMaxBytes: "1024",
+ CheckRequestBody: kbapi.JsonObject{"type": "text", "value": "name=first&email=someemail%40someemailprovider.com"},
+ CheckRequestHeaders: kbapi.JsonObject{"Content-Type": "application/x-www-form-urlencoded"},
+ CheckRequestMethod: "POST",
+ SslVerificationMode: "full",
+ SslSupportedProtocols: []string{"TLSv1.2", "TLSv1.3"},
+ },
+ expected: tfModelV0{
+ ID: types.StringValue("default/test-id-http"),
+ Name: types.StringValue("test-name-http"),
+ SpaceID: types.StringValue("default"),
+ Schedule: types.Int64Value(5),
+ Locations: []types.String{types.StringValue("us_east")},
+ PrivateLocations: []types.String{types.StringValue("test private location")},
+ Enabled: types.BoolPointerValue(tBool),
+ Tags: []types.String{types.StringValue("tag1"), types.StringValue("tag2")},
+ Alert: &tfAlertConfigV0{Status: &tfStatusConfigV0{Enabled: types.BoolPointerValue(tBool)}, TLS: &tfStatusConfigV0{Enabled: types.BoolPointerValue(fBool)}},
+ APMServiceName: types.StringValue("test-service-http"),
+ TimeoutSeconds: types.Int64Value(30),
+ Params: jsontypes.NewNormalizedValue(`{"param1":"value1"}`),
+ HTTP: &tfHTTPMonitorFieldsV0{
+ URL: types.StringValue("https://example.com"),
+ SslVerificationMode: types.StringValue("full"),
+ SslSupportedProtocols: []types.String{types.StringValue("TLSv1.2"), types.StringValue("TLSv1.3")},
+ MaxRedirects: types.Int64Value(5),
+ Mode: types.StringValue("all"),
+ IPv4: types.BoolPointerValue(tBool),
+ IPv6: types.BoolPointerValue(fBool),
+ Username: types.StringValue("user"),
+ Password: types.StringValue("pass"),
+ ProxyHeader: jsontypes.NewNormalizedValue(`{"header1":"value1"}`),
+ ProxyURL: types.StringValue("https://proxy.com"),
+ },
+ },
+ },
+ {
+ name: "TCP monitor",
+ input: kbapi.SyntheticsMonitor{
+ Id: "test-id-tcp",
+ Name: "test-name-tcp",
+ Namespace: "default",
+ Enabled: tBool,
+ Alert: &kbapi.MonitorAlertConfig{Status: &kbapi.SyntheticsStatusConfig{Enabled: tBool}},
+ Schedule: &kbapi.MonitorScheduleConfig{Number: "5", Unit: "m"},
+ Tags: nil,
+ APMServiceName: "test-service-tcp",
+ Timeout: json.Number("30"),
+ Locations: []kbapi.MonitorLocationConfig{
+ {Label: "test private location", IsServiceManaged: false},
+ },
+ Origin: "origin",
+ Params: kbapi.JsonObject{"param1": "value1"},
+ MaxAttempts: 3,
+ Revision: 1,
+ Ui: kbapi.JsonObject{"is_tls_enabled": false},
+ Type: kbapi.Tcp,
+ SslVerificationMode: "full",
+ SslSupportedProtocols: []string{"TLSv1.2", "TLSv1.3"},
+ ProxyUrl: "http://proxy.com",
+ Host: "example.com:9200",
+ CheckSend: "hello",
+ CheckReceive: "world",
+ ProxyUseLocalResolver: tBool,
+ },
+ expected: tfModelV0{
+ ID: types.StringValue("default/test-id-tcp"),
+ Name: types.StringValue("test-name-tcp"),
+ SpaceID: types.StringValue("default"),
+ Schedule: types.Int64Value(5),
+ Locations: nil,
+ PrivateLocations: []types.String{types.StringValue("test private location")},
+ Enabled: types.BoolPointerValue(tBool),
+ Tags: nil,
+ Alert: &tfAlertConfigV0{Status: &tfStatusConfigV0{Enabled: types.BoolPointerValue(tBool)}},
+ APMServiceName: types.StringValue("test-service-tcp"),
+ TimeoutSeconds: types.Int64Value(30),
+ Params: jsontypes.NewNormalizedValue(`{"param1":"value1"}`),
+ TCP: &tfTCPMonitorFieldsV0{
+ Host: types.StringValue("example.com:9200"),
+ SslVerificationMode: types.StringValue("full"),
+ SslSupportedProtocols: []types.String{types.StringValue("TLSv1.2"), types.StringValue("TLSv1.3")},
+ CheckSend: types.StringValue("hello"),
+ CheckReceive: types.StringValue("world"),
+ ProxyURL: types.StringValue("http://proxy.com"),
+ ProxyUseLocalResolver: types.BoolPointerValue(tBool),
+ },
+ },
+ },
+ }
+
+ for _, tt := range testcases {
+ t.Run(tt.name, func(t *testing.T) {
+ model, err := tt.expected.toModelV0(&tt.input)
+ assert.NoError(t, err)
+ assert.Equal(t, &tt.expected, model)
+ })
+ }
+}
+
+func TestToKibanaAPIRequest(t *testing.T) {
+ testcases := []struct {
+ name string
+ input tfModelV0
+ expected kibanaAPIRequest
+ }{
+ {
+ name: "Empty HTTP monitor",
+ input: tfModelV0{
+ HTTP: &tfHTTPMonitorFieldsV0{},
+ },
+ expected: kibanaAPIRequest{
+ fields: kbapi.HTTPMonitorFields{},
+ config: kbapi.SyntheticsMonitorConfig{},
+ },
+ },
+ {
+ name: "Empty TCP monitor",
+ input: tfModelV0{
+ TCP: &tfTCPMonitorFieldsV0{},
+ },
+ expected: kibanaAPIRequest{
+ fields: kbapi.TCPMonitorFields{},
+ config: kbapi.SyntheticsMonitorConfig{},
+ },
+ },
+ {
+ name: "HTTP monitor",
+ input: tfModelV0{
+ ID: types.StringValue("test-id-http"),
+ Name: types.StringValue("test-name-http"),
+ SpaceID: types.StringValue("default"),
+ Schedule: types.Int64Value(5),
+ Locations: []types.String{types.StringValue("us_east")},
+ PrivateLocations: []types.String{types.StringValue("test private location")},
+ Enabled: types.BoolPointerValue(tBool),
+ Tags: []types.String{types.StringValue("tag1"), types.StringValue("tag2")},
+ Alert: &tfAlertConfigV0{Status: &tfStatusConfigV0{Enabled: types.BoolPointerValue(tBool)}, TLS: &tfStatusConfigV0{Enabled: types.BoolPointerValue(fBool)}},
+ APMServiceName: types.StringValue("test-service-http"),
+ TimeoutSeconds: types.Int64Value(30),
+ Params: jsontypes.NewNormalizedValue(`{"param1":"value1"}`),
+ HTTP: &tfHTTPMonitorFieldsV0{
+ URL: types.StringValue("https://example.com"),
+ SslVerificationMode: types.StringValue("full"),
+ SslSupportedProtocols: []types.String{types.StringValue("TLSv1.2"), types.StringValue("TLSv1.3")},
+ MaxRedirects: types.Int64Value(5),
+ Mode: types.StringValue("all"),
+ IPv4: types.BoolPointerValue(tBool),
+ IPv6: types.BoolPointerValue(fBool),
+ Username: types.StringValue("user"),
+ Password: types.StringValue("pass"),
+ ProxyHeader: jsontypes.NewNormalizedValue(`{"header1":"value1"}`),
+ ProxyURL: types.StringValue("https://proxy.com"),
+ Response: jsontypes.NewNormalizedValue(`{"response1":"value1"}`),
+ Check: jsontypes.NewNormalizedValue(`{"check1":"value1"}`),
+ },
+ },
+ expected: kibanaAPIRequest{
+ config: kbapi.SyntheticsMonitorConfig{
+ Name: "test-name-http",
+ Schedule: kbapi.MonitorSchedule(5),
+ Locations: []kbapi.MonitorLocation{"us_east"},
+ PrivateLocations: []string{"test private location"},
+ Enabled: tBool,
+ Tags: []string{"tag1", "tag2"},
+ Alert: &kbapi.MonitorAlertConfig{Status: &kbapi.SyntheticsStatusConfig{Enabled: tBool}, Tls: &kbapi.SyntheticsStatusConfig{Enabled: fBool}},
+ APMServiceName: "test-service-http",
+ Namespace: "default",
+ TimeoutSeconds: 30,
+ Params: kbapi.JsonObject{"param1": "value1"},
+ },
+ fields: kbapi.HTTPMonitorFields{
+ Url: "https://example.com",
+ SslVerificationMode: "full",
+ SslSupportedProtocols: []string{"TLSv1.2", "TLSv1.3"},
+ MaxRedirects: "5",
+ Mode: "all",
+ Ipv4: tBool,
+ Ipv6: fBool,
+ Username: "user",
+ Password: "pass",
+ ProxyHeader: kbapi.JsonObject{"header1": "value1"},
+ ProxyUrl: "https://proxy.com",
+ Response: kbapi.JsonObject{"response1": "value1"},
+ Check: kbapi.JsonObject{"check1": "value1"},
+ },
+ },
+ },
+ {
+ name: "TCP monitor",
+ input: tfModelV0{
+ ID: types.StringValue("test-id-tcp"),
+ Name: types.StringValue("test-name-tcp"),
+ SpaceID: types.StringValue("default"),
+ Schedule: types.Int64Value(5),
+ Locations: []types.String{types.StringValue("us_east")},
+ PrivateLocations: nil,
+ Enabled: types.BoolPointerValue(tBool),
+ Tags: []types.String{types.StringValue("tag1"), types.StringValue("tag2")},
+ Alert: &tfAlertConfigV0{Status: &tfStatusConfigV0{Enabled: types.BoolPointerValue(tBool)}},
+ APMServiceName: types.StringValue("test-service-tcp"),
+ TimeoutSeconds: types.Int64Value(30),
+ Params: jsontypes.NewNormalizedValue(`{"param1":"value1"}`),
+ TCP: &tfTCPMonitorFieldsV0{
+ Host: types.StringValue("example.com:9200"),
+ SslVerificationMode: types.StringValue("full"),
+ SslSupportedProtocols: []types.String{types.StringValue("TLSv1.2"), types.StringValue("TLSv1.3")},
+ CheckSend: types.StringValue("hello"),
+ CheckReceive: types.StringValue("world"),
+ ProxyURL: types.StringValue("http://proxy.com"),
+ ProxyUseLocalResolver: types.BoolPointerValue(tBool),
+ },
+ },
+ expected: kibanaAPIRequest{
+ config: kbapi.SyntheticsMonitorConfig{
+ Name: "test-name-tcp",
+ Schedule: kbapi.MonitorSchedule(5),
+ Locations: []kbapi.MonitorLocation{"us_east"},
+ PrivateLocations: nil,
+ Enabled: tBool,
+ Tags: []string{"tag1", "tag2"},
+ Alert: &kbapi.MonitorAlertConfig{Status: &kbapi.SyntheticsStatusConfig{Enabled: tBool}},
+ APMServiceName: "test-service-tcp",
+ Namespace: "default",
+ TimeoutSeconds: 30,
+ Params: kbapi.JsonObject{"param1": "value1"},
+ },
+ fields: kbapi.TCPMonitorFields{
+ Host: "example.com:9200",
+ SslVerificationMode: "full",
+ SslSupportedProtocols: []string{"TLSv1.2", "TLSv1.3"},
+ CheckSend: "hello",
+ CheckReceive: "world",
+ ProxyUrl: "http://proxy.com",
+ ProxyUseLocalResolver: tBool,
+ },
+ },
+ },
+ }
+
+ for _, tt := range testcases {
+ t.Run(tt.name, func(t *testing.T) {
+ apiRequest, dg := tt.input.toKibanaAPIRequest()
+ assert.False(t, dg.HasError(), dg.Errors())
+ assert.Equal(t, &tt.expected, apiRequest)
+ })
+ }
+}
+
+func TestToModelV0MergeAttributes(t *testing.T) {
+
+ testcases := []struct {
+ name string
+ input kbapi.SyntheticsMonitor
+ state tfModelV0
+ expected tfModelV0
+ }{
+ {
+ name: "HTTP monitor",
+ state: tfModelV0{
+ HTTP: &tfHTTPMonitorFieldsV0{
+ ProxyHeader: jsontypes.NewNormalizedValue(`{"header1":"value1"}`),
+ Username: types.StringValue("test"),
+ Password: types.StringValue("password"),
+ Check: jsontypes.NewNormalizedValue(`{"check1":"value1"}`),
+ Response: jsontypes.NewNormalizedValue(`{"response1":"value1"}`),
+ },
+ Params: jsontypes.NewNormalizedValue(`{"param1":"value1"}`),
+ RetestOnFailure: types.BoolValue(true),
+ },
+ input: kbapi.SyntheticsMonitor{
+ Type: kbapi.Http,
+ },
+ expected: tfModelV0{
+ ID: types.StringValue("/"),
+ Name: types.StringValue(""),
+ SpaceID: types.StringValue(""),
+ Schedule: types.Int64Value(0),
+ APMServiceName: types.StringValue(""),
+ TimeoutSeconds: types.Int64Value(0),
+ Params: jsontypes.NewNormalizedValue(`{"param1":"value1"}`),
+ RetestOnFailure: types.BoolValue(true),
+ HTTP: &tfHTTPMonitorFieldsV0{
+ URL: types.StringValue(""),
+ SslVerificationMode: types.StringValue(""),
+ MaxRedirects: types.Int64Value(0),
+ Mode: types.StringValue(""),
+ ProxyURL: types.StringValue(""),
+ ProxyHeader: jsontypes.NewNormalizedValue(`{"header1":"value1"}`),
+ Username: types.StringValue("test"),
+ Password: types.StringValue("password"),
+ Check: jsontypes.NewNormalizedValue(`{"check1":"value1"}`),
+ Response: jsontypes.NewNormalizedValue(`{"response1":"value1"}`),
+ },
+ },
+ },
+ {
+ name: "TCP monitor",
+ state: tfModelV0{
+ TCP: &tfTCPMonitorFieldsV0{
+ CheckSend: types.StringValue("hello"),
+ CheckReceive: types.StringValue("world"),
+ },
+ },
+ input: kbapi.SyntheticsMonitor{
+ Type: kbapi.Tcp,
+ },
+ expected: tfModelV0{
+ ID: types.StringValue("/"),
+ Name: types.StringValue(""),
+ SpaceID: types.StringValue(""),
+ Schedule: types.Int64Value(0),
+ APMServiceName: types.StringValue(""),
+ TimeoutSeconds: types.Int64Value(0),
+ TCP: &tfTCPMonitorFieldsV0{
+ Host: types.StringValue(""),
+ SslVerificationMode: types.StringValue(""),
+ CheckSend: types.StringValue("hello"),
+ CheckReceive: types.StringValue("world"),
+ ProxyURL: types.StringValue(""),
+ },
+ },
+ },
+ }
+
+ for _, tt := range testcases {
+ t.Run(tt.name, func(t *testing.T) {
+ actual, err := tt.state.toModelV0(&tt.input)
+ assert.NoError(t, err)
+ assert.NotNil(t, actual)
+ assert.Equal(t, &tt.expected, actual)
+ })
+ }
+}
diff --git a/internal/kibana/synthetics/update.go b/internal/kibana/synthetics/update.go
new file mode 100644
index 000000000..bcd66b914
--- /dev/null
+++ b/internal/kibana/synthetics/update.go
@@ -0,0 +1,54 @@
+package synthetics
+
+import (
+ "context"
+ "fmt"
+ "github.com/disaster37/go-kibana-rest/v8/kbapi"
+ "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)
+ if kibanaClient == nil {
+ return
+ }
+
+ plan := new(tfModelV0)
+ diags := request.Plan.Get(ctx, plan)
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ input, diags := plan.toKibanaAPIRequest()
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ monitorId, dg := GetCompositeId(plan.ID.ValueString())
+ response.Diagnostics.Append(dg...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+
+ namespace := plan.SpaceID.ValueString()
+ result, err := kibanaClient.KibanaSynthetics.Monitor.Update(ctx, kbapi.MonitorID(monitorId.ResourceId), input.config, input.fields, namespace)
+ if err != nil {
+ response.Diagnostics.AddError(fmt.Sprintf("Failed to update Kibana monitor `%s`, namespace %s", input.config.Name, namespace), err.Error())
+ return
+ }
+
+ plan, err = plan.toModelV0(result)
+ if err != nil {
+ response.Diagnostics.AddError("Failed to convert Kibana monitor API to TF state", err.Error())
+ return
+ }
+
+ diags = response.State.Set(ctx, plan)
+ response.Diagnostics.Append(diags...)
+ if response.Diagnostics.HasError() {
+ return
+ }
+}
diff --git a/libs/go-kibana-rest/docker-compose.yml b/libs/go-kibana-rest/docker-compose.yml
index 0f12cb3e1..e0c6ac37b 100644
--- a/libs/go-kibana-rest/docker-compose.yml
+++ b/libs/go-kibana-rest/docker-compose.yml
@@ -4,10 +4,10 @@ services:
environment:
cluster.name: test
discovery.type: single-node
- ELASTIC_PASSWORD: changeme
- xpack.security.enabled: "true"
+ xpack.security.enabled: true
xpack.security.http.ssl.enabled: false
xpack.license.self_generated.type: trial
+ ELASTIC_PASSWORD: changeme
ports:
- "9200:9200/tcp"
set-kibana-password:
@@ -24,11 +24,10 @@ services:
kibana:
image: docker.elastic.co/kibana/kibana:8.15.0
environment:
+ SERVER_NAME: kibana
ELASTICSEARCH_HOSTS: http://es:9200
ELASTICSEARCH_USERNAME: kibana_system
ELASTICSEARCH_PASSWORD: changeme
- xpack.security.http.ssl.enabled: false
- xpack.license.self_generated.type: trial
XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY: min-32-byte-long-strong-encryption-key
links:
- elasticsearch:es
@@ -36,4 +35,4 @@ services:
- "5601:5601/tcp"
depends_on:
set-kibana-password:
- condition: service_completed_successfully
\ No newline at end of file
+ condition: service_completed_successfully
diff --git a/libs/go-kibana-rest/kbapi/api.kibana_synthetics.go b/libs/go-kibana-rest/kbapi/api.kibana_synthetics.go
index f141d8bed..44193dede 100644
--- a/libs/go-kibana-rest/kbapi/api.kibana_synthetics.go
+++ b/libs/go-kibana-rest/kbapi/api.kibana_synthetics.go
@@ -1,11 +1,13 @@
package kbapi
import (
+ "context"
"encoding/json"
"fmt"
+ "time"
+
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
- "time"
)
const (
@@ -45,6 +47,10 @@ const (
ModeAny = "any"
)
+type MonitorFields interface {
+ APIRequest(cfg SyntheticsMonitorConfig) interface{}
+}
+
type KibanaError struct {
Code int `json:"statusCode,omitempty"`
Error string `json:"error,omitempty"`
@@ -81,19 +87,30 @@ type MonitorAlertConfig struct {
Tls *SyntheticsStatusConfig `json:"tls,omitempty"`
}
+type TCPMonitorFields struct {
+ Host string `json:"host"`
+ SslVerificationMode string `json:"ssl.verification_mode,omitempty"`
+ SslSupportedProtocols []string `json:"ssl.supported_protocols,omitempty"`
+ CheckSend string `json:"check.send,omitempty"`
+ CheckReceive string `json:"check.receive,omitempty"`
+ ProxyUrl string `json:"proxy_url,omitempty"`
+ ProxyUseLocalResolver *bool `json:"proxy_use_local_resolver,omitempty"`
+}
+
type HTTPMonitorFields struct {
- Url string `json:"url"`
- SslSetting JsonObject `json:"ssl,omitempty"` //https://www.elastic.co/guide/en/beats/heartbeat/current/configuration-ssl.html
- MaxRedirects string `json:"max_redirects,omitempty"`
- Mode HttpMonitorMode `json:"mode,omitempty"`
- Ipv4 *bool `json:"ipv4,omitempty"`
- Ipv6 *bool `json:"ipv6,omitempty"`
- Username string `json:"username,omitempty"`
- Password string `json:"password,omitempty"`
- ProxyHeader JsonObject `json:"proxy_headers,omitempty"`
- ProxyUrl string `json:"proxy_url,omitempty"`
- Response JsonObject `json:"response,omitempty"`
- Check JsonObject `json:"check,omitempty"`
+ Url string `json:"url"`
+ SslVerificationMode string `json:"ssl.verification_mode,omitempty"`
+ SslSupportedProtocols []string `json:"ssl.supported_protocols,omitempty"`
+ MaxRedirects string `json:"max_redirects,omitempty"`
+ Mode HttpMonitorMode `json:"mode,omitempty"`
+ Ipv4 *bool `json:"ipv4,omitempty"`
+ Ipv6 *bool `json:"ipv6,omitempty"`
+ Username string `json:"username,omitempty"`
+ Password string `json:"password,omitempty"`
+ ProxyHeader JsonObject `json:"proxy_headers,omitempty"`
+ ProxyUrl string `json:"proxy_url,omitempty"`
+ Response JsonObject `json:"response,omitempty"`
+ Check JsonObject `json:"check,omitempty"`
}
type SyntheticsMonitorConfig struct {
@@ -147,56 +164,103 @@ type MonitorDeleteStatus struct {
}
type SyntheticsMonitor struct {
- Name string `json:"name"`
- Type MonitorType `json:"type"`
- ConfigId MonitorID `json:"config_id"`
- Id MonitorID `json:"id"`
- Mode HttpMonitorMode `json:"mode"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
- Namespace string `json:"namespace"`
- Enabled *bool `json:"enabled,omitempty"`
- Alert *MonitorAlertConfig `json:"alert,omitempty"`
- Schedule *MonitorScheduleConfig `json:"schedule,omitempty"`
- Tags []string `json:"tags,omitempty"`
- APMServiceName string `json:"service.name,omitempty"`
- Timeout json.Number `json:"timeout,omitempty"`
- Locations []MonitorLocationConfig `json:"locations,omitempty"`
- Origin string `json:"origin,omitempty"`
- Params JsonObject `json:"params,omitempty"`
- MaxAttempts int `json:"max_attempts"`
- MaxRedirects string `json:"max_redirects"`
- ResponseIncludeBody string `json:"response.include_body"`
- ResponseIncludeHeaders bool `json:"response.include_headers"`
- CheckRequestMethod string `json:"check.request.method"`
- ResponseIncludeBodyMaxBytes string `json:"response.include_body_max_bytes,omitempty"`
- Ipv4 bool `json:"ipv4,omitempty"`
- Ipv6 bool `json:"ipv6,omitempty"`
- SslVerificationMode string `json:"ssl.verification_mode,omitempty"`
- SslSupportedProtocols []string `json:"ssl.supported_protocols,omitempty"`
- Revision int `json:"revision,omitempty"`
- Url string `json:"url,omitempty"`
- Ui struct {
- IsTlsEnabled bool `json:"is_tls_enabled"`
- } `json:"__ui,omitempty"`
+ Name string `json:"name"`
+ Type MonitorType `json:"type"`
+ ConfigId MonitorID `json:"config_id"`
+ Id MonitorID `json:"id"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ Namespace string `json:"namespace"`
+ Enabled *bool `json:"enabled,omitempty"`
+ Alert *MonitorAlertConfig `json:"alert,omitempty"`
+ Schedule *MonitorScheduleConfig `json:"schedule,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ APMServiceName string `json:"service.name,omitempty"`
+ Timeout json.Number `json:"timeout,omitempty"`
+ Locations []MonitorLocationConfig `json:"locations,omitempty"`
+ Origin string `json:"origin,omitempty"`
+ Params JsonObject `json:"params,omitempty"`
+ MaxAttempts int `json:"max_attempts"`
+ Revision int `json:"revision,omitempty"`
+ Ui JsonObject `json:"__ui,omitempty"`
+ //http
+ Url string `json:"url,omitempty"`
+ Mode HttpMonitorMode `json:"mode"`
+ MaxRedirects string `json:"max_redirects"`
+ Ipv4 *bool `json:"ipv4,omitempty"`
+ Ipv6 *bool `json:"ipv6,omitempty"`
+ Username string `json:"username,omitempty"`
+ Password string `json:"password,omitempty"`
+ ProxyHeaders JsonObject `json:"proxy_headers,omitempty"`
+ CheckResponseBodyPositive []string `json:"check.response.body.positive,omitempty"`
+ CheckResponseStatus []string `json:"check.response.status,omitempty"`
+ ResponseIncludeBody string `json:"response.include_body,omitempty"`
+ ResponseIncludeHeaders bool `json:"response.include_headers,omitempty"`
+ ResponseIncludeBodyMaxBytes string `json:"response.include_body_max_bytes,omitempty"`
+ CheckRequestBody JsonObject `json:"check.request.body,omitempty"`
+ CheckRequestHeaders JsonObject `json:"check.request.headers,omitempty"`
+ CheckRequestMethod string `json:"check.request.method,omitempty"`
+ //http and tcp
+ ProxyUrl string `json:"proxy_url,omitempty"`
+ SslVerificationMode string `json:"ssl.verification_mode"`
+ SslSupportedProtocols []string `json:"ssl.supported_protocols"`
+ //tcp
+ Host string `json:"host,omitempty"`
+ ProxyUseLocalResolver *bool `json:"proxy_use_local_resolver,omitempty"`
+ CheckSend string `json:"check.send,omitempty"`
+ CheckReceive string `json:"check.receive,omitempty"`
+}
+
+type MonitorTypeConfig struct {
+ Type MonitorType `json:"type"`
+}
+
+func (f HTTPMonitorFields) APIRequest(config SyntheticsMonitorConfig) interface{} {
+
+ mType := MonitorTypeConfig{Type: Http}
+
+ return struct {
+ SyntheticsMonitorConfig
+ MonitorTypeConfig
+ HTTPMonitorFields
+ }{
+ config,
+ mType,
+ f,
+ }
+}
+
+func (f TCPMonitorFields) APIRequest(config SyntheticsMonitorConfig) interface{} {
+
+ mType := MonitorTypeConfig{Type: Tcp}
+
+ return struct {
+ SyntheticsMonitorConfig
+ MonitorTypeConfig
+ TCPMonitorFields
+ }{
+ config,
+ mType,
+ f,
+ }
}
-type KibanaSyntheticsMonitorAdd func(config SyntheticsMonitorConfig, fields HTTPMonitorFields, namespace string) (*SyntheticsMonitor, error)
+type KibanaSyntheticsMonitorAdd func(ctx context.Context, config SyntheticsMonitorConfig, fields MonitorFields, namespace string) (*SyntheticsMonitor, error)
-type KibanaSyntheticsMonitorUpdate func(id MonitorID, config SyntheticsMonitorConfig, fields HTTPMonitorFields, namespace string) (*SyntheticsMonitor, error)
+type KibanaSyntheticsMonitorUpdate func(ctx context.Context, id MonitorID, config SyntheticsMonitorConfig, fields MonitorFields, namespace string) (*SyntheticsMonitor, error)
-type KibanaSyntheticsMonitorGet func(id MonitorID, namespace string) (*SyntheticsMonitor, error)
+type KibanaSyntheticsMonitorGet func(ctx context.Context, id MonitorID, namespace string) (*SyntheticsMonitor, error)
-type KibanaSyntheticsMonitorDelete func(namespace string, ids ...MonitorID) ([]MonitorDeleteStatus, error)
+type KibanaSyntheticsMonitorDelete func(ctx context.Context, namespace string, ids ...MonitorID) ([]MonitorDeleteStatus, error)
-type KibanaSyntheticsPrivateLocationCreate func(pLoc PrivateLocationConfig, namespace string) (*PrivateLocation, error)
+type KibanaSyntheticsPrivateLocationCreate func(ctx context.Context, pLoc PrivateLocationConfig, namespace string) (*PrivateLocation, error)
-type KibanaSyntheticsPrivateLocationGet func(idOrLabel string, namespace string) (*PrivateLocation, error)
+type KibanaSyntheticsPrivateLocationGet func(ctx context.Context, idOrLabel string, namespace string) (*PrivateLocation, error)
-type KibanaSyntheticsPrivateLocationDelete func(id string, namespace string) error
+type KibanaSyntheticsPrivateLocationDelete func(ctx context.Context, id string, namespace string) error
func newKibanaSyntheticsPrivateLocationGetFunc(c *resty.Client) KibanaSyntheticsPrivateLocationGet {
- return func(idOrLabel string, namespace string) (*PrivateLocation, error) {
+ return func(ctx context.Context, idOrLabel string, namespace string) (*PrivateLocation, error) {
if idOrLabel == "" {
return nil, APIError{
@@ -207,7 +271,7 @@ func newKibanaSyntheticsPrivateLocationGetFunc(c *resty.Client) KibanaSynthetics
path := basePathWithId(namespace, privateLocationsSuffix, idOrLabel)
log.Debugf("URL to get private locations: %s", path)
- resp, err := c.R().Get(path)
+ resp, err := c.R().SetContext(ctx).Get(path)
if err = handleKibanaError(err, resp); err != nil {
return nil, err
}
@@ -216,21 +280,21 @@ func newKibanaSyntheticsPrivateLocationGetFunc(c *resty.Client) KibanaSynthetics
}
func newKibanaSyntheticsPrivateLocationDeleteFunc(c *resty.Client) KibanaSyntheticsPrivateLocationDelete {
- return func(id string, namespace string) error {
+ return func(ctx context.Context, id string, namespace string) error {
path := basePathWithId(namespace, privateLocationsSuffix, id)
log.Debugf("URL to delete private locations: %s", path)
- resp, err := c.R().Delete(path)
+ resp, err := c.R().SetContext(ctx).Delete(path)
err = handleKibanaError(err, resp)
return err
}
}
func newKibanaSyntheticsMonitorGetFunc(c *resty.Client) KibanaSyntheticsMonitorGet {
- return func(id MonitorID, namespace string) (*SyntheticsMonitor, error) {
+ return func(ctx context.Context, id MonitorID, namespace string) (*SyntheticsMonitor, error) {
path := basePathWithId(namespace, monitorsSuffix, id)
log.Debugf("URL to get monitor: %s", path)
- resp, err := c.R().Get(path)
+ resp, err := c.R().SetContext(ctx).Get(path)
if err := handleKibanaError(err, resp); err != nil {
return nil, err
}
@@ -239,11 +303,11 @@ func newKibanaSyntheticsMonitorGetFunc(c *resty.Client) KibanaSyntheticsMonitorG
}
func newKibanaSyntheticsMonitorDeleteFunc(c *resty.Client) KibanaSyntheticsMonitorDelete {
- return func(namespace string, ids ...MonitorID) ([]MonitorDeleteStatus, error) {
+ return func(ctx context.Context, namespace string, ids ...MonitorID) ([]MonitorDeleteStatus, error) {
path := basePath(namespace, monitorsSuffix)
log.Debugf("URL to delete monitors: %s", path)
- resp, err := c.R().SetBody(map[string]interface{}{
+ resp, err := c.R().SetContext(ctx).SetBody(map[string]interface{}{
"ids": ids,
}).Delete(path)
if err = handleKibanaError(err, resp); err != nil {
@@ -256,11 +320,11 @@ func newKibanaSyntheticsMonitorDeleteFunc(c *resty.Client) KibanaSyntheticsMonit
}
func newKibanaSyntheticsPrivateLocationCreateFunc(c *resty.Client) KibanaSyntheticsPrivateLocationCreate {
- return func(pLoc PrivateLocationConfig, namespace string) (*PrivateLocation, error) {
+ return func(ctx context.Context, pLoc PrivateLocationConfig, namespace string) (*PrivateLocation, error) {
path := basePath(namespace, privateLocationsSuffix)
log.Debugf("URL to create private locations: %s", path)
- resp, err := c.R().SetBody(pLoc).Post(path)
+ resp, err := c.R().SetContext(ctx).SetBody(pLoc).Post(path)
if err = handleKibanaError(err, resp); err != nil {
return nil, err
}
@@ -269,12 +333,12 @@ func newKibanaSyntheticsPrivateLocationCreateFunc(c *resty.Client) KibanaSynthet
}
func newKibanaSyntheticsMonitorUpdateFunc(c *resty.Client) KibanaSyntheticsMonitorUpdate {
- return func(id MonitorID, config SyntheticsMonitorConfig, fields HTTPMonitorFields, namespace string) (*SyntheticsMonitor, error) {
+ return func(ctx context.Context, id MonitorID, config SyntheticsMonitorConfig, fields MonitorFields, namespace string) (*SyntheticsMonitor, error) {
path := basePathWithId(namespace, monitorsSuffix, id)
log.Debugf("URL to update monitor: %s", path)
- data := buildMonitorJson(config, fields)
- resp, err := c.R().SetBody(data).Put(path)
+ data := fields.APIRequest(config)
+ resp, err := c.R().SetContext(ctx).SetBody(data).Put(path)
if err := handleKibanaError(err, resp); err != nil {
return nil, err
}
@@ -283,12 +347,12 @@ func newKibanaSyntheticsMonitorUpdateFunc(c *resty.Client) KibanaSyntheticsMonit
}
func newKibanaSyntheticsMonitorAddFunc(c *resty.Client) KibanaSyntheticsMonitorAdd {
- return func(config SyntheticsMonitorConfig, fields HTTPMonitorFields, namespace string) (*SyntheticsMonitor, error) {
+ return func(ctx context.Context, config SyntheticsMonitorConfig, fields MonitorFields, namespace string) (*SyntheticsMonitor, error) {
path := basePath(namespace, monitorsSuffix)
log.Debugf("URL to create monitor: %s", path)
- data := buildMonitorJson(config, fields)
- resp, err := c.R().SetBody(data).Post(path)
+ data := fields.APIRequest(config)
+ resp, err := c.R().SetContext(ctx).SetBody(data).Post(path)
if err := handleKibanaError(err, resp); err != nil {
return nil, err
}
@@ -296,27 +360,6 @@ func newKibanaSyntheticsMonitorAddFunc(c *resty.Client) KibanaSyntheticsMonitorA
}
}
-// current idea here is to switch fields HTTPMonitorFields to interface{} and to
-// type switch in the function for future monitor types
-func buildMonitorJson(config SyntheticsMonitorConfig, fields HTTPMonitorFields) interface{} {
-
- type MonitorTypeConfig struct {
- Type MonitorType `json:"type"`
- }
-
- mType := MonitorTypeConfig{Type: Http}
-
- return struct {
- SyntheticsMonitorConfig
- MonitorTypeConfig
- HTTPMonitorFields
- }{
- config,
- mType,
- fields,
- }
-}
-
func unmarshal[T interface{}](resp *resty.Response, result T) (*T, error) {
respBody := resp.Body()
err := json.Unmarshal(respBody, &result)
diff --git a/libs/go-kibana-rest/kbapi/api.kibana_synthetics_test.go b/libs/go-kibana-rest/kbapi/api.kibana_synthetics_test.go
index b3341653d..22df4891b 100644
--- a/libs/go-kibana-rest/kbapi/api.kibana_synthetics_test.go
+++ b/libs/go-kibana-rest/kbapi/api.kibana_synthetics_test.go
@@ -1,6 +1,7 @@
package kbapi
import (
+ "context"
"encoding/json"
"fmt"
"testing"
@@ -50,13 +51,17 @@ func testWithPolicy(t *testing.T, client *resty.Client, namespace string, f func
f(policy.Item.Id)
}
+// TODO: test update method when set an optional parameter to `null`
+
func (s *KBAPITestSuite) TestKibanaSyntheticsMonitorAPI() {
type TestConfig struct {
config SyntheticsMonitorConfig
- fields HTTPMonitorFields
+ fields MonitorFields
}
+ ctx := context.Background()
+
for _, n := range namespaces {
testUuid := uuid.New().String()
space := n
@@ -67,10 +72,10 @@ func (s *KBAPITestSuite) TestKibanaSyntheticsMonitorAPI() {
Label: fmt.Sprintf("TestKibanaSyntheticsMonitorAdd %s", testUuid),
AgentPolicyId: policyId,
}
- location, err := syntheticsAPI.PrivateLocation.Create(locationConfig, space)
+ location, err := syntheticsAPI.PrivateLocation.Create(ctx, locationConfig, space)
assert.NoError(s.T(), err)
defer func(id string) {
- syntheticsAPI.PrivateLocation.Delete(id, space)
+ syntheticsAPI.PrivateLocation.Delete(ctx, id, space)
}(location.Id)
f := new(bool)
@@ -101,6 +106,24 @@ func (s *KBAPITestSuite) TestKibanaSyntheticsMonitorAPI() {
},
},
},
+ {
+ name: "bare minimum tcp monitor",
+ input: TestConfig{
+ config: SyntheticsMonitorConfig{
+ Name: fmt.Sprintf("test synthetics tcp monitor %s", testUuid),
+ PrivateLocations: []string{location.Label},
+ },
+ fields: TCPMonitorFields{
+ Host: "localhost:5601",
+ },
+ },
+ update: TestConfig{
+ config: SyntheticsMonitorConfig{},
+ fields: TCPMonitorFields{
+ Host: "localhost:9200",
+ },
+ },
+ },
{
name: "all fields http monitor",
input: TestConfig{
@@ -124,16 +147,15 @@ func (s *KBAPITestSuite) TestKibanaSyntheticsMonitorAPI() {
RetestOnFailure: f,
},
fields: HTTPMonitorFields{
- Url: "http://localhost:5601",
- SslSetting: map[string]interface{}{
- "supported_protocols": []string{"TLSv1.0", "TLSv1.1", "TLSv1.2"},
- },
- MaxRedirects: "2",
- Mode: ModeAny,
- Ipv4: t,
- Ipv6: f,
- Username: "test-user-name",
- Password: "test-password",
+ Url: "http://localhost:5601",
+ SslSupportedProtocols: []string{"TLSv1.0", "TLSv1.1", "TLSv1.2"},
+ SslVerificationMode: "full",
+ MaxRedirects: "2",
+ Mode: ModeAny,
+ Ipv4: t,
+ Ipv6: f,
+ Username: "test-user-name",
+ Password: "test-password",
ProxyHeader: map[string]interface{}{
"User-Agent": "test",
},
@@ -170,6 +192,50 @@ func (s *KBAPITestSuite) TestKibanaSyntheticsMonitorAPI() {
},
},
},
+ {
+ name: "all fields tcp monitor",
+ input: TestConfig{
+ config: SyntheticsMonitorConfig{
+ Name: fmt.Sprintf("test all fields tcp monitor %s", testUuid),
+ Schedule: Every10Minutes,
+ PrivateLocations: []string{location.Label},
+ Enabled: f,
+ Tags: []string{"aaa", "bbb"},
+ Alert: &MonitorAlertConfig{
+ Status: &SyntheticsStatusConfig{Enabled: t},
+ Tls: &SyntheticsStatusConfig{Enabled: f},
+ },
+ APMServiceName: "APMServiceName",
+ TimeoutSeconds: 42,
+ Namespace: space,
+ Params: map[string]interface{}{
+ "param1": "some-params",
+ "my_url": "http://localhost:8080",
+ },
+ RetestOnFailure: f,
+ },
+ fields: TCPMonitorFields{
+ Host: "localhost:5601",
+ SslSupportedProtocols: []string{"TLSv1.0", "TLSv1.1", "TLSv1.2"},
+ SslVerificationMode: "full",
+ ProxyUseLocalResolver: t,
+ ProxyUrl: "http://localhost",
+ CheckSend: "Hello World",
+ CheckReceive: "Hello",
+ },
+ },
+ update: TestConfig{
+ config: SyntheticsMonitorConfig{
+ Name: fmt.Sprintf("update all fields tcp monitor %s", testUuid),
+ Schedule: Every30Minutes,
+ },
+ fields: TCPMonitorFields{
+ Host: "localhost:9200",
+ ProxyUrl: "http://127.0.0.1",
+ ProxyUseLocalResolver: f,
+ },
+ },
+ },
}
for _, tc := range testCases {
@@ -177,48 +243,67 @@ func (s *KBAPITestSuite) TestKibanaSyntheticsMonitorAPI() {
config := tc.input.config
fields := tc.input.fields
- monitor, err := syntheticsAPI.Monitor.Add(config, fields, space)
+ monitor, err := syntheticsAPI.Monitor.Add(ctx, config, fields, space)
assert.NoError(s.T(), err)
assert.NotNil(s.T(), monitor)
- monitor.Params = nil //kibana API doesn't return params for GET request
+ updateDueToKibanaAPIDiff(monitor)
- get, err := syntheticsAPI.Monitor.Get(monitor.Id, space)
+ get, err := syntheticsAPI.Monitor.Get(ctx, monitor.Id, space)
assert.NoError(s.T(), err)
assert.Equal(s.T(), monitor, get)
- get, err = syntheticsAPI.Monitor.Get(monitor.ConfigId, space)
+ get, err = syntheticsAPI.Monitor.Get(ctx, monitor.ConfigId, space)
assert.NoError(s.T(), err)
assert.Equal(s.T(), monitor, get)
- update, err := syntheticsAPI.Monitor.Update(monitor.Id, tc.update.config, tc.update.fields, space)
+ update, err := syntheticsAPI.Monitor.Update(ctx, monitor.Id, tc.update.config, tc.update.fields, space)
assert.NoError(s.T(), err)
assert.NotNil(s.T(), update)
- update.Params = nil //kibana API doesn't return params for GET request
+ updateDueToKibanaAPIDiff(update)
- get, err = syntheticsAPI.Monitor.Get(monitor.ConfigId, space)
+ get, err = syntheticsAPI.Monitor.Get(ctx, monitor.ConfigId, space)
assert.NoError(s.T(), err)
get.CreatedAt = time.Time{} // update response doesn't have created_at field
assert.Equal(s.T(), update, get)
- deleted, err := syntheticsAPI.Monitor.Delete(space, monitor.ConfigId)
+ deleted, err := syntheticsAPI.Monitor.Delete(ctx, space, monitor.ConfigId)
assert.NoError(s.T(), err)
for _, d := range deleted {
assert.True(s.T(), d.Deleted)
}
- deleted, err = syntheticsAPI.Monitor.Delete(space, monitor.Id)
+ deleted, err = syntheticsAPI.Monitor.Delete(ctx, space, monitor.Id)
assert.NoError(s.T(), err)
for _, d := range deleted {
assert.False(s.T(), d.Deleted)
}
+ _, err = syntheticsAPI.Monitor.Get(ctx, monitor.Id, space)
+ assert.Error(s.T(), err)
+ assert.IsType(s.T(), APIError{}, err)
+ assert.Equal(s.T(), 404, err.(APIError).Code)
})
}
})
}
}
+// see https://github.com/elastic/kibana/issues/189906
+func updateDueToKibanaAPIDiff(m *SyntheticsMonitor) {
+ m.Params = nil
+ m.Username = ""
+ m.Password = ""
+ m.ProxyHeaders = nil
+ m.CheckResponseBodyPositive = nil
+ m.CheckRequestBody = nil
+ m.CheckRequestHeaders = nil
+ m.CheckSend = ""
+ m.CheckReceive = ""
+}
+
func (s *KBAPITestSuite) TestKibanaSyntheticsPrivateLocationAPI() {
+ ctx := context.Background()
+
for _, n := range namespaces {
testUuid := uuid.New().String()
space := n
@@ -236,23 +321,23 @@ func (s *KBAPITestSuite) TestKibanaSyntheticsPrivateLocationAPI() {
Lon: -42.42,
},
}
- created, err := pAPI.Create(cfg, space)
+ created, err := pAPI.Create(ctx, cfg, space)
assert.NoError(s.T(), err)
assert.Equal(s.T(), created.Label, cfg.Label)
assert.Equal(s.T(), created.AgentPolicyId, cfg.AgentPolicyId)
- get, err := pAPI.Get(created.Id, space)
+ get, err := pAPI.Get(ctx, created.Id, space)
assert.NoError(s.T(), err)
assert.Equal(s.T(), created, get)
- get, err = pAPI.Get(created.Label, space)
+ get, err = pAPI.Get(ctx, created.Label, space)
assert.NoError(s.T(), err)
assert.Equal(s.T(), created, get)
- err = pAPI.Delete(created.Id, space)
+ err = pAPI.Delete(ctx, created.Id, space)
assert.NoError(s.T(), err)
- _, err = pAPI.Get(created.Id, space)
+ _, err = pAPI.Get(ctx, created.Id, space)
assert.Error(s.T(), err)
})
})
@@ -266,10 +351,11 @@ func (s *KBAPITestSuite) TestKibanaSyntheticsPrivateLocationNotFound() {
pAPI := s.API.KibanaSynthetics.PrivateLocation
ids := []string{"", "not-found", testUuid}
+ ctx := context.Background()
for _, id := range ids {
s.Run(fmt.Sprintf("TestKibanaSyntheticsPrivateLocationNotFound - %s - %s", n, id), func() {
- _, err := pAPI.Get(id, space)
+ _, err := pAPI.Get(ctx, id, space)
assert.Error(s.T(), err)
assert.IsType(s.T(), APIError{}, err)
assert.Equal(s.T(), 404, err.(APIError).Code)
diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go
index dceb73adf..305d81324 100644
--- a/provider/plugin_framework.go
+++ b/provider/plugin_framework.go
@@ -8,6 +8,7 @@ import (
"github.com/elastic/terraform-provider-elasticstack/internal/kibana/data_view"
"github.com/elastic/terraform-provider-elasticstack/internal/kibana/import_saved_objects"
"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/private_location"
"github.com/elastic/terraform-provider-elasticstack/internal/schema"
"github.com/hashicorp/terraform-plugin-framework/datasource"
@@ -76,5 +77,6 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource {
func() resource.Resource { return &import_saved_objects.Resource{} },
func() resource.Resource { return &data_view.Resource{} },
func() resource.Resource { return &private_location.Resource{} },
+ func() resource.Resource { return &synthetics.Resource{} },
}
}
diff --git a/templates/resources/kibana_synthetics_monitor.md.tmpl b/templates/resources/kibana_synthetics_monitor.md.tmpl
new file mode 100644
index 000000000..8700aa561
--- /dev/null
+++ b/templates/resources/kibana_synthetics_monitor.md.tmpl
@@ -0,0 +1,37 @@
+---
+subcategory: "Kibana"
+layout: ""
+page_title: "Elasticstack: elasticstack_kibana_synthetics_monitor Resource"
+description: |-
+ Creates or updates a Kibana synthetics monitor.
+---
+
+# Resource: elasticstack_kibana_synthetics_monitor
+
+Creates or updates a Kibana synthetics monitor.
+See [API docs](https://www.elastic.co/guide/en/kibana/current/add-monitor-api.html)
+
+## Supported monitor types
+ * `http`
+ * `tcp`
+
+**NOTE:** Due-to nature of partial update API, reset values to defaults is not supported.
+In case you would like to reset an optional monitor value, please set it explicitly or delete and create new monitor.
+
+
+## Example Usage
+
+{{ tffile "examples/resources/elasticstack_kibana_synthetics_monitor/resource.tf" }}
+
+{{ .SchemaMarkdown | trimspace }}
+
+## Import
+
+Import is supported using the following syntax:
+
+{{ codefile "shell" "examples/resources/elasticstack_kibana_synthetics_monitor/import.sh" }}
+
+**NOTE:** Not all monitor fields are supported during the import due-to API limitation.
+Full field support could be implemented after this [kibana issue](https://github.com/elastic/kibana/issues/189906) is resolved.
+
+Currently not supported fields during the import: `params`, `retest_on_failure`, `http.proxy_header`, `http.username`, `http.password`, `http.check`, `http.response`, `tcp.check_send`, `tcp.check_receive`