diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 753f5df7e..fd1ccfc82 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -179,4 +179,5 @@ jobs: ELASTICSEARCH_USERNAME: "elastic" ELASTICSEARCH_PASSWORD: ${{ env.ELASTIC_PASSWORD }} KIBANA_ENDPOINT: "http://localhost:5601" - KIBANA_API_KEY: ${{ steps.get-api-key.outputs.apikey }} + KIBANA_USERNAME: "elastic" + KIBANA_PASSWORD: ${{ env.ELASTIC_PASSWORD }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d4e1c89c..acee066ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Migrate `elasticstack_elasticsearch_system_user` resource to Terraform plugin framework ([#1154](https://github.com/elastic/terraform-provider-elasticstack/pull/1154)) - Add custom `endpoint` configuration support for snapshot repository setup ([#1158](https://github.com/elastic/terraform-provider-elasticstack/pull/1158)) - Add `description` to `elasticstack_kibana_security_role` ([#1172](https://github.com/elastic/terraform-provider-elasticstack/issues/1172)) +- Add `elasticstack_kibana_synthetics_parameter` resource ([#1155](https://github.com/elastic/terraform-provider-elasticstack/pull/1155)) ## [0.11.15] - 2025-04-23 @@ -425,7 +426,8 @@ - Initial set of docs - CI integration -[Unreleased]: https://github.com/elastic/terraform-provider-elasticstack/compare/v0.11.14...HEAD +[Unreleased]: https://github.com/elastic/terraform-provider-elasticstack/compare/v0.11.15...HEAD +[0.11.15]: https://github.com/elastic/terraform-provider-elasticstack/compare/v0.11.14...v0.11.15 [0.11.14]: https://github.com/elastic/terraform-provider-elasticstack/compare/v0.11.13...v0.11.14 [0.11.13]: https://github.com/elastic/terraform-provider-elasticstack/compare/v0.11.12...v0.11.13 [0.11.12]: https://github.com/elastic/terraform-provider-elasticstack/compare/v0.11.11...v0.11.12 diff --git a/docs/resources/kibana_synthetics_parameter.md b/docs/resources/kibana_synthetics_parameter.md new file mode 100644 index 000000000..d43247b6a --- /dev/null +++ b/docs/resources/kibana_synthetics_parameter.md @@ -0,0 +1,54 @@ +--- +subcategory: "Kibana" +layout: "" +page_title: "Elasticstack: elasticstack_kibana_synthetics_parameter Resource" +description: |- + Creates or updates a Kibana synthetics parameter. +--- + +# Resource: elasticstack_kibana_synthetics_parameter + +Creates or updates a Kibana synthetics parameter. +See [Working with secrets and sensitive values](https://www.elastic.co/docs/solutions/observability/synthetics/work-with-params-secrets) +and [API docs](https://www.elastic.co/docs/api/doc/kibana/group/endpoint-synthetics) + +## Example Usage + +```terraform +provider "elasticstack" { + kibana {} +} + +resource "elasticstack_kibana_synthetics_parameter" "example" { + key = "example_key" + value = "example_value" + description = "Example description" + tags = ["tag-a", "tag-b"] +} +``` + + +## Schema + +### Required + +- `key` (String) The key of the parameter. +- `value` (String, Sensitive) The value associated with the parameter. + +### Optional + +- `description` (String) A description of the parameter. +- `share_across_spaces` (Boolean) Whether the parameter should be shared across spaces. +- `tags` (List of String) An array of tags to categorize the parameter. + +### Read-Only + +- `id` (String) Generated id for the parameter. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import elasticstack_kibana_synthetics_parameter.my_param / +``` \ No newline at end of file diff --git a/examples/resources/elasticstack_kibana_synthetics_parameter/import.sh b/examples/resources/elasticstack_kibana_synthetics_parameter/import.sh new file mode 100644 index 000000000..76eb4585b --- /dev/null +++ b/examples/resources/elasticstack_kibana_synthetics_parameter/import.sh @@ -0,0 +1 @@ +terraform import elasticstack_kibana_synthetics_parameter.my_param / diff --git a/examples/resources/elasticstack_kibana_synthetics_parameter/resource.tf b/examples/resources/elasticstack_kibana_synthetics_parameter/resource.tf new file mode 100644 index 000000000..86d6f27a0 --- /dev/null +++ b/examples/resources/elasticstack_kibana_synthetics_parameter/resource.tf @@ -0,0 +1,10 @@ +provider "elasticstack" { + kibana {} +} + +resource "elasticstack_kibana_synthetics_parameter" "example" { + key = "example_key" + value = "example_value" + description = "Example description" + tags = ["tag-a", "tag-b"] +} diff --git a/generated/kbapi/kibana.gen.go b/generated/kbapi/kibana.gen.go index 2fea293a2..060792f2c 100644 --- a/generated/kbapi/kibana.gen.go +++ b/generated/kbapi/kibana.gen.go @@ -829,6 +829,66 @@ type DataViewsUpdateDataViewRequestObjectInner struct { TypeMeta *DataViewsTypemeta `json:"typeMeta,omitempty"` } +// SyntheticsGetParameterResponse defines model for Synthetics_getParameterResponse. +type SyntheticsGetParameterResponse struct { + // Description The description of the parameter. It is included in the response if the user has read-only permissions to the Synthetics app. + Description *string `json:"description,omitempty"` + + // Id The unique identifier of the parameter. + Id *string `json:"id,omitempty"` + + // Key The key of the parameter. + Key *string `json:"key,omitempty"` + + // Namespaces The namespaces associated with the parameter. It is included in the response if the user has read-only permissions to the Synthetics app. + Namespaces *[]string `json:"namespaces,omitempty"` + + // Tags An array of tags associated with the parameter. It is included in the response if the user has read-only permissions to the Synthetics app. + Tags *[]string `json:"tags,omitempty"` + + // Value The value associated with the parameter. It will be included in the response if the user has write permissions. + Value *string `json:"value,omitempty"` +} + +// SyntheticsParameterRequest defines model for Synthetics_parameterRequest. +type SyntheticsParameterRequest struct { + // Description A description of the parameter. + Description *string `json:"description,omitempty"` + + // Key The key of the parameter. + Key string `json:"key"` + + // ShareAcrossSpaces Specify whether the parameter should be shared across spaces. + ShareAcrossSpaces *bool `json:"share_across_spaces,omitempty"` + + // Tags An array of tags to categorize the parameter. + Tags *[]string `json:"tags,omitempty"` + + // Value The value associated with the parameter. + Value string `json:"value"` +} + +// SyntheticsPostParameterResponse defines model for Synthetics_postParameterResponse. +type SyntheticsPostParameterResponse struct { + // Description A description of the parameter. + Description *string `json:"description,omitempty"` + + // Id The unique identifier for the parameter. + Id *string `json:"id,omitempty"` + + // Key The parameter key. + Key *string `json:"key,omitempty"` + + // ShareAcrossSpaces Indicates whether the parameter is shared across spaces. + ShareAcrossSpaces *bool `json:"share_across_spaces,omitempty"` + + // Tags An array of tags associated with the parameter. + Tags *[]string `json:"tags,omitempty"` + + // Value The value associated with the parameter. + Value *string `json:"value,omitempty"` +} + // AgentPolicy defines model for agent_policy. type AgentPolicy struct { AdvancedSettings *struct { @@ -1211,6 +1271,14 @@ type AgentPolicyGlobalDataTagsItem_Value struct { union json.RawMessage } +// CreateParamResponse defines model for create_param_response. +type CreateParamResponse struct { + union json.RawMessage +} + +// CreateParamResponse0 defines model for . +type CreateParamResponse0 = []SyntheticsPostParameterResponse + // EnrollmentApiKey defines model for enrollment_api_key. type EnrollmentApiKey struct { // Active When false, the enrollment API key is revoked and cannot be used for enrolling Elastic Agents. @@ -3278,6 +3346,29 @@ type PutFleetPackagePoliciesPackagepolicyidParams struct { // PutFleetPackagePoliciesPackagepolicyidParamsFormat defines parameters for PutFleetPackagePoliciesPackagepolicyid. type PutFleetPackagePoliciesPackagepolicyidParamsFormat string +// PostParametersJSONBody defines parameters for PostParameters. +type PostParametersJSONBody struct { + union json.RawMessage +} + +// PostParametersJSONBody0 defines parameters for PostParameters. +type PostParametersJSONBody0 = []SyntheticsParameterRequest + +// PutParameterJSONBody defines parameters for PutParameter. +type PutParameterJSONBody struct { + // Description The updated description of the parameter. + Description *string `json:"description,omitempty"` + + // Key The key of the parameter. + Key *string `json:"key,omitempty"` + + // Tags An array of updated tags to categorize the parameter. + Tags *[]string `json:"tags,omitempty"` + + // Value The updated value associated with the parameter. + Value *string `json:"value,omitempty"` +} + // PostFleetAgentPoliciesJSONRequestBody defines body for PostFleetAgentPolicies for application/json ContentType. type PostFleetAgentPoliciesJSONRequestBody PostFleetAgentPoliciesJSONBody @@ -3308,6 +3399,12 @@ type PostFleetPackagePoliciesJSONRequestBody = PackagePolicyRequest // PutFleetPackagePoliciesPackagepolicyidJSONRequestBody defines body for PutFleetPackagePoliciesPackagepolicyid for application/json ContentType. type PutFleetPackagePoliciesPackagepolicyidJSONRequestBody = PackagePolicyRequest +// PostParametersJSONRequestBody defines body for PostParameters for application/json ContentType. +type PostParametersJSONRequestBody PostParametersJSONBody + +// PutParameterJSONRequestBody defines body for PutParameter for application/json ContentType. +type PutParameterJSONRequestBody PutParameterJSONBody + // CreateDataViewDefaultwJSONRequestBody defines body for CreateDataViewDefaultw for application/json ContentType. type CreateDataViewDefaultwJSONRequestBody = DataViewsCreateDataViewRequestObject @@ -11943,6 +12040,68 @@ func (t *AgentPolicyGlobalDataTagsItem_Value) UnmarshalJSON(b []byte) error { return err } +// AsCreateParamResponse0 returns the union data inside the CreateParamResponse as a CreateParamResponse0 +func (t CreateParamResponse) AsCreateParamResponse0() (CreateParamResponse0, error) { + var body CreateParamResponse0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromCreateParamResponse0 overwrites any union data inside the CreateParamResponse as the provided CreateParamResponse0 +func (t *CreateParamResponse) FromCreateParamResponse0(v CreateParamResponse0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeCreateParamResponse0 performs a merge with any union data inside the CreateParamResponse, using the provided CreateParamResponse0 +func (t *CreateParamResponse) MergeCreateParamResponse0(v CreateParamResponse0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsSyntheticsPostParameterResponse returns the union data inside the CreateParamResponse as a SyntheticsPostParameterResponse +func (t CreateParamResponse) AsSyntheticsPostParameterResponse() (SyntheticsPostParameterResponse, error) { + var body SyntheticsPostParameterResponse + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromSyntheticsPostParameterResponse overwrites any union data inside the CreateParamResponse as the provided SyntheticsPostParameterResponse +func (t *CreateParamResponse) FromSyntheticsPostParameterResponse(v SyntheticsPostParameterResponse) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeSyntheticsPostParameterResponse performs a merge with any union data inside the CreateParamResponse, using the provided SyntheticsPostParameterResponse +func (t *CreateParamResponse) MergeSyntheticsPostParameterResponse(v SyntheticsPostParameterResponse) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t CreateParamResponse) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *CreateParamResponse) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // AsNewOutputElasticsearchSecretsSslKey0 returns the union data inside the NewOutputElasticsearch_Secrets_Ssl_Key as a NewOutputElasticsearchSecretsSslKey0 func (t NewOutputElasticsearch_Secrets_Ssl_Key) AsNewOutputElasticsearchSecretsSslKey0() (NewOutputElasticsearchSecretsSslKey0, error) { var body NewOutputElasticsearchSecretsSslKey0 @@ -14207,6 +14366,22 @@ type ClientInterface interface { PutFleetPackagePoliciesPackagepolicyid(ctx context.Context, packagePolicyId string, params *PutFleetPackagePoliciesPackagepolicyidParams, body PutFleetPackagePoliciesPackagepolicyidJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // PostParametersWithBody request with any body + PostParametersWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PostParameters(ctx context.Context, body PostParametersJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteParameter request + DeleteParameter(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetParameter request + GetParameter(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PutParameterWithBody request with any body + PutParameterWithBody(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PutParameter(ctx context.Context, id string, body PutParameterJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetAllDataViewsDefault request GetAllDataViewsDefault(ctx context.Context, spaceId SpaceId, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -14659,6 +14834,78 @@ func (c *Client) PutFleetPackagePoliciesPackagepolicyid(ctx context.Context, pac return c.Client.Do(req) } +func (c *Client) PostParametersWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostParametersRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PostParameters(ctx context.Context, body PostParametersJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostParametersRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteParameter(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteParameterRequest(c.Server, id) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetParameter(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetParameterRequest(c.Server, id) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PutParameterWithBody(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPutParameterRequestWithBody(c.Server, id, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PutParameter(ctx context.Context, id string, body PutParameterJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPutParameterRequest(c.Server, id, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) GetAllDataViewsDefault(ctx context.Context, spaceId SpaceId, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewGetAllDataViewsDefaultRequest(c.Server, spaceId) if err != nil { @@ -16458,6 +16705,161 @@ func NewPutFleetPackagePoliciesPackagepolicyidRequestWithBody(server string, pac return req, nil } +// NewPostParametersRequest calls the generic PostParameters builder with application/json body +func NewPostParametersRequest(server string, body PostParametersJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPostParametersRequestWithBody(server, "application/json", bodyReader) +} + +// NewPostParametersRequestWithBody generates requests for PostParameters with any type of body +func NewPostParametersRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/synthetics/params") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewDeleteParameterRequest generates requests for DeleteParameter +func NewDeleteParameterRequest(server string, id string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/synthetics/params/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetParameterRequest generates requests for GetParameter +func NewGetParameterRequest(server string, id string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/synthetics/params/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPutParameterRequest calls the generic PutParameter builder with application/json body +func NewPutParameterRequest(server string, id string, body PutParameterJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPutParameterRequestWithBody(server, id, "application/json", bodyReader) +} + +// NewPutParameterRequestWithBody generates requests for PutParameter with any type of body +func NewPutParameterRequestWithBody(server string, id string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/synthetics/params/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewGetAllDataViewsDefaultRequest generates requests for GetAllDataViewsDefault func NewGetAllDataViewsDefaultRequest(server string, spaceId SpaceId) (*http.Request, error) { var err error @@ -16816,6 +17218,22 @@ type ClientWithResponsesInterface interface { PutFleetPackagePoliciesPackagepolicyidWithResponse(ctx context.Context, packagePolicyId string, params *PutFleetPackagePoliciesPackagepolicyidParams, body PutFleetPackagePoliciesPackagepolicyidJSONRequestBody, reqEditors ...RequestEditorFn) (*PutFleetPackagePoliciesPackagepolicyidResponse, error) + // PostParametersWithBodyWithResponse request with any body + PostParametersWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostParametersResponse, error) + + PostParametersWithResponse(ctx context.Context, body PostParametersJSONRequestBody, reqEditors ...RequestEditorFn) (*PostParametersResponse, error) + + // DeleteParameterWithResponse request + DeleteParameterWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*DeleteParameterResponse, error) + + // GetParameterWithResponse request + GetParameterWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*GetParameterResponse, error) + + // PutParameterWithBodyWithResponse request with any body + PutParameterWithBodyWithResponse(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PutParameterResponse, error) + + PutParameterWithResponse(ctx context.Context, id string, body PutParameterJSONRequestBody, reqEditors ...RequestEditorFn) (*PutParameterResponse, error) + // GetAllDataViewsDefaultWithResponse request GetAllDataViewsDefaultWithResponse(ctx context.Context, spaceId SpaceId, reqEditors ...RequestEditorFn) (*GetAllDataViewsDefaultResponse, error) @@ -17739,6 +18157,93 @@ func (r PutFleetPackagePoliciesPackagepolicyidResponse) StatusCode() int { return 0 } +type PostParametersResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *CreateParamResponse +} + +// Status returns HTTPResponse.Status +func (r PostParametersResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostParametersResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteParameterResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r DeleteParameterResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteParameterResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetParameterResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *SyntheticsGetParameterResponse +} + +// Status returns HTTPResponse.Status +func (r GetParameterResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetParameterResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PutParameterResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *map[string]interface{} +} + +// Status returns HTTPResponse.Status +func (r PutParameterResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PutParameterResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type GetAllDataViewsDefaultResponse struct { Body []byte HTTPResponse *http.Response @@ -18169,6 +18674,58 @@ func (c *ClientWithResponses) PutFleetPackagePoliciesPackagepolicyidWithResponse return ParsePutFleetPackagePoliciesPackagepolicyidResponse(rsp) } +// PostParametersWithBodyWithResponse request with arbitrary body returning *PostParametersResponse +func (c *ClientWithResponses) PostParametersWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostParametersResponse, error) { + rsp, err := c.PostParametersWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostParametersResponse(rsp) +} + +func (c *ClientWithResponses) PostParametersWithResponse(ctx context.Context, body PostParametersJSONRequestBody, reqEditors ...RequestEditorFn) (*PostParametersResponse, error) { + rsp, err := c.PostParameters(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostParametersResponse(rsp) +} + +// DeleteParameterWithResponse request returning *DeleteParameterResponse +func (c *ClientWithResponses) DeleteParameterWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*DeleteParameterResponse, error) { + rsp, err := c.DeleteParameter(ctx, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteParameterResponse(rsp) +} + +// GetParameterWithResponse request returning *GetParameterResponse +func (c *ClientWithResponses) GetParameterWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*GetParameterResponse, error) { + rsp, err := c.GetParameter(ctx, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetParameterResponse(rsp) +} + +// PutParameterWithBodyWithResponse request with arbitrary body returning *PutParameterResponse +func (c *ClientWithResponses) PutParameterWithBodyWithResponse(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PutParameterResponse, error) { + rsp, err := c.PutParameterWithBody(ctx, id, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePutParameterResponse(rsp) +} + +func (c *ClientWithResponses) PutParameterWithResponse(ctx context.Context, id string, body PutParameterJSONRequestBody, reqEditors ...RequestEditorFn) (*PutParameterResponse, error) { + rsp, err := c.PutParameter(ctx, id, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePutParameterResponse(rsp) +} + // GetAllDataViewsDefaultWithResponse request returning *GetAllDataViewsDefaultResponse func (c *ClientWithResponses) GetAllDataViewsDefaultWithResponse(ctx context.Context, spaceId SpaceId, reqEditors ...RequestEditorFn) (*GetAllDataViewsDefaultResponse, error) { rsp, err := c.GetAllDataViewsDefault(ctx, spaceId, reqEditors...) @@ -19362,6 +19919,100 @@ func ParsePutFleetPackagePoliciesPackagepolicyidResponse(rsp *http.Response) (*P return response, nil } +// ParsePostParametersResponse parses an HTTP response from a PostParametersWithResponse call +func ParsePostParametersResponse(rsp *http.Response) (*PostParametersResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostParametersResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest CreateParamResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseDeleteParameterResponse parses an HTTP response from a DeleteParameterWithResponse call +func ParseDeleteParameterResponse(rsp *http.Response) (*DeleteParameterResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteParameterResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseGetParameterResponse parses an HTTP response from a GetParameterWithResponse call +func ParseGetParameterResponse(rsp *http.Response) (*GetParameterResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetParameterResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest SyntheticsGetParameterResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParsePutParameterResponse parses an HTTP response from a PutParameterWithResponse call +func ParsePutParameterResponse(rsp *http.Response) (*PutParameterResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PutParameterResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest map[string]interface{} + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + // ParseGetAllDataViewsDefaultResponse parses an HTTP response from a GetAllDataViewsDefaultWithResponse call func ParseGetAllDataViewsDefaultResponse(rsp *http.Response) (*GetAllDataViewsDefaultResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/generated/kbapi/transform_schema.go b/generated/kbapi/transform_schema.go index 82b8b461a..130f45f0a 100644 --- a/generated/kbapi/transform_schema.go +++ b/generated/kbapi/transform_schema.go @@ -568,6 +568,8 @@ func transformFilterPaths(schema *Schema) { "/api/fleet/outputs/{outputId}": {"get", "put", "delete"}, "/api/fleet/package_policies": {"get", "post"}, "/api/fleet/package_policies/{packagePolicyId}": {"get", "put", "delete"}, + "/api/synthetics/params": {"post"}, + "/api/synthetics/params/{id}": {"get", "put", "delete"}, } for path, pathInfo := range schema.Paths { @@ -692,7 +694,11 @@ func transformSimplifyContentType(schema *Schema) { func transformAddMisingDescriptions(schema *Schema) { for _, pathInfo := range schema.Paths { for _, endpoint := range pathInfo.Endpoints { - responses := endpoint.MustGetMap("responses") + responses, ok := endpoint.GetMap("responses") + if !ok { + return + } + for code := range responses { response := responses.MustGetMap(code) if _, ok := response["description"]; !ok { @@ -748,6 +754,9 @@ func transformKibanaPaths(schema *Schema) { dataViewsPath.Get.CreateRef(schema, "get_data_views_response_item", "responses.200.content.application/json.schema.properties.data_view.items") + sytheticsParamsPath := schema.MustGetPath("/api/synthetics/params") + sytheticsParamsPath.Post.CreateRef(schema, "create_param_response", "responses.200.content.application/json.schema") + schema.Components.CreateRef(schema, "Data_views_data_view_response_object_inner", "schemas.Data_views_data_view_response_object.properties.data_view") schema.Components.CreateRef(schema, "Data_views_sourcefilter_item", "schemas.Data_views_sourcefilters.items") schema.Components.CreateRef(schema, "Data_views_runtimefieldmap_script", "schemas.Data_views_runtimefieldmap.properties.script") diff --git a/internal/kibana/synthetics/parameter/create.go b/internal/kibana/synthetics/parameter/create.go new file mode 100644 index 000000000..387293e39 --- /dev/null +++ b/internal/kibana/synthetics/parameter/create.go @@ -0,0 +1,58 @@ +package parameter + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + kibanaClient := synthetics.GetKibanaOAPIClient(r, response.Diagnostics) + if kibanaClient == nil { + return + } + + var plan tfModelV0 + diags := request.Plan.Get(ctx, &plan) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + input := plan.toParameterRequest(false) + + // We shouldn't have to do this json marshalling ourselves, + // https://github.com/oapi-codegen/oapi-codegen/issues/1620 means the generated code doesn't handle the oneOf + // request body properly. + inputJson, err := json.Marshal(input) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("Failed to marshal JSON for parameter `%s`", input.Key), err.Error()) + return + } + + createResult, err := kibanaClient.API.PostParametersWithBodyWithResponse(ctx, "application/json", bytes.NewReader(inputJson)) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("Failed to create parameter `%s`", input.Key), err.Error()) + return + } + + createResponse, err := createResult.JSON200.AsSyntheticsPostParameterResponse() + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("Failed to parse parameter response `%s`", input.Key), err.Error()) + return + } + + if createResponse.Id == nil { + response.Diagnostics.AddError(fmt.Sprintf("Unexpected nil id in create parameter response `%s`", input.Key), "") + return + } + + // We can't trust the response from the POST request, so read the parameter + // again. At least with Kibana 9.0.0, the POST request responds without the + // `value` field set. + r.readState(ctx, kibanaClient, *createResponse.Id, &response.State, &response.Diagnostics) +} diff --git a/internal/kibana/synthetics/parameter/delete.go b/internal/kibana/synthetics/parameter/delete.go new file mode 100644 index 000000000..cee36b155 --- /dev/null +++ b/internal/kibana/synthetics/parameter/delete.go @@ -0,0 +1,41 @@ +package parameter + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *Resource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + kibanaClient := synthetics.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 + } + + resourceId := plan.ID.ValueString() + + compositeId, dg := tryReadCompositeId(resourceId) + response.Diagnostics.Append(dg...) + if response.Diagnostics.HasError() { + return + } + + if compositeId != nil { + resourceId = compositeId.ResourceId + } + + _, err := kibanaClient.KibanaSynthetics.Parameter.Delete(ctx, resourceId) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("Failed to delete parameter `%s`", resourceId), err.Error()) + return + } +} diff --git a/internal/kibana/synthetics/parameter/read.go b/internal/kibana/synthetics/parameter/read.go new file mode 100644 index 000000000..0d3e1a620 --- /dev/null +++ b/internal/kibana/synthetics/parameter/read.go @@ -0,0 +1,65 @@ +package parameter + +import ( + "context" + "errors" + "fmt" + + "github.com/disaster37/go-kibana-rest/v8/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func (r *Resource) readState(ctx context.Context, kibanaClient *kibana_oapi.Client, resourceId string, state *tfsdk.State, diagnostics *diag.Diagnostics) { + getResult, err := kibanaClient.API.GetParameterWithResponse(ctx, resourceId) + if err != nil { + var apiError *kbapi.APIError + if errors.As(err, &apiError) && apiError.Code == 404 { + state.RemoveResource(ctx) + return + } + + diagnostics.AddError(fmt.Sprintf("Failed to get parameter `%s`", resourceId), err.Error()) + return + } + + model := modelV0FromOAPI(*getResult.JSON200) + + // Set refreshed state + diags := state.Set(ctx, &model) + diagnostics.Append(diags...) + if diagnostics.HasError() { + return + } +} + +func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + kibanaClient := synthetics.GetKibanaOAPIClient(r, response.Diagnostics) + if kibanaClient == nil { + return + } + + var state tfModelV0 + diags := request.State.Get(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + resourceId := state.ID.ValueString() + + compositeId, dg := tryReadCompositeId(resourceId) + response.Diagnostics.Append(dg...) + if response.Diagnostics.HasError() { + return + } + + if compositeId != nil { + resourceId = compositeId.ResourceId + } + + r.readState(ctx, kibanaClient, resourceId, &response.State, &response.Diagnostics) +} diff --git a/internal/kibana/synthetics/parameter/resource.go b/internal/kibana/synthetics/parameter/resource.go new file mode 100644 index 000000000..65b904937 --- /dev/null +++ b/internal/kibana/synthetics/parameter/resource.go @@ -0,0 +1,45 @@ +package parameter + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +const resourceName = synthetics.MetadataPrefix + "parameter" + +// Ensure provider defined types fully satisfy framework interfaces +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) { + resp.Schema = parameterSchema() +} + +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 +} diff --git a/internal/kibana/synthetics/parameter/resource_test.go b/internal/kibana/synthetics/parameter/resource_test.go new file mode 100644 index 000000000..3a4ff0920 --- /dev/null +++ b/internal/kibana/synthetics/parameter/resource_test.go @@ -0,0 +1,89 @@ +package parameter_test + +import ( + "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" +) + +const ( + providerConfig = ` +provider "elasticstack" { + kibana {} +} +` +) + +var ( + minKibanaParameterAPIVersion = version.Must(version.NewVersion("8.12.0")) +) + +func TestSyntheticParameterResource(t *testing.T) { + resourceId := "elasticstack_kibana_synthetics_parameter.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + // Create and Read testing + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minKibanaParameterAPIVersion), + Config: providerConfig + ` +resource "elasticstack_kibana_synthetics_parameter" "test" { + key = "test-key" + value = "test-value" + description = "Test description" + tags = ["a", "b"] +} +`, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceId, "key", "test-key"), + resource.TestCheckResourceAttr(resourceId, "value", "test-value"), + resource.TestCheckResourceAttr(resourceId, "description", "Test description"), + resource.TestCheckResourceAttr(resourceId, "tags.#", "2"), + resource.TestCheckResourceAttr(resourceId, "tags.0", "a"), + resource.TestCheckResourceAttr(resourceId, "tags.1", "b"), + ), + }, + // ImportState testing + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minKibanaParameterAPIVersion), + ResourceName: resourceId, + ImportState: true, + ImportStateVerify: true, + Config: providerConfig + ` +resource "elasticstack_kibana_synthetics_parameter" "test" { + key = "test-key" + value = "test-value" + description = "Test description" + tags = ["a", "b"] +} +`, + }, + // Update and Read testing + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minKibanaParameterAPIVersion), + Config: providerConfig + ` +resource "elasticstack_kibana_synthetics_parameter" "test" { + key = "test-key-2" + value = "test-value-2" + description = "Test description 2" + tags = ["c", "d", "e"] +} +`, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceId, "key", "test-key-2"), + resource.TestCheckResourceAttr(resourceId, "value", "test-value-2"), + resource.TestCheckResourceAttr(resourceId, "description", "Test description 2"), + resource.TestCheckResourceAttr(resourceId, "tags.#", "3"), + resource.TestCheckResourceAttr(resourceId, "tags.0", "c"), + resource.TestCheckResourceAttr(resourceId, "tags.1", "d"), + resource.TestCheckResourceAttr(resourceId, "tags.2", "e"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/kibana/synthetics/parameter/schema.go b/internal/kibana/synthetics/parameter/schema.go new file mode 100644 index 000000000..00dc87321 --- /dev/null +++ b/internal/kibana/synthetics/parameter/schema.go @@ -0,0 +1,134 @@ +package parameter + +import ( + "slices" + "strings" + + kboapi "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "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/types" +) + +type tfModelV0 struct { + ID types.String `tfsdk:"id"` + Key types.String `tfsdk:"key"` + Value types.String `tfsdk:"value"` + Description types.String `tfsdk:"description"` + Tags []types.String `tfsdk:"tags"` //> string + ShareAcrossSpaces types.Bool `tfsdk:"share_across_spaces"` +} + +func parameterSchema() schema.Schema { + return schema.Schema{ + MarkdownDescription: "Synthetics parameter config, see https://www.elastic.co/docs/api/doc/kibana/group/endpoint-synthetics for more details", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Generated id for the parameter.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + }, + "key": schema.StringAttribute{ + Optional: false, + Required: true, + MarkdownDescription: "The key of the parameter.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "value": schema.StringAttribute{ + Optional: false, + Required: true, + Sensitive: true, + MarkdownDescription: "The value associated with the parameter.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + MarkdownDescription: "A description of the parameter.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "tags": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Computed: true, + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + MarkdownDescription: "An array of tags to categorize the parameter.", + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + "share_across_spaces": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Whether the parameter should be shared across spaces.", + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + boolplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (m *tfModelV0) toParameterRequest(forUpdate bool) kboapi.SyntheticsParameterRequest { + // share_across_spaces is not allowed to be set when updating an existing + // global parameter. + var shareAcrossSpaces *bool = nil + if !forUpdate { + shareAcrossSpaces = m.ShareAcrossSpaces.ValueBoolPointer() + } + + return kboapi.SyntheticsParameterRequest{ + Key: m.Key.ValueString(), + Value: m.Value.ValueString(), + Description: utils.Pointer(m.Description.ValueString()), + // We need this to marshal as an empty JSON array, not null. + Tags: utils.Pointer(utils.NonNilSlice(synthetics.ValueStringSlice(m.Tags))), + ShareAcrossSpaces: shareAcrossSpaces, + } +} + +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 modelV0FromOAPI(param kboapi.SyntheticsGetParameterResponse) tfModelV0 { + allSpaces := slices.Equal(*param.Namespaces, []string{"*"}) + + return tfModelV0{ + ID: types.StringPointerValue(param.Id), + Key: types.StringPointerValue(param.Key), + Value: types.StringPointerValue(param.Value), + Description: types.StringPointerValue(param.Description), + // Terraform, like json.Marshal, treats empty slices as null. We need an + // actual backing array of size 0. + Tags: utils.NonNilSlice(synthetics.StringSliceValue(utils.DefaultIfNil(param.Tags))), + ShareAcrossSpaces: types.BoolValue(allSpaces), + } +} diff --git a/internal/kibana/synthetics/parameter/schema_test.go b/internal/kibana/synthetics/parameter/schema_test.go new file mode 100644 index 000000000..1eae2009e --- /dev/null +++ b/internal/kibana/synthetics/parameter/schema_test.go @@ -0,0 +1,91 @@ +package parameter + +import ( + "testing" + + kboapi "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/stretchr/testify/assert" +) + +func Test_roundtrip(t *testing.T) { + tests := []struct { + name string + id string + namespaces []string + request kboapi.SyntheticsParameterRequest + }{ + { + name: "only required fields", + id: "id-1", + namespaces: []string{"ns-1"}, + request: kboapi.SyntheticsParameterRequest{ + Key: "key-1", + Value: "value-1", + }, + }, + { + name: "all fields", + id: "id-2", + namespaces: []string{"*"}, + request: kboapi.SyntheticsParameterRequest{ + Key: "key-2", + Value: "value-2", + Description: utils.Pointer("description-2"), + Tags: utils.Pointer([]string{"tag-1", "tag-2", "tag-3"}), + ShareAcrossSpaces: utils.Pointer(true), + }, + }, + { + name: "only description", + id: "id-3", + namespaces: []string{"ns-3"}, + request: kboapi.SyntheticsParameterRequest{ + Key: "key-3", + Value: "value-3", + Description: utils.Pointer("description-3"), + }, + }, + { + name: "only tags", + id: "id-4", + namespaces: []string{"ns-4"}, + request: kboapi.SyntheticsParameterRequest{ + Key: "key-4", + Value: "value-4", + Description: utils.Pointer("description-4"), + }, + }, + { + name: "all namespaces", + id: "id-5", + namespaces: []string{"ns-5"}, + request: kboapi.SyntheticsParameterRequest{ + Key: "key-5", + Value: "value-5", + Description: utils.Pointer("description-5"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + response := kboapi.SyntheticsGetParameterResponse{ + Id: &tt.id, + Namespaces: &tt.namespaces, + Key: &tt.request.Key, + Value: &tt.request.Value, + Description: tt.request.Description, + Tags: tt.request.Tags, + } + modelV0 := modelV0FromOAPI(response) + + actual := modelV0.toParameterRequest(false) + + assert.Equal(t, tt.request.Key, actual.Key) + assert.Equal(t, tt.request.Value, actual.Value) + assert.Equal(t, utils.DefaultIfNil(tt.request.Description), utils.DefaultIfNil(actual.Description)) + assert.Equal(t, utils.NonNilSlice(utils.DefaultIfNil(tt.request.Tags)), utils.NonNilSlice(utils.DefaultIfNil(actual.Tags))) + assert.Equal(t, utils.DefaultIfNil(tt.request.ShareAcrossSpaces), utils.DefaultIfNil(actual.ShareAcrossSpaces)) + }) + } +} diff --git a/internal/kibana/synthetics/parameter/update.go b/internal/kibana/synthetics/parameter/update.go new file mode 100644 index 000000000..9c5394953 --- /dev/null +++ b/internal/kibana/synthetics/parameter/update.go @@ -0,0 +1,59 @@ +package parameter + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *Resource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + kibanaClient := synthetics.GetKibanaOAPIClient(r, response.Diagnostics) + if kibanaClient == nil { + return + } + + var state tfModelV0 + diags := request.Plan.Get(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + resourceId := state.ID.ValueString() + + compositeId, dg := tryReadCompositeId(resourceId) + response.Diagnostics.Append(dg...) + if response.Diagnostics.HasError() { + return + } + + if compositeId != nil { + resourceId = compositeId.ResourceId + } + + input := state.toParameterRequest(true) + + // We shouldn't have to do this json marshalling ourselves, + // https://github.com/oapi-codegen/oapi-codegen/issues/1620 means the generated code doesn't handle the oneOf + // request body properly. + inputJson, err := json.Marshal(input) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("Failed to marshal JSON for parameter `%s`", input.Key), err.Error()) + return + } + + _, err = kibanaClient.API.PutParameterWithBodyWithResponse(ctx, resourceId, "application/json", bytes.NewReader(inputJson)) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("Failed to update parameter `%s`", resourceId), err.Error()) + return + } + + // We can't trust the response from the PUT request, so read the parameter + // again. At least with Kibana 9.0.0, the PUT request responds with the new + // values for every field, except `value`, which contains the old value. + r.readState(ctx, kibanaClient, resourceId, &response.State, &response.Diagnostics) +} diff --git a/internal/kibana/synthetics/resource.go b/internal/kibana/synthetics/resource.go index 33df23985..899a8b9bc 100644 --- a/internal/kibana/synthetics/resource.go +++ b/internal/kibana/synthetics/resource.go @@ -2,8 +2,10 @@ package synthetics import ( "context" + "github.com/disaster37/go-kibana-rest/v8" "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -42,6 +44,25 @@ func GetKibanaClient(c ESApiClient, dg diag.Diagnostics) *kibana.Client { return kibanaClient } +func GetKibanaOAPIClient(c ESApiClient, dg diag.Diagnostics) *kibana_oapi.Client { + + client := c.GetClient() + if client == nil { + dg.AddError( + "Unconfigured Client", + "Expected configured client. Please report this issue to the provider developers.", + ) + return nil + } + + kibanaClient, err := client.GetKibanaOapiClient() + if err != nil { + dg.AddError("unable to get kibana oapi client", err.Error()) + return nil + } + return kibanaClient +} + type Resource struct { client *clients.ApiClient ESApiClient diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 9d4af5c4a..ae74c3358 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -239,3 +239,24 @@ func ConvertToAttrDiags(diags fwdiag.Diagnostics, path path.Path) fwdiag.Diagnos } return nd } + +func DefaultIfNil[T any](value *T) T { + var result T + + if value != nil { + result = *value + } + + return result +} + +// Returns an empty slice if s is a slice represented by nil (no backing array). +// Guarantees that json.Marshal and terraform parameters will not treat the +// empty slice as null. +func NonNilSlice[T any](s []T) []T { + if s == nil { + return []T{} + } + + return s +} diff --git a/libs/go-kibana-rest/kbapi/api._.go b/libs/go-kibana-rest/kbapi/api._.go index 189fa2fad..5d8c4fe30 100644 --- a/libs/go-kibana-rest/kbapi/api._.go +++ b/libs/go-kibana-rest/kbapi/api._.go @@ -72,6 +72,7 @@ type KibanaShortenURLAPI struct { type KibanaSyntheticsAPI struct { Monitor *KibanaSyntheticsMonitorAPI PrivateLocation *KibanaSyntheticsPrivateLocationAPI + Parameter *KibanaSyntheticsParameterAPI } // New initialise the API implementation @@ -128,6 +129,9 @@ func New(c *resty.Client) *API { Delete: newKibanaSyntheticsPrivateLocationDeleteFunc(c), Get: newKibanaSyntheticsPrivateLocationGetFunc(c), }, + Parameter: &KibanaSyntheticsParameterAPI{ + Delete: newKibanaSyntheticsParameterDeleteFunc(c), + }, }, } } diff --git a/libs/go-kibana-rest/kbapi/api.kibana_synthetics.go b/libs/go-kibana-rest/kbapi/api.kibana_synthetics.go index c80c7cbb0..99bde4939 100644 --- a/libs/go-kibana-rest/kbapi/api.kibana_synthetics.go +++ b/libs/go-kibana-rest/kbapi/api.kibana_synthetics.go @@ -15,6 +15,7 @@ const ( basePathKibanaSynthetics = "/api/synthetics" privateLocationsSuffix = "/private_locations" monitorsSuffix = "/monitors" + parametersSuffix = "/params" Http MonitorType = "http" Tcp MonitorType = "tcp" @@ -86,6 +87,10 @@ type KibanaSyntheticsPrivateLocationAPI struct { Get KibanaSyntheticsPrivateLocationGet } +type KibanaSyntheticsParameterAPI struct { + Delete KibanaSyntheticsParameterDelete +} + type SyntheticsStatusConfig struct { Enabled *bool `json:"enabled,omitempty"` } @@ -256,6 +261,11 @@ type MonitorTypeConfig struct { Type MonitorType `json:"type"` } +type ParameterDeleteStatus struct { + Id string `json:"id"` + Deleted bool `json:"deleted"` +} + func (f HTTPMonitorFields) APIRequest(config SyntheticsMonitorConfig) interface{} { mType := MonitorTypeConfig{Type: Http} @@ -330,6 +340,8 @@ type KibanaSyntheticsPrivateLocationGet func(ctx context.Context, idOrLabel stri type KibanaSyntheticsPrivateLocationDelete func(ctx context.Context, id string) error +type KibanaSyntheticsParameterDelete func(ctx context.Context, id string) ([]ParameterDeleteStatus, error) + func newKibanaSyntheticsPrivateLocationGetFunc(c *resty.Client) KibanaSyntheticsPrivateLocationGet { return func(ctx context.Context, idOrLabel string) (*PrivateLocation, error) { if idOrLabel == "" { @@ -435,6 +447,44 @@ func newKibanaSyntheticsMonitorAddFunc(c *resty.Client) KibanaSyntheticsMonitorA } } +func newKibanaSyntheticsParameterDeleteFunc(c *resty.Client) KibanaSyntheticsParameterDelete { + // We're intentionally using the undocumented delete API call that the + // Kibana UI does. It's the only endpoint that works correctly across all + // Kibana versions. There have been many bugs... + + // DELETE /api/synthetics/params/ (from documentation) + // - Works on >=8.17.0. + // - HTTP 404 on 8.12.x through 8.16.x. + // DELETE /api/synthetics/params/_bulk_delete (from documentation) + // - HTTP 400 on 9.0.x with error message about URL parameters and body + // provided at the same time ("_bulk_delete" interpreted as parameter + // ID?). + // - HTTP 403 on 8.18.x with error message about missing `uptime-read` and + // `uptime-write` permissions despite having the `superuser` role. + // - HTTP 403 with no details on 8.17.x. + // - HTTP 404 on 8.12.x through 8.16.x. + // POST /api/synthetics/params/_bulk_delete (from comment in documentation example) + // - Works on >=8.17.0. + // - HTTP 404 on 8.12.x through 8.16.x. + // DELETE /api/synthetics/params (what the Kibana UI does) + // - Works on >=8.12.0. + + return func(ctx context.Context, id string) ([]ParameterDeleteStatus, error) { + path := basePath("", parametersSuffix) + log.Debugf("URL to delete parameter: %s", path) + + resp, err := c.R().SetContext(ctx).SetBody(map[string]interface{}{ + "ids": []string{id}, + }).Delete(path) + if err = handleKibanaError(err, resp); err != nil { + return nil, err + } + + result, err := unmarshal(resp, []ParameterDeleteStatus{}) + return *result, err + } +} + func unmarshal[T interface{}](resp *resty.Response, result T) (*T, error) { respBody := resp.Body() err := json.Unmarshal(respBody, &result) diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go index 07c1781c4..fcc31bfe6 100644 --- a/provider/plugin_framework.go +++ b/provider/plugin_framework.go @@ -21,6 +21,7 @@ import ( "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/parameter" "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" @@ -91,6 +92,7 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ func() resource.Resource { return &import_saved_objects.Resource{} }, data_view.NewResource, + func() resource.Resource { return ¶meter.Resource{} }, func() resource.Resource { return &private_location.Resource{} }, func() resource.Resource { return &index.Resource{} }, func() resource.Resource { return &synthetics.Resource{} }, diff --git a/templates/resources/kibana_synthetics_parameter.md.tmpl b/templates/resources/kibana_synthetics_parameter.md.tmpl new file mode 100644 index 000000000..6e2789c68 --- /dev/null +++ b/templates/resources/kibana_synthetics_parameter.md.tmpl @@ -0,0 +1,25 @@ +--- +subcategory: "Kibana" +layout: "" +page_title: "Elasticstack: elasticstack_kibana_synthetics_parameter Resource" +description: |- + Creates or updates a Kibana synthetics parameter. +--- + +# Resource: elasticstack_kibana_synthetics_parameter + +Creates or updates a Kibana synthetics parameter. +See [Working with secrets and sensitive values](https://www.elastic.co/docs/solutions/observability/synthetics/work-with-params-secrets) +and [API docs](https://www.elastic.co/docs/api/doc/kibana/group/endpoint-synthetics) + +## Example Usage + +{{ tffile "examples/resources/elasticstack_kibana_synthetics_parameter/resource.tf" }} + +{{ .SchemaMarkdown | trimspace }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" "examples/resources/elasticstack_kibana_synthetics_parameter/import.sh" }} \ No newline at end of file