diff --git a/docs/resources/elasticsearch_ml_job_state.md b/docs/resources/elasticsearch_ml_job_state.md new file mode 100644 index 000000000..7e586c7ce --- /dev/null +++ b/docs/resources/elasticsearch_ml_job_state.md @@ -0,0 +1,145 @@ +--- +page_title: "elasticstack_elasticsearch_ml_job_state Resource - terraform-provider-elasticstack" +subcategory: "" +description: |- + ML Job State Resource + Manages the state of an Elasticsearch Machine Learning (ML) job, allowing you to open or close ML jobs. + This resource uses the following Elasticsearch APIs: + Open ML Job API https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-open-job.htmlClose ML Job API https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-close-job.htmlGet ML Job Stats API https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-job-stats.html + Important Notes + This resource manages the state of an existing ML job, not the job configuration itself.The ML job must already exist before using this resource.Opening a job allows it to receive and process data.Closing a job stops data processing and frees up resources.Jobs can be opened and closed multiple times throughout their lifecycle. +--- + +# elasticstack_elasticsearch_ml_job_state (Resource) + +# ML Job State Resource + +Manages the state of an Elasticsearch Machine Learning (ML) job, allowing you to open or close ML jobs. + +This resource uses the following Elasticsearch APIs: +- [Open ML Job API](https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-open-job.html) +- [Close ML Job API](https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-close-job.html) +- [Get ML Job Stats API](https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-job-stats.html) + +## Important Notes + +- This resource manages the **state** of an existing ML job, not the job configuration itself. +- The ML job must already exist before using this resource. +- Opening a job allows it to receive and process data. +- Closing a job stops data processing and frees up resources. +- Jobs can be opened and closed multiple times throughout their lifecycle. + +## Example Usage + +```terraform +provider "elasticstack" { + elasticsearch {} +} + +# First create an ML anomaly detection job +resource "elasticstack_elasticsearch_ml_anomaly_detector" "example" { + job_id = "example-ml-job" + description = "Example anomaly detection job" + + analysis_config = { + bucket_span = "15m" + detectors = [ + { + function = "count" + detector_description = "Count detector" + } + ] + } + + data_description = { + time_field = "@timestamp" + time_format = "epoch_ms" + } +} + +# Manage the state of the ML job - open it +resource "elasticstack_elasticsearch_ml_job_state" "example" { + job_id = elasticstack_elasticsearch_ml_anomaly_detector.example.job_id + state = "opened" + + # Optional settings + force = false + job_timeout = "30s" + + # Timeouts for asynchronous operations + timeouts { + create = "5m" + update = "5m" + } + + depends_on = [elasticstack_elasticsearch_ml_anomaly_detector.example] +} + +# Example with different configuration options +resource "elasticstack_elasticsearch_ml_job_state" "example_with_options" { + job_id = elasticstack_elasticsearch_ml_anomaly_detector.example.job_id + state = "closed" + + # Use force close for quicker shutdown + force = true + + # Custom timeout + job_timeout = "2m" + + # Custom timeouts for asynchronous operations + timeouts { + create = "10m" + update = "3m" + } + + depends_on = [elasticstack_elasticsearch_ml_anomaly_detector.example] +} +``` + + +## Schema + +### Required + +- `job_id` (String) Identifier for the anomaly detection job. +- `state` (String) The desired state for the ML job. Valid values are `opened` and `closed`. + +### Optional + +- `elasticsearch_connection` (Block List, Deprecated) Elasticsearch connection configuration block. (see [below for nested schema](#nestedblock--elasticsearch_connection)) +- `force` (Boolean) When closing a job, use to forcefully close it. This method is quicker but can miss important clean up tasks. +- `job_timeout` (String) Timeout for the operation. Examples: `30s`, `5m`, `1h`. Default is `30s`. +- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) + +### Read-Only + +- `id` (String) Internal identifier of the resource + + +### Nested Schema for `elasticsearch_connection` + +Optional: + +- `api_key` (String, Sensitive) API Key to use for authentication to Elasticsearch +- `bearer_token` (String, Sensitive) Bearer Token to use for authentication to Elasticsearch +- `ca_data` (String) PEM-encoded custom Certificate Authority certificate +- `ca_file` (String) Path to a custom Certificate Authority certificate +- `cert_data` (String) PEM encoded certificate for client auth +- `cert_file` (String) Path to a file containing the PEM encoded certificate for client auth +- `endpoints` (List of String, Sensitive) A list of endpoints where the terraform provider will point to, this must include the http(s) schema and port number. +- `es_client_authentication` (String, Sensitive) ES Client Authentication field to be used with the JWT token +- `headers` (Map of String, Sensitive) A list of headers to be sent with each request to Elasticsearch. +- `insecure` (Boolean) Disable TLS certificate validation +- `key_data` (String, Sensitive) PEM encoded private key for client auth +- `key_file` (String) Path to a file containing the PEM encoded private key for client auth +- `password` (String, Sensitive) Password to use for API authentication to Elasticsearch. +- `username` (String) Username to use for API authentication to Elasticsearch. + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). \ No newline at end of file diff --git a/examples/resources/elasticstack_elasticsearch_ml_job_state/resource.tf b/examples/resources/elasticstack_elasticsearch_ml_job_state/resource.tf new file mode 100644 index 000000000..1bfc44c00 --- /dev/null +++ b/examples/resources/elasticstack_elasticsearch_ml_job_state/resource.tf @@ -0,0 +1,62 @@ +provider "elasticstack" { + elasticsearch {} +} + +# First create an ML anomaly detection job +resource "elasticstack_elasticsearch_ml_anomaly_detector" "example" { + job_id = "example-ml-job" + description = "Example anomaly detection job" + + analysis_config = { + bucket_span = "15m" + detectors = [ + { + function = "count" + detector_description = "Count detector" + } + ] + } + + data_description = { + time_field = "@timestamp" + time_format = "epoch_ms" + } +} + +# Manage the state of the ML job - open it +resource "elasticstack_elasticsearch_ml_job_state" "example" { + job_id = elasticstack_elasticsearch_ml_anomaly_detector.example.job_id + state = "opened" + + # Optional settings + force = false + job_timeout = "30s" + + # Timeouts for asynchronous operations + timeouts { + create = "5m" + update = "5m" + } + + depends_on = [elasticstack_elasticsearch_ml_anomaly_detector.example] +} + +# Example with different configuration options +resource "elasticstack_elasticsearch_ml_job_state" "example_with_options" { + job_id = elasticstack_elasticsearch_ml_anomaly_detector.example.job_id + state = "closed" + + # Use force close for quicker shutdown + force = true + + # Custom timeout + job_timeout = "2m" + + # Custom timeouts for asynchronous operations + timeouts { + create = "10m" + update = "3m" + } + + depends_on = [elasticstack_elasticsearch_ml_anomaly_detector.example] +} \ No newline at end of file diff --git a/go.mod b/go.mod index a553bf000..b58bbd729 100644 --- a/go.mod +++ b/go.mod @@ -11,10 +11,11 @@ require ( github.com/hashicorp/go-cty v1.5.0 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 - github.com/hashicorp/terraform-plugin-framework v1.15.1 + github.com/hashicorp/terraform-plugin-framework v1.16.0 github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0 + github.com/hashicorp/terraform-plugin-framework-timeouts v0.6.0 github.com/hashicorp/terraform-plugin-framework-validators v0.18.0 - github.com/hashicorp/terraform-plugin-go v0.28.0 + github.com/hashicorp/terraform-plugin-go v0.29.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-mux v0.20.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 @@ -27,11 +28,11 @@ require ( require ( al.essio.dev/pkg/shellescape v1.6.0 // indirect - cel.dev/expr v0.22.1 // indirect + cel.dev/expr v0.24.0 // indirect cloud.google.com/go v0.120.0 // indirect cloud.google.com/go/auth v0.15.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect cloud.google.com/go/iam v1.4.2 // indirect cloud.google.com/go/kms v1.21.1 // indirect cloud.google.com/go/longrunning v0.6.6 // indirect @@ -59,7 +60,7 @@ require ( github.com/Azure/go-autorest/tracing v0.6.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect github.com/BurntSushi/toml v1.5.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect @@ -131,7 +132,7 @@ require ( github.com/charmbracelet/x/term v0.2.1 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect github.com/cloudflare/circl v1.6.1 // indirect - github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect + github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect @@ -170,8 +171,8 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-git/v5 v5.14.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.23.0 // indirect github.com/go-openapi/errors v0.22.1 // indirect @@ -216,7 +217,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.6.3 // indirect + github.com/hashicorp/go-plugin v1.7.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/hc-install v0.9.2 // indirect @@ -225,9 +226,9 @@ require ( github.com/hashicorp/terraform-exec v0.23.0 // indirect github.com/hashicorp/terraform-json v0.25.0 // indirect github.com/hashicorp/terraform-plugin-docs v0.22.0 // indirect - github.com/hashicorp/terraform-registry-address v0.2.5 // indirect + github.com/hashicorp/terraform-registry-address v0.4.0 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/in-toto/attestation v1.1.1 // indirect github.com/in-toto/in-toto-golang v0.9.0 // indirect @@ -361,14 +362,14 @@ require ( go.mongodb.org/mongo-driver v1.17.3 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.35.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect @@ -389,10 +390,10 @@ require ( google.golang.org/api v0.228.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a // indirect - google.golang.org/grpc v1.72.1 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/grpc v1.75.1 // indirect + google.golang.org/protobuf v1.36.9 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index 101f85864..e1f50eed3 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= -cel.dev/expr v0.22.1 h1:xoFEsNh972Yzey8N9TCPx2nDvMN7TMhQEzxLuj/iRrI= -cel.dev/expr v0.22.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= @@ -9,8 +9,8 @@ cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/iam v1.4.2 h1:4AckGYAYsowXeHzsn/LCKWIwSWLkdb0eGjH8wWkd27Q= cloud.google.com/go/iam v1.4.2/go.mod h1:REGlrt8vSlh4dfCJfSEcNjLGq75wW75c5aU3FLOYq34= cloud.google.com/go/kms v1.21.1 h1:r1Auo+jlfJSf8B7mUnVw5K0fI7jWyoUy65bV53VjKyk= @@ -98,8 +98,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= @@ -230,8 +230,8 @@ github.com/bluesky-social/indigo v0.0.0-20240813042137-4006c0eca043/go.mod h1:dX github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/bufbuild/protocompile v0.10.0 h1:+jW/wnLMLxaCEG8AX9lD0bQ5v9h1RUiMKOBOT5ll9dM= -github.com/bufbuild/protocompile v0.10.0/go.mod h1:G9qQIQo0xZ6Uyj6CMNz0saGmx2so+KONo8/KrELABiY= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/caarlos0/ctrlc v1.2.0 h1:AtbThhmbeYx1WW3WXdWrd94EHKi+0NPRGS4/4pzrjwk= @@ -282,8 +282,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= -github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= @@ -407,11 +407,11 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= -github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= -github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= +github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= @@ -572,8 +572,8 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= -github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= @@ -607,14 +607,16 @@ github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGo github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= github.com/hashicorp/terraform-plugin-docs v0.22.0 h1:fwIDStbFel1PPNkM+mDPnpB4efHZBdGoMz/zt5FbTDw= github.com/hashicorp/terraform-plugin-docs v0.22.0/go.mod h1:55DJVyZ7BNK4t/lANcQ1YpemRuS6KsvIO1BbGA+xzGE= -github.com/hashicorp/terraform-plugin-framework v1.15.1 h1:2mKDkwb8rlx/tvJTlIcpw0ykcmvdWv+4gY3SIgk8Pq8= -github.com/hashicorp/terraform-plugin-framework v1.15.1/go.mod h1:hxrNI/GY32KPISpWqlCoTLM9JZsGH3CyYlir09bD/fI= +github.com/hashicorp/terraform-plugin-framework v1.16.0 h1:tP0f+yJg0Z672e7levixDe5EpWwrTrNryPM9kDMYIpE= +github.com/hashicorp/terraform-plugin-framework v1.16.0/go.mod h1:0xFOxLy5lRzDTayc4dzK/FakIgBhNf/lC4499R9cV4Y= github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0 h1:SJXL5FfJJm17554Kpt9jFXngdM6fXbnUnZ6iT2IeiYA= github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0/go.mod h1:p0phD0IYhsu9bR4+6OetVvvH59I6LwjXGnTVEr8ox6E= +github.com/hashicorp/terraform-plugin-framework-timeouts v0.6.0 h1:Vv16e7EW4nT9668IV0RhdpEmnLl0im7BZx6J+QMlUkg= +github.com/hashicorp/terraform-plugin-framework-timeouts v0.6.0/go.mod h1:rpHo9hZLn4vEkvNL5xsSdLRdaDZKSinuc0xL+BdOpVA= github.com/hashicorp/terraform-plugin-framework-validators v0.18.0 h1:OQnlOt98ua//rCw+QhBbSqfW3QbwtVrcdWeQN5gI3Hw= github.com/hashicorp/terraform-plugin-framework-validators v0.18.0/go.mod h1:lZvZvagw5hsJwuY7mAY6KUz45/U6fiDR0CzQAwWD0CA= -github.com/hashicorp/terraform-plugin-go v0.28.0 h1:zJmu2UDwhVN0J+J20RE5huiF3XXlTYVIleaevHZgKPA= -github.com/hashicorp/terraform-plugin-go v0.28.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o= +github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU= +github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-mux v0.20.0 h1:3QpBnI9uCuL0Yy2Rq/kR9cOdmOFNhw88A2GoZtk5aXM= @@ -623,14 +625,14 @@ github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsor github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0/go.mod h1:QYmYnLfsosrxjCnGY1p9c7Zj6n9thnEE+7RObeYs3fA= github.com/hashicorp/terraform-plugin-testing v1.13.3 h1:QLi/khB8Z0a5L54AfPrHukFpnwsGL8cwwswj4RZduCo= github.com/hashicorp/terraform-plugin-testing v1.13.3/go.mod h1:WHQ9FDdiLoneey2/QHpGM/6SAYf4A7AZazVg7230pLE= -github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwDkRe4pcBDq0uuK5VJngU1M= -github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg= +github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= +github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= -github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= -github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -693,8 +695,8 @@ github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 h1:FWpSWRD8Fb github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7/go.mod h1:BMxO138bOokdgt4UaxZiEfypcSHX0t6SIFimVP1oRfk= github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc= github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw= -github.com/jhump/protoreflect v1.16.0 h1:54fZg+49widqXYQ0b+usAFHbMkBGR4PpXrsHc8+TBDg= -github.com/jhump/protoreflect v1.16.0/go.mod h1:oYPd7nPvcBw/5wlDfm/AVmU9zH9BgqGCI469pGxfj/8= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -1080,16 +1082,16 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= -go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= -go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= @@ -1114,16 +1116,16 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsu go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.step.sm/crypto v0.60.0 h1:UgSw8DFG5xUOGB3GUID17UA32G4j1iNQ4qoMhBmsVFw= @@ -1304,6 +1306,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs= google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -1315,17 +1319,17 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 h1:qEFnJI6AnfZk0NNe8YTyXQh5i//Zxi4gBHwRgp76qpw= google.golang.org/genproto v0.0.0-20250324211829-b45e905df463/go.mod h1:SqIx1NV9hcvqdLHo7uNZDS5lrUJybQ3evo3+z/WBfA0= -google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a h1:OQ7sHVzkx6L57dQpzUS4ckfWJ51KDH74XHTDe23xWAs= -google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a h1:GIqLhp/cYUkuGuiT+vJk8vhOP86L4+SP5j8yXgeVpvI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1337,8 +1341,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/clients/elasticsearch/ml_job.go b/internal/clients/elasticsearch/ml_job.go new file mode 100644 index 000000000..d03ac6225 --- /dev/null +++ b/internal/clients/elasticsearch/ml_job.go @@ -0,0 +1,138 @@ +package elasticsearch + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/elastic/go-elasticsearch/v8/esapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/diagutil" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +// MLJobStats represents the statistics structure for an ML job +type MLJobStats struct { + Jobs []MLJob `json:"jobs"` +} + +// MLJob represents a single ML job in the stats response +type MLJob struct { + JobId string `json:"job_id"` + State string `json:"state"` + Node *MLJobNode `json:"node,omitempty"` +} + +// MLJobNode represents the node information for an ML job +type MLJobNode struct { + Id string `json:"id"` + Name string `json:"name"` + Attributes map[string]interface{} `json:"attributes"` +} + +// OpenMLJob opens a machine learning job +func OpenMLJob(ctx context.Context, apiClient *clients.ApiClient, jobId string) diag.Diagnostics { + var diags diag.Diagnostics + + esClient, err := apiClient.GetESClient() + if err != nil { + diags.AddError("Failed to get Elasticsearch client", err.Error()) + return diags + } + + res, err := esClient.ML.OpenJob(jobId, esClient.ML.OpenJob.WithContext(ctx)) + if err != nil { + diags.AddError("Failed to open ML job", err.Error()) + return diags + } + defer res.Body.Close() + + fwDiags := diagutil.CheckErrorFromFW(res, fmt.Sprintf("Unable to open ML job: %s", jobId)) + diags.Append(fwDiags...) + + return diags +} + +// CloseMLJob closes a machine learning job +func CloseMLJob(ctx context.Context, apiClient *clients.ApiClient, jobId string, force bool, timeout time.Duration) diag.Diagnostics { + var diags diag.Diagnostics + + esClient, err := apiClient.GetESClient() + if err != nil { + diags.AddError("Failed to get Elasticsearch client", err.Error()) + return diags + } + + options := []func(*esapi.MLCloseJobRequest){ + esClient.ML.CloseJob.WithContext(ctx), + esClient.ML.CloseJob.WithForce(force), + esClient.ML.CloseJob.WithAllowNoMatch(true), + } + + if timeout > 0 { + options = append(options, esClient.ML.CloseJob.WithTimeout(timeout)) + } + + res, err := esClient.ML.CloseJob(jobId, options...) + if err != nil { + diags.AddError("Failed to close ML job", err.Error()) + return diags + } + defer res.Body.Close() + + fwDiags := diagutil.CheckErrorFromFW(res, fmt.Sprintf("Unable to close ML job: %s", jobId)) + diags.Append(fwDiags...) + + return diags +} + +// GetMLJobStats retrieves the stats for a specific machine learning job +func GetMLJobStats(ctx context.Context, apiClient *clients.ApiClient, jobId string) (*MLJob, diag.Diagnostics) { + var diags diag.Diagnostics + + esClient, err := apiClient.GetESClient() + if err != nil { + diags.AddError("Failed to get Elasticsearch client", err.Error()) + return nil, diags + } + + options := []func(*esapi.MLGetJobStatsRequest){ + esClient.ML.GetJobStats.WithContext(ctx), + esClient.ML.GetJobStats.WithJobID(jobId), + esClient.ML.GetJobStats.WithAllowNoMatch(true), + } + + res, err := esClient.ML.GetJobStats(options...) + if err != nil { + diags.AddError("Failed to get ML job stats", err.Error()) + return nil, diags + } + defer res.Body.Close() + + if res.StatusCode == http.StatusNotFound { + return nil, diags + } + + if fwDiags := diagutil.CheckErrorFromFW(res, fmt.Sprintf("Unable to get ML job stats: %s", jobId)); fwDiags.HasError() { + diags.Append(fwDiags...) + return nil, diags + } + + var jobStats MLJobStats + if err := json.NewDecoder(res.Body).Decode(&jobStats); err != nil { + diags.AddError("Failed to decode ML job stats response", err.Error()) + return nil, diags + } + + // Find the specific job in the response + for _, job := range jobStats.Jobs { + if job.JobId == jobId { + return &job, diags + } + } + + // Job not found in response + return nil, diags +} diff --git a/internal/elasticsearch/ml/job_state/acc_test.go b/internal/elasticsearch/ml/job_state/acc_test.go new file mode 100644 index 000000000..dd95da6b9 --- /dev/null +++ b/internal/elasticsearch/ml/job_state/acc_test.go @@ -0,0 +1,192 @@ +package job_state_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccResourceMLJobState(t *testing.T) { + jobID := fmt.Sprintf("test-ml-job-state-%s", sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccResourceMLJobStateConfig(jobID, "opened"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "job_id", jobID), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "state", "opened"), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "force", "false"), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "job_timeout", "30s"), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_ml_job_state.test", "id"), + // Verify that the ML job was created by the anomaly detector resource + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_anomaly_detector.test", "job_id", jobID), + ), + }, + { + Config: testAccResourceMLJobStateConfig(jobID, "closed"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "job_id", jobID), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "state", "closed"), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "force", "false"), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "job_timeout", "30s"), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_ml_job_state.test", "id"), + ), + }, + { + Config: testAccResourceMLJobStateConfigWithOptions(jobID, "opened", true, "1m"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "job_id", jobID), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "state", "opened"), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "force", "true"), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "force", "true"), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "job_timeout", "1m"), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_ml_job_state.test", "id"), + ), + }, + }, + }) +} + +func TestAccResourceMLJobStateNonExistent(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccResourceMLJobStateNonExistent, + ExpectError: regexp.MustCompile(`ML job .* does not exist`), + }, + }, + }) +} + +func TestAccResourceMLJobStateImport(t *testing.T) { + jobID := fmt.Sprintf("test-ml-job-state-import-%s", sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccResourceMLJobStateConfig(jobID, "opened"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "job_id", jobID), + resource.TestCheckResourceAttr("elasticstack_elasticsearch_ml_job_state.test", "state", "opened"), + ), + }, + { + ResourceName: "elasticstack_elasticsearch_ml_job_state.test", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs := s.RootModule().Resources["elasticstack_elasticsearch_ml_job_state.test"] + return rs.Primary.ID, nil + }, + }, + }, + }) +} + +func testAccResourceMLJobStateConfig(jobID, state string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} +} + +# First create an ML anomaly detection job +resource "elasticstack_elasticsearch_ml_anomaly_detector" "test" { + job_id = "%s" + description = "Test anomaly detection job for state management" + + analysis_config = { + bucket_span = "15m" + detectors = [ + { + function = "count" + detector_description = "Count detector" + } + ] + } + + analysis_limits = { + model_memory_limit = "100mb" + } + + data_description = { + time_field = "@timestamp" + time_format = "epoch_ms" + } +} + +# Then manage the state of that ML job +resource "elasticstack_elasticsearch_ml_job_state" "test" { + job_id = elasticstack_elasticsearch_ml_anomaly_detector.test.job_id + state = "%s" + + depends_on = [elasticstack_elasticsearch_ml_anomaly_detector.test] +} +`, jobID, state) +} + +func testAccResourceMLJobStateConfigWithOptions(jobID, state string, force bool, timeout string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} +} + +# First create an ML anomaly detection job +resource "elasticstack_elasticsearch_ml_anomaly_detector" "test" { + job_id = "%s" + description = "Test anomaly detection job for state management with options" + + analysis_config = { + bucket_span = "15m" + detectors = [ + { + function = "count" + detector_description = "Count detector" + } + ] + } + + analysis_limits = { + model_memory_limit = "100mb" + } + + data_description = { + time_field = "@timestamp" + time_format = "epoch_ms" + } +} + +# Then manage the state of that ML job with custom options +resource "elasticstack_elasticsearch_ml_job_state" "test" { + job_id = elasticstack_elasticsearch_ml_anomaly_detector.test.job_id + state = "%s" + force = %t + job_timeout = "%s" + + depends_on = [elasticstack_elasticsearch_ml_anomaly_detector.test] +} +`, jobID, state, force, timeout) +} + +const testAccResourceMLJobStateNonExistent = ` +provider "elasticstack" { + elasticsearch {} +} + +# Try to manage state of a non-existent ML job +resource "elasticstack_elasticsearch_ml_job_state" "test" { + job_id = "non-existent-ml-job" + state = "opened" +} +` diff --git a/internal/elasticsearch/ml/job_state/create.go b/internal/elasticsearch/ml/job_state/create.go new file mode 100644 index 000000000..10e7914cd --- /dev/null +++ b/internal/elasticsearch/ml/job_state/create.go @@ -0,0 +1,27 @@ +package job_state + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *mlJobStateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data MLJobStateData + diags := req.Plan.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get create timeout + createTimeout, fwDiags := data.Timeouts.Create(ctx, 5*time.Minute) // Default 5 minutes + resp.Diagnostics.Append(fwDiags...) + if resp.Diagnostics.HasError() { + return + } + + diags = r.update(ctx, req.Plan, &resp.State, createTimeout) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/elasticsearch/ml/job_state/delete.go b/internal/elasticsearch/ml/job_state/delete.go new file mode 100644 index 000000000..fa8457cc6 --- /dev/null +++ b/internal/elasticsearch/ml/job_state/delete.go @@ -0,0 +1,21 @@ +package job_state + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func (r *mlJobStateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // ML job state resource only manages the state, not the job itself. + // When the resource is deleted, we simply remove it from Terraform state + // without affecting the actual ML job state in Elasticsearch. + // The job will remain in its current state (opened or closed). + var jobId basetypes.StringValue + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("job_id"), &jobId)...) + tflog.Info(ctx, fmt.Sprintf(`Dropping ML job state "%s", this does not close the job`, jobId.ValueString())) +} diff --git a/internal/elasticsearch/ml/job_state/models.go b/internal/elasticsearch/ml/job_state/models.go new file mode 100644 index 000000000..fd9638343 --- /dev/null +++ b/internal/elasticsearch/ml/job_state/models.go @@ -0,0 +1,36 @@ +package job_state + +import ( + "github.com/elastic/terraform-provider-elasticstack/internal/utils/customtypes" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type MLJobStateData struct { + Id types.String `tfsdk:"id"` + ElasticsearchConnection types.List `tfsdk:"elasticsearch_connection"` + JobId types.String `tfsdk:"job_id"` + State types.String `tfsdk:"state"` + Force types.Bool `tfsdk:"force"` + Timeout customtypes.Duration `tfsdk:"job_timeout"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +// MLJobStats represents the statistics structure for an ML job +type MLJobStats struct { + Jobs []MLJob `json:"jobs"` +} + +// MLJob represents a single ML job in the stats response +type MLJob struct { + JobId string `json:"job_id"` + State string `json:"state"` + Node *MLJobNode `json:"node,omitempty"` +} + +// MLJobNode represents the node information for an ML job +type MLJobNode struct { + Id string `json:"id"` + Name string `json:"name"` + Attributes map[string]interface{} `json:"attributes"` +} diff --git a/internal/elasticsearch/ml/job_state/read.go b/internal/elasticsearch/ml/job_state/read.go new file mode 100644 index 000000000..63c49227c --- /dev/null +++ b/internal/elasticsearch/ml/job_state/read.go @@ -0,0 +1,61 @@ +package job_state + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch" + "github.com/elastic/terraform-provider-elasticstack/internal/utils/customtypes" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func (r *mlJobStateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data MLJobStateData + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + compId, diags := clients.CompositeIdFromStrFw(data.Id.ValueString()) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + jobId := compId.ResourceId + + client, diags := clients.MaybeNewApiClientFromFrameworkResource(ctx, data.ElasticsearchConnection, r.client) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get job stats to check current state + currentJob, fwDiags := elasticsearch.GetMLJobStats(ctx, client, jobId) + resp.Diagnostics.Append(fwDiags...) + if resp.Diagnostics.HasError() { + return + } + + if currentJob == nil { + tflog.Warn(ctx, fmt.Sprintf(`ML job "%s" not found, removing from state`, jobId)) + resp.State.RemoveResource(ctx) + return + } + + // Update the state with current job information + data.JobId = types.StringValue(jobId) + data.State = types.StringValue(currentJob.State) + + // Set defaults for computed attributes if they're not already set (e.g., during import) + if data.Force.IsNull() { + data.Force = types.BoolValue(false) + } + if data.Timeout.IsNull() { + data.Timeout = customtypes.NewDurationValue("30s") + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/elasticsearch/ml/job_state/resource-description.md b/internal/elasticsearch/ml/job_state/resource-description.md new file mode 100644 index 000000000..6da15e32a --- /dev/null +++ b/internal/elasticsearch/ml/job_state/resource-description.md @@ -0,0 +1,16 @@ +# ML Job State Resource + +Manages the state of an Elasticsearch Machine Learning (ML) job, allowing you to open or close ML jobs. + +This resource uses the following Elasticsearch APIs: +- [Open ML Job API](https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-open-job.html) +- [Close ML Job API](https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-close-job.html) +- [Get ML Job Stats API](https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-job-stats.html) + +## Important Notes + +- This resource manages the **state** of an existing ML job, not the job configuration itself. +- The ML job must already exist before using this resource. +- Opening a job allows it to receive and process data. +- Closing a job stops data processing and frees up resources. +- Jobs can be opened and closed multiple times throughout their lifecycle. \ No newline at end of file diff --git a/internal/elasticsearch/ml/job_state/resource.go b/internal/elasticsearch/ml/job_state/resource.go new file mode 100644 index 000000000..0067a7689 --- /dev/null +++ b/internal/elasticsearch/ml/job_state/resource.go @@ -0,0 +1,32 @@ +package job_state + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func NewMLJobStateResource() resource.Resource { + return &mlJobStateResource{} +} + +type mlJobStateResource struct { + client *clients.ApiClient +} + +func (r *mlJobStateResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_elasticsearch_ml_job_state" +} + +func (r *mlJobStateResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + client, diags := clients.ConvertProviderData(req.ProviderData) + resp.Diagnostics.Append(diags...) + r.client = client +} + +func (r *mlJobStateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to id attribute + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/internal/elasticsearch/ml/job_state/schema.go b/internal/elasticsearch/ml/job_state/schema.go new file mode 100644 index 000000000..a5ddb67d2 --- /dev/null +++ b/internal/elasticsearch/ml/job_state/schema.go @@ -0,0 +1,80 @@ +package job_state + +import ( + "context" + _ "embed" + "regexp" + + "github.com/elastic/terraform-provider-elasticstack/internal/utils/customtypes" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + providerschema "github.com/elastic/terraform-provider-elasticstack/internal/schema" +) + +//go:embed resource-description.md +var mlJobStateResourceDescription string + +func (r *mlJobStateResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = GetSchema() +} + +func GetSchema() schema.Schema { + return schema.Schema{ + MarkdownDescription: mlJobStateResourceDescription, + Blocks: map[string]schema.Block{ + "elasticsearch_connection": providerschema.GetEsFWConnectionBlock("elasticsearch_connection", false), + }, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Internal identifier of the resource", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "job_id": schema.StringAttribute{ + MarkdownDescription: "Identifier for the anomaly detection job.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 64), + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-zA-Z0-9_-]+$`), "must contain only alphanumeric characters, hyphens, and underscores"), + }, + }, + "state": schema.StringAttribute{ + MarkdownDescription: "The desired state for the ML job. Valid values are `opened` and `closed`.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("opened", "closed"), + }, + }, + "force": schema.BoolAttribute{ + MarkdownDescription: "When closing a job, use to forcefully close it. This method is quicker but can miss important clean up tasks.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "job_timeout": schema.StringAttribute{ + MarkdownDescription: "Timeout for the operation. Examples: `30s`, `5m`, `1h`. Default is `30s`.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("30s"), + CustomType: customtypes.DurationType{}, + }, + "timeouts": timeouts.Attributes(context.Background(), timeouts.Opts{ + Create: true, + Update: true, + }), + }, + } +} diff --git a/internal/elasticsearch/ml/job_state/update.go b/internal/elasticsearch/ml/job_state/update.go new file mode 100644 index 000000000..d52699ac0 --- /dev/null +++ b/internal/elasticsearch/ml/job_state/update.go @@ -0,0 +1,181 @@ +package job_state + +import ( + "context" + "fmt" + "time" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func (r *mlJobStateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data MLJobStateData + diags := req.Plan.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get update timeout + updateTimeout, fwDiags := data.Timeouts.Update(ctx, 5*time.Minute) // Default 5 minutes + resp.Diagnostics.Append(fwDiags...) + if resp.Diagnostics.HasError() { + return + } + + diags = r.update(ctx, req.Plan, &resp.State, updateTimeout) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *mlJobStateResource) update(ctx context.Context, plan tfsdk.Plan, state *tfsdk.State, operationTimeout time.Duration) diag.Diagnostics { + var data MLJobStateData + diags := plan.Get(ctx, &data) + if diags.HasError() { + return diags + } + + client, fwDiags := clients.MaybeNewApiClientFromFrameworkResource(ctx, data.ElasticsearchConnection, r.client) + diags.Append(fwDiags...) + if diags.HasError() { + return diags + } + + jobId := data.JobId.ValueString() + desiredState := data.State.ValueString() + + // First, get the current job stats to check if the job exists and its current state + currentJob, fwDiags := elasticsearch.GetMLJobStats(ctx, client, jobId) + diags.Append(fwDiags...) + if diags.HasError() { + return diags + } + + if currentJob == nil { + diags.AddError( + "ML Job not found", + fmt.Sprintf("ML job %s does not exist", jobId), + ) + return diags + } + + currentState := currentJob.State + + // Perform state transition if needed + fwDiags = r.performStateTransition(ctx, client, data, currentState, operationTimeout) + diags.Append(fwDiags...) + if diags.HasError() { + return diags + } + + // Generate composite ID + compId, sdkDiags := client.ID(ctx, jobId) + if len(sdkDiags) > 0 { + for _, d := range sdkDiags { + diags.AddError(d.Summary, d.Detail) + } + return diags + } + + // Set the response state + data.Id = types.StringValue(compId.String()) + data.JobId = types.StringValue(jobId) + data.State = types.StringValue(desiredState) + + diags.Append(state.Set(ctx, data)...) + return diags +} + +// waitForJobStateTransition waits for an ML job to reach the desired state +func waitForJobStateTransition(ctx context.Context, client *clients.ApiClient, jobId, desiredState string) error { + const pollInterval = 2 * time.Second + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + currentJob, fwDiags := elasticsearch.GetMLJobStats(ctx, client, jobId) + if fwDiags.HasError() { + return fmt.Errorf("failed to get job stats during state transition check") + } + + if currentJob == nil { + return fmt.Errorf("job not found during state transition check") + } + + if currentJob.State == desiredState { + return nil // Successfully reached desired state + } + tflog.Debug(ctx, fmt.Sprintf("ML job %s current state: %s, waiting for: %s", jobId, currentJob.State, desiredState)) + } + } +} + +// performStateTransition handles the ML job state transition process +func (r *mlJobStateResource) performStateTransition(ctx context.Context, client *clients.ApiClient, data MLJobStateData, currentState string, operationTimeout time.Duration) diag.Diagnostics { + jobId := data.JobId.ValueString() + desiredState := data.State.ValueString() + force := data.Force.ValueBool() + + // Parse timeout duration + timeout, parseErrs := data.Timeout.Parse() + if parseErrs.HasError() { + return parseErrs + } + + // Return early if no state change is needed + if currentState == desiredState { + tflog.Debug(ctx, fmt.Sprintf("ML job %s is already in desired state %s", jobId, desiredState)) + return nil + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(ctx, operationTimeout) + defer cancel() + + // Initiate the state change + switch desiredState { + case "opened": + diags := elasticsearch.OpenMLJob(ctx, client, jobId) + if diags.HasError() { + return diags + } + case "closed": + diags := elasticsearch.CloseMLJob(ctx, client, jobId, force, timeout) // Always allow no match + if diags.HasError() { + return diags + } + default: + return diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid state", + fmt.Sprintf("Invalid state %s. Valid states are 'opened' and 'closed'", desiredState), + ), + } + } + + // Wait for state transition to complete + err := waitForJobStateTransition(ctx, client, jobId, desiredState) + if err != nil { + return diag.Diagnostics{ + diag.NewErrorDiagnostic( + "State transition timeout", + fmt.Sprintf("ML job %s did not transition to state %s within timeout: %s", jobId, desiredState, err.Error()), + ), + } + } + + tflog.Info(ctx, fmt.Sprintf("ML job %s successfully transitioned to state %s", jobId, desiredState)) + return nil +} diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go index bba736241..2c96cab21 100644 --- a/provider/plugin_framework.go +++ b/provider/plugin_framework.go @@ -10,6 +10,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/data_stream_lifecycle" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/index" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/indices" + "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/ml/job_state" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/api_key" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/role_mapping" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/system_user" @@ -116,5 +117,6 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { maintenance_window.NewResource, enrich.NewEnrichPolicyResource, role_mapping.NewRoleMappingResource, + job_state.NewMLJobStateResource, } } diff --git a/templates/resources/elasticsearch_ml_job_state.md.tmpl b/templates/resources/elasticsearch_ml_job_state.md.tmpl new file mode 100644 index 000000000..113f174f2 --- /dev/null +++ b/templates/resources/elasticsearch_ml_job_state.md.tmpl @@ -0,0 +1,18 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile .ExampleFile }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} \ No newline at end of file