From b5fa9c4c486c89f96db9639627201a1b0072f252 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 28 Jul 2025 17:23:16 +0200 Subject: [PATCH 01/28] Maintenance window resource: - uses the plugin framework - client generating the openApi specification --- .gitignore | 1 + generated/kbapi/kibana.gen.go | 1036 ++++++++++++++++- generated/kbapi/transform_schema.go | 12 + .../clients/kibana_oapi/maintenance_window.go | 73 ++ .../kibana/maintenance_window/acc_test.go | 153 +++ internal/kibana/maintenance_window/create.go | 50 + internal/kibana/maintenance_window/delete.go | 28 + internal/kibana/maintenance_window/models.go | 446 +++++++ internal/kibana/maintenance_window/read.go | 45 + .../kibana/maintenance_window/resource.go | 34 + internal/kibana/maintenance_window/schema.go | 146 +++ internal/kibana/maintenance_window/update.go | 46 + .../kibana/maintenance_window/validators.go | 91 ++ provider/plugin_framework.go | 2 + 14 files changed, 2147 insertions(+), 16 deletions(-) create mode 100644 internal/clients/kibana_oapi/maintenance_window.go create mode 100644 internal/kibana/maintenance_window/acc_test.go create mode 100644 internal/kibana/maintenance_window/create.go create mode 100644 internal/kibana/maintenance_window/delete.go create mode 100644 internal/kibana/maintenance_window/models.go create mode 100644 internal/kibana/maintenance_window/read.go create mode 100644 internal/kibana/maintenance_window/resource.go create mode 100644 internal/kibana/maintenance_window/schema.go create mode 100644 internal/kibana/maintenance_window/update.go create mode 100644 internal/kibana/maintenance_window/validators.go diff --git a/.gitignore b/.gitignore index 3c429c9dd..bcfa6a1c2 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ website/node_modules *.test *.iml *.vscode +__debug_* website/vendor diff --git a/generated/kbapi/kibana.gen.go b/generated/kbapi/kibana.gen.go index 52a36324e..8f5616dac 100644 --- a/generated/kbapi/kibana.gen.go +++ b/generated/kbapi/kibana.gen.go @@ -3539,6 +3539,103 @@ type DeleteAgentConfigurationJSONRequestBody = APMUIDeleteServiceObject // CreateUpdateAgentConfigurationJSONRequestBody defines body for CreateUpdateAgentConfiguration for application/json ContentType. type CreateUpdateAgentConfigurationJSONRequestBody = APMUIAgentConfigurationIntakeObject +// PostMaintenanceWindowJSONBody defines parameters for PostMaintenanceWindow. + +type PostMaintenanceWindowJSONBody struct { + // Enabled Whether the current maintenance window is enabled. Disabled maintenance windows do not suppress notifications. + Enabled *bool `json:"enabled,omitempty"` + Schedule struct { + Custom struct { + // Duration The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. + Duration string `json:"duration"` + Recurring *struct { + // End The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`. + End *string `json:"end,omitempty"` + + // Every The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`. + Every *string `json:"every,omitempty"` + + // Occurrences The total number of recurrences of the schedule. + Occurrences *float32 `json:"occurrences,omitempty"` + + // OnMonth The specific months for a recurring schedule. Valid values are 1-12. + OnMonth *[]float32 `json:"onMonth,omitempty"` + + // OnMonthDay The specific days of the month for a recurring schedule. Valid values are 1-31. + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + + // OnWeekDay The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + + // Start The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. + Start string `json:"start"` + + // Timezone The timezone of the schedule. The default timezone is UTC. + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + } `json:"schedule"` + Scope *struct { + Alerting struct { + Query struct { + // Kql A filter written in Kibana Query Language (KQL). Only alerts matching this query will be supressed by the maintenance window. + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + } `json:"scope,omitempty"` + + // Title The name of the maintenance window. While this name does not have to be unique, a distinctive name can help you identify a specific maintenance window. + Title string `json:"title"` +} + +// PatchMaintenanceWindowIdJSONBody defines parameters for PatchMaintenanceWindowId. +type PatchMaintenanceWindowIdJSONBody struct { + // Enabled Whether the current maintenance window is enabled. Disabled maintenance windows do not suppress notifications. + Enabled *bool `json:"enabled,omitempty"` + Schedule *struct { + Custom struct { + // Duration The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. + Duration string `json:"duration"` + Recurring *struct { + // End The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`. + End *string `json:"end,omitempty"` + + // Every The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`. + Every *string `json:"every,omitempty"` + + // Occurrences The total number of recurrences of the schedule. + Occurrences *float32 `json:"occurrences,omitempty"` + + // OnMonth The specific months for a recurring schedule. Valid values are 1-12. + OnMonth *[]float32 `json:"onMonth,omitempty"` + + // OnMonthDay The specific days of the month for a recurring schedule. Valid values are 1-31. + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + + // OnWeekDay The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + + // Start The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. + Start string `json:"start"` + + // Timezone The timezone of the schedule. The default timezone is UTC. + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + } `json:"schedule,omitempty"` + Scope *struct { + Alerting struct { + Query struct { + // Kql A filter written in Kibana Query Language (KQL). Only alerts matching this query will be supressed by the maintenance window. + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + } `json:"scope,omitempty"` + + // Title The name of the maintenance window. While this name does not have to be unique, a distinctive name can help you identify a specific maintenance window. + Title *string `json:"title,omitempty"` +} + // PostFleetAgentPoliciesJSONRequestBody defines body for PostFleetAgentPolicies for application/json ContentType. type PostFleetAgentPoliciesJSONRequestBody PostFleetAgentPoliciesJSONBody @@ -3581,6 +3678,12 @@ type CreateDataViewDefaultwJSONRequestBody = DataViewsCreateDataViewRequestObjec // UpdateDataViewDefaultJSONRequestBody defines body for UpdateDataViewDefault for application/json ContentType. type UpdateDataViewDefaultJSONRequestBody = DataViewsUpdateDataViewRequestObject +// PostMaintenanceWindowJSONRequestBody defines body for PostMaintenanceWindow for application/json ContentType. +type PostMaintenanceWindowJSONRequestBody PostMaintenanceWindowJSONBody + +// PatchMaintenanceWindowIdJSONRequestBody defines body for PatchMaintenanceWindowId for application/json ContentType. +type PatchMaintenanceWindowIdJSONRequestBody PatchMaintenanceWindowIdJSONBody + // Getter for additional properties for AgentPolicy_PackagePolicies_1_Elasticsearch_Privileges. Returns the specified // element and whether it was found func (a AgentPolicy_PackagePolicies_1_Elasticsearch_Privileges) Get(fieldName string) (value interface{}, found bool) { @@ -14583,6 +14686,22 @@ type ClientInterface interface { UpdateDataViewDefaultWithBody(ctx context.Context, spaceId SpaceId, viewId DataViewsViewId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) UpdateDataViewDefault(ctx context.Context, spaceId SpaceId, viewId DataViewsViewId, body UpdateDataViewDefaultJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PostMaintenanceWindowWithBody request with any body + PostMaintenanceWindowWithBody(ctx context.Context, spaceId SpaceId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PostMaintenanceWindow(ctx context.Context, spaceId SpaceId, body PostMaintenanceWindowJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteMaintenanceWindowId request + DeleteMaintenanceWindowId(ctx context.Context, spaceId SpaceId, id string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetMaintenanceWindowId request + GetMaintenanceWindowId(ctx context.Context, spaceId SpaceId, id string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PatchMaintenanceWindowIdWithBody request with any body + PatchMaintenanceWindowIdWithBody(ctx context.Context, spaceId SpaceId, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PatchMaintenanceWindowId(ctx context.Context, spaceId SpaceId, id string, body PatchMaintenanceWindowIdJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) } func (c *Client) DeleteAgentConfigurationWithBody(ctx context.Context, params *DeleteAgentConfigurationParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -15401,6 +15520,78 @@ func NewCreateUpdateAgentConfigurationRequestWithBody(server string, params *Cre return req, nil } +func (c *Client) PostMaintenanceWindowWithBody(ctx context.Context, spaceId SpaceId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostMaintenanceWindowRequestWithBody(c.Server, spaceId, 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) PostMaintenanceWindow(ctx context.Context, spaceId SpaceId, body PostMaintenanceWindowJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostMaintenanceWindowRequest(c.Server, spaceId, 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) DeleteMaintenanceWindowId(ctx context.Context, spaceId SpaceId, id string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteMaintenanceWindowIdRequest(c.Server, spaceId, 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) GetMaintenanceWindowId(ctx context.Context, spaceId SpaceId, id string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetMaintenanceWindowIdRequest(c.Server, spaceId, 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) PatchMaintenanceWindowIdWithBody(ctx context.Context, spaceId SpaceId, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPatchMaintenanceWindowIdRequestWithBody(c.Server, spaceId, 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) PatchMaintenanceWindowId(ctx context.Context, spaceId SpaceId, id string, body PatchMaintenanceWindowIdJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPatchMaintenanceWindowIdRequest(c.Server, spaceId, 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) +} + // NewGetFleetAgentPoliciesRequest generates requests for GetFleetAgentPolicies func NewGetFleetAgentPoliciesRequest(server string, params *GetFleetAgentPoliciesParams) (*http.Request, error) { var err error @@ -17488,6 +17679,189 @@ func NewUpdateDataViewDefaultRequestWithBody(server string, spaceId SpaceId, vie return req, nil } +// NewPostMaintenanceWindowRequest calls the generic PostMaintenanceWindow builder with application/json body +func NewPostMaintenanceWindowRequest(server string, spaceId SpaceId, body PostMaintenanceWindowJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPostMaintenanceWindowRequestWithBody(server, spaceId, "application/json", bodyReader) +} + +// NewPostMaintenanceWindowRequestWithBody generates requests for PostMaintenanceWindow with any type of body +func NewPostMaintenanceWindowRequestWithBody(server string, spaceId SpaceId, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "spaceId", runtime.ParamLocationPath, spaceId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/s/%s/api/maintenance_window", pathParam0) + 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 +} + +// NewDeleteMaintenanceWindowIdRequest generates requests for DeleteMaintenanceWindowId +func NewDeleteMaintenanceWindowIdRequest(server string, spaceId SpaceId, id string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "spaceId", runtime.ParamLocationPath, spaceId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, 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("/s/%s/api/maintenance_window/%s", pathParam0, pathParam1) + 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 +} + +// NewGetMaintenanceWindowIdRequest generates requests for GetMaintenanceWindowId +func NewGetMaintenanceWindowIdRequest(server string, spaceId SpaceId, id string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "spaceId", runtime.ParamLocationPath, spaceId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, 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("/s/%s/api/maintenance_window/%s", pathParam0, pathParam1) + 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 +} + +// NewPatchMaintenanceWindowIdRequest calls the generic PatchMaintenanceWindowId builder with application/json body +func NewPatchMaintenanceWindowIdRequest(server string, spaceId SpaceId, id string, body PatchMaintenanceWindowIdJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPatchMaintenanceWindowIdRequestWithBody(server, spaceId, id, "application/json", bodyReader) +} + +// NewPatchMaintenanceWindowIdRequestWithBody generates requests for PatchMaintenanceWindowId with any type of body +func NewPatchMaintenanceWindowIdRequestWithBody(server string, spaceId SpaceId, id string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "spaceId", runtime.ParamLocationPath, spaceId) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, 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("/s/%s/api/maintenance_window/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PATCH", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { for _, r := range c.RequestEditors { if err := r(ctx, req); err != nil { @@ -17676,6 +18050,22 @@ type ClientWithResponsesInterface interface { UpdateDataViewDefaultWithBodyWithResponse(ctx context.Context, spaceId SpaceId, viewId DataViewsViewId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateDataViewDefaultResponse, error) UpdateDataViewDefaultWithResponse(ctx context.Context, spaceId SpaceId, viewId DataViewsViewId, body UpdateDataViewDefaultJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateDataViewDefaultResponse, error) + + // PostMaintenanceWindowWithBodyWithResponse request with any body + PostMaintenanceWindowWithBodyWithResponse(ctx context.Context, spaceId SpaceId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostMaintenanceWindowResponse, error) + + PostMaintenanceWindowWithResponse(ctx context.Context, spaceId SpaceId, body PostMaintenanceWindowJSONRequestBody, reqEditors ...RequestEditorFn) (*PostMaintenanceWindowResponse, error) + + // DeleteMaintenanceWindowIdWithResponse request + DeleteMaintenanceWindowIdWithResponse(ctx context.Context, spaceId SpaceId, id string, reqEditors ...RequestEditorFn) (*DeleteMaintenanceWindowIdResponse, error) + + // GetMaintenanceWindowIdWithResponse request + GetMaintenanceWindowIdWithResponse(ctx context.Context, spaceId SpaceId, id string, reqEditors ...RequestEditorFn) (*GetMaintenanceWindowIdResponse, error) + + // PatchMaintenanceWindowIdWithBodyWithResponse request with any body + PatchMaintenanceWindowIdWithBodyWithResponse(ctx context.Context, spaceId SpaceId, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchMaintenanceWindowIdResponse, error) + + PatchMaintenanceWindowIdWithResponse(ctx context.Context, spaceId SpaceId, id string, body PatchMaintenanceWindowIdJSONRequestBody, reqEditors ...RequestEditorFn) (*PatchMaintenanceWindowIdResponse, error) } type DeleteAgentConfigurationResponse struct { @@ -18904,23 +19294,302 @@ func (c *ClientWithResponses) CreateUpdateAgentConfigurationWithResponse(ctx con return ParseCreateUpdateAgentConfigurationResponse(rsp) } -// GetFleetAgentPoliciesWithResponse request returning *GetFleetAgentPoliciesResponse -func (c *ClientWithResponses) GetFleetAgentPoliciesWithResponse(ctx context.Context, params *GetFleetAgentPoliciesParams, reqEditors ...RequestEditorFn) (*GetFleetAgentPoliciesResponse, error) { - rsp, err := c.GetFleetAgentPolicies(ctx, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseGetFleetAgentPoliciesResponse(rsp) -} +type PostMaintenanceWindowResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + // CreatedAt The date and time when the maintenance window was created. + CreatedAt string `json:"created_at"` -// PostFleetAgentPoliciesWithBodyWithResponse request with arbitrary body returning *PostFleetAgentPoliciesResponse -func (c *ClientWithResponses) PostFleetAgentPoliciesWithBodyWithResponse(ctx context.Context, params *PostFleetAgentPoliciesParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostFleetAgentPoliciesResponse, error) { - rsp, err := c.PostFleetAgentPoliciesWithBody(ctx, params, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostFleetAgentPoliciesResponse(rsp) -} + // CreatedBy The identifier for the user that created the maintenance window. + CreatedBy *string `json:"created_by"` + + // Enabled Whether the current maintenance window is enabled. Disabled maintenance windows do not suppress notifications. + Enabled bool `json:"enabled"` + + // Id The identifier for the maintenance window. + Id string `json:"id"` + Schedule struct { + Custom struct { + // Duration The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. + Duration string `json:"duration"` + Recurring *struct { + // End The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`. + End *string `json:"end,omitempty"` + + // Every The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`. + Every *string `json:"every,omitempty"` + + // Occurrences The total number of recurrences of the schedule. + Occurrences *float32 `json:"occurrences,omitempty"` + + // OnMonth The specific months for a recurring schedule. Valid values are 1-12. + OnMonth *[]float32 `json:"onMonth,omitempty"` + + // OnMonthDay The specific days of the month for a recurring schedule. Valid values are 1-31. + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + + // OnWeekDay The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + + // Start The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. + Start string `json:"start"` + + // Timezone The timezone of the schedule. The default timezone is UTC. + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + } `json:"schedule"` + Scope *struct { + Alerting struct { + Query struct { + // Kql A filter written in Kibana Query Language (KQL). + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + } `json:"scope,omitempty"` + + // Status The current status of the maintenance window. + Status PostMaintenanceWindow200Status `json:"status"` + + // Title The name of the maintenance window. + Title string `json:"title"` + + // UpdatedAt The date and time when the maintenance window was last updated. + UpdatedAt string `json:"updated_at"` + + // UpdatedBy The identifier for the user that last updated this maintenance window. + UpdatedBy *string `json:"updated_by"` + } +} +type PostMaintenanceWindow200Status string + +// Status returns HTTPResponse.Status +func (r PostMaintenanceWindowResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostMaintenanceWindowResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteMaintenanceWindowIdResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r DeleteMaintenanceWindowIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteMaintenanceWindowIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetMaintenanceWindowIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + // CreatedAt The date and time when the maintenance window was created. + CreatedAt string `json:"created_at"` + + // CreatedBy The identifier for the user that created the maintenance window. + CreatedBy *string `json:"created_by"` + + // Enabled Whether the current maintenance window is enabled. Disabled maintenance windows do not suppress notifications. + Enabled bool `json:"enabled"` + + // Id The identifier for the maintenance window. + Id string `json:"id"` + Schedule struct { + Custom struct { + // Duration The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. + Duration string `json:"duration"` + Recurring *struct { + // End The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`. + End *string `json:"end,omitempty"` + + // Every The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`. + Every *string `json:"every,omitempty"` + + // Occurrences The total number of recurrences of the schedule. + Occurrences *float32 `json:"occurrences,omitempty"` + + // OnMonth The specific months for a recurring schedule. Valid values are 1-12. + OnMonth *[]float32 `json:"onMonth,omitempty"` + + // OnMonthDay The specific days of the month for a recurring schedule. Valid values are 1-31. + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + + // OnWeekDay The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + + // Start The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. + Start string `json:"start"` + + // Timezone The timezone of the schedule. The default timezone is UTC. + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + } `json:"schedule"` + Scope *struct { + Alerting struct { + Query struct { + // Kql A filter written in Kibana Query Language (KQL). + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + } `json:"scope,omitempty"` + + // Status The current status of the maintenance window. + Status GetMaintenanceWindowId200Status `json:"status"` + + // Title The name of the maintenance window. + Title string `json:"title"` + + // UpdatedAt The date and time when the maintenance window was last updated. + UpdatedAt string `json:"updated_at"` + + // UpdatedBy The identifier for the user that last updated this maintenance window. + UpdatedBy *string `json:"updated_by"` + } +} +type GetMaintenanceWindowId200Status string + +// Status returns HTTPResponse.Status +func (r GetMaintenanceWindowIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetMaintenanceWindowIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PatchMaintenanceWindowIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + // CreatedAt The date and time when the maintenance window was created. + CreatedAt string `json:"created_at"` + + // CreatedBy The identifier for the user that created the maintenance window. + CreatedBy *string `json:"created_by"` + + // Enabled Whether the current maintenance window is enabled. Disabled maintenance windows do not suppress notifications. + Enabled bool `json:"enabled"` + + // Id The identifier for the maintenance window. + Id string `json:"id"` + Schedule struct { + Custom struct { + // Duration The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. + Duration string `json:"duration"` + Recurring *struct { + // End The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`. + End *string `json:"end,omitempty"` + + // Every The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`. + Every *string `json:"every,omitempty"` + + // Occurrences The total number of recurrences of the schedule. + Occurrences *float32 `json:"occurrences,omitempty"` + + // OnMonth The specific months for a recurring schedule. Valid values are 1-12. + OnMonth *[]float32 `json:"onMonth,omitempty"` + + // OnMonthDay The specific days of the month for a recurring schedule. Valid values are 1-31. + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + + // OnWeekDay The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + + // Start The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. + Start string `json:"start"` + + // Timezone The timezone of the schedule. The default timezone is UTC. + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + } `json:"schedule"` + Scope *struct { + Alerting struct { + Query struct { + // Kql A filter written in Kibana Query Language (KQL). + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + } `json:"scope,omitempty"` + + // Status The current status of the maintenance window. + Status PatchMaintenanceWindowId200Status `json:"status"` + + // Title The name of the maintenance window. + Title string `json:"title"` + + // UpdatedAt The date and time when the maintenance window was last updated. + UpdatedAt string `json:"updated_at"` + + // UpdatedBy The identifier for the user that last updated this maintenance window. + UpdatedBy *string `json:"updated_by"` + } +} +type PatchMaintenanceWindowId200Status string + +// Status returns HTTPResponse.Status +func (r PatchMaintenanceWindowIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PatchMaintenanceWindowIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetFleetAgentPoliciesWithResponse request returning *GetFleetAgentPoliciesResponse +func (c *ClientWithResponses) GetFleetAgentPoliciesWithResponse(ctx context.Context, params *GetFleetAgentPoliciesParams, reqEditors ...RequestEditorFn) (*GetFleetAgentPoliciesResponse, error) { + rsp, err := c.GetFleetAgentPolicies(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetFleetAgentPoliciesResponse(rsp) +} + +// PostFleetAgentPoliciesWithBodyWithResponse request with arbitrary body returning *PostFleetAgentPoliciesResponse +func (c *ClientWithResponses) PostFleetAgentPoliciesWithBodyWithResponse(ctx context.Context, params *PostFleetAgentPoliciesParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostFleetAgentPoliciesResponse, error) { + rsp, err := c.PostFleetAgentPoliciesWithBody(ctx, params, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostFleetAgentPoliciesResponse(rsp) +} func (c *ClientWithResponses) PostFleetAgentPoliciesWithResponse(ctx context.Context, params *PostFleetAgentPoliciesParams, body PostFleetAgentPoliciesJSONRequestBody, reqEditors ...RequestEditorFn) (*PostFleetAgentPoliciesResponse, error) { rsp, err := c.PostFleetAgentPolicies(ctx, params, body, reqEditors...) @@ -19486,6 +20155,58 @@ func ParseCreateUpdateAgentConfigurationResponse(rsp *http.Response) (*CreateUpd return response, nil } +// PostMaintenanceWindowWithBodyWithResponse request with arbitrary body returning *PostMaintenanceWindowResponse +func (c *ClientWithResponses) PostMaintenanceWindowWithBodyWithResponse(ctx context.Context, spaceId SpaceId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostMaintenanceWindowResponse, error) { + rsp, err := c.PostMaintenanceWindowWithBody(ctx, spaceId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostMaintenanceWindowResponse(rsp) +} + +func (c *ClientWithResponses) PostMaintenanceWindowWithResponse(ctx context.Context, spaceId SpaceId, body PostMaintenanceWindowJSONRequestBody, reqEditors ...RequestEditorFn) (*PostMaintenanceWindowResponse, error) { + rsp, err := c.PostMaintenanceWindow(ctx, spaceId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostMaintenanceWindowResponse(rsp) +} + +// DeleteMaintenanceWindowIdWithResponse request returning *DeleteMaintenanceWindowIdResponse +func (c *ClientWithResponses) DeleteMaintenanceWindowIdWithResponse(ctx context.Context, spaceId SpaceId, id string, reqEditors ...RequestEditorFn) (*DeleteMaintenanceWindowIdResponse, error) { + rsp, err := c.DeleteMaintenanceWindowId(ctx, spaceId, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteMaintenanceWindowIdResponse(rsp) +} + +// GetMaintenanceWindowIdWithResponse request returning *GetMaintenanceWindowIdResponse +func (c *ClientWithResponses) GetMaintenanceWindowIdWithResponse(ctx context.Context, spaceId SpaceId, id string, reqEditors ...RequestEditorFn) (*GetMaintenanceWindowIdResponse, error) { + rsp, err := c.GetMaintenanceWindowId(ctx, spaceId, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetMaintenanceWindowIdResponse(rsp) +} + +// PatchMaintenanceWindowIdWithBodyWithResponse request with arbitrary body returning *PatchMaintenanceWindowIdResponse +func (c *ClientWithResponses) PatchMaintenanceWindowIdWithBodyWithResponse(ctx context.Context, spaceId SpaceId, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchMaintenanceWindowIdResponse, error) { + rsp, err := c.PatchMaintenanceWindowIdWithBody(ctx, spaceId, id, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePatchMaintenanceWindowIdResponse(rsp) +} + +func (c *ClientWithResponses) PatchMaintenanceWindowIdWithResponse(ctx context.Context, spaceId SpaceId, id string, body PatchMaintenanceWindowIdJSONRequestBody, reqEditors ...RequestEditorFn) (*PatchMaintenanceWindowIdResponse, error) { + rsp, err := c.PatchMaintenanceWindowId(ctx, spaceId, id, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePatchMaintenanceWindowIdResponse(rsp) +} + // ParseGetFleetAgentPoliciesResponse parses an HTTP response from a GetFleetAgentPoliciesWithResponse call func ParseGetFleetAgentPoliciesResponse(rsp *http.Response) (*GetFleetAgentPoliciesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -20871,3 +21592,286 @@ func ParseUpdateDataViewDefaultResponse(rsp *http.Response) (*UpdateDataViewDefa return response, nil } + +// ParsePostMaintenanceWindowResponse parses an HTTP response from a PostMaintenanceWindowWithResponse call +func ParsePostMaintenanceWindowResponse(rsp *http.Response) (*PostMaintenanceWindowResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostMaintenanceWindowResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + // CreatedAt The date and time when the maintenance window was created. + CreatedAt string `json:"created_at"` + + // CreatedBy The identifier for the user that created the maintenance window. + CreatedBy *string `json:"created_by"` + + // Enabled Whether the current maintenance window is enabled. Disabled maintenance windows do not suppress notifications. + Enabled bool `json:"enabled"` + + // Id The identifier for the maintenance window. + Id string `json:"id"` + Schedule struct { + Custom struct { + // Duration The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. + Duration string `json:"duration"` + Recurring *struct { + // End The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`. + End *string `json:"end,omitempty"` + + // Every The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`. + Every *string `json:"every,omitempty"` + + // Occurrences The total number of recurrences of the schedule. + Occurrences *float32 `json:"occurrences,omitempty"` + + // OnMonth The specific months for a recurring schedule. Valid values are 1-12. + OnMonth *[]float32 `json:"onMonth,omitempty"` + + // OnMonthDay The specific days of the month for a recurring schedule. Valid values are 1-31. + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + + // OnWeekDay The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + + // Start The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. + Start string `json:"start"` + + // Timezone The timezone of the schedule. The default timezone is UTC. + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + } `json:"schedule"` + Scope *struct { + Alerting struct { + Query struct { + // Kql A filter written in Kibana Query Language (KQL). + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + } `json:"scope,omitempty"` + + // Status The current status of the maintenance window. + Status PostMaintenanceWindow200Status `json:"status"` + + // Title The name of the maintenance window. + Title string `json:"title"` + + // UpdatedAt The date and time when the maintenance window was last updated. + UpdatedAt string `json:"updated_at"` + + // UpdatedBy The identifier for the user that last updated this maintenance window. + UpdatedBy *string `json:"updated_by"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseDeleteMaintenanceWindowIdResponse parses an HTTP response from a DeleteMaintenanceWindowIdWithResponse call +func ParseDeleteMaintenanceWindowIdResponse(rsp *http.Response) (*DeleteMaintenanceWindowIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteMaintenanceWindowIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseGetMaintenanceWindowIdResponse parses an HTTP response from a GetMaintenanceWindowIdWithResponse call +func ParseGetMaintenanceWindowIdResponse(rsp *http.Response) (*GetMaintenanceWindowIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetMaintenanceWindowIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + // CreatedAt The date and time when the maintenance window was created. + CreatedAt string `json:"created_at"` + + // CreatedBy The identifier for the user that created the maintenance window. + CreatedBy *string `json:"created_by"` + + // Enabled Whether the current maintenance window is enabled. Disabled maintenance windows do not suppress notifications. + Enabled bool `json:"enabled"` + + // Id The identifier for the maintenance window. + Id string `json:"id"` + Schedule struct { + Custom struct { + // Duration The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. + Duration string `json:"duration"` + Recurring *struct { + // End The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`. + End *string `json:"end,omitempty"` + + // Every The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`. + Every *string `json:"every,omitempty"` + + // Occurrences The total number of recurrences of the schedule. + Occurrences *float32 `json:"occurrences,omitempty"` + + // OnMonth The specific months for a recurring schedule. Valid values are 1-12. + OnMonth *[]float32 `json:"onMonth,omitempty"` + + // OnMonthDay The specific days of the month for a recurring schedule. Valid values are 1-31. + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + + // OnWeekDay The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + + // Start The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. + Start string `json:"start"` + + // Timezone The timezone of the schedule. The default timezone is UTC. + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + } `json:"schedule"` + Scope *struct { + Alerting struct { + Query struct { + // Kql A filter written in Kibana Query Language (KQL). + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + } `json:"scope,omitempty"` + + // Status The current status of the maintenance window. + Status GetMaintenanceWindowId200Status `json:"status"` + + // Title The name of the maintenance window. + Title string `json:"title"` + + // UpdatedAt The date and time when the maintenance window was last updated. + UpdatedAt string `json:"updated_at"` + + // UpdatedBy The identifier for the user that last updated this maintenance window. + UpdatedBy *string `json:"updated_by"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParsePatchMaintenanceWindowIdResponse parses an HTTP response from a PatchMaintenanceWindowIdWithResponse call +func ParsePatchMaintenanceWindowIdResponse(rsp *http.Response) (*PatchMaintenanceWindowIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PatchMaintenanceWindowIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + // CreatedAt The date and time when the maintenance window was created. + CreatedAt string `json:"created_at"` + + // CreatedBy The identifier for the user that created the maintenance window. + CreatedBy *string `json:"created_by"` + + // Enabled Whether the current maintenance window is enabled. Disabled maintenance windows do not suppress notifications. + Enabled bool `json:"enabled"` + + // Id The identifier for the maintenance window. + Id string `json:"id"` + Schedule struct { + Custom struct { + // Duration The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. + Duration string `json:"duration"` + Recurring *struct { + // End The end date of a recurring schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-04-01T00:00:00.000Z`. + End *string `json:"end,omitempty"` + + // Every The interval and frequency of a recurring schedule. It allows values in `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`. + Every *string `json:"every,omitempty"` + + // Occurrences The total number of recurrences of the schedule. + Occurrences *float32 `json:"occurrences,omitempty"` + + // OnMonth The specific months for a recurring schedule. Valid values are 1-12. + OnMonth *[]float32 `json:"onMonth,omitempty"` + + // OnMonthDay The specific days of the month for a recurring schedule. Valid values are 1-31. + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + + // OnWeekDay The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + + // Start The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. + Start string `json:"start"` + + // Timezone The timezone of the schedule. The default timezone is UTC. + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + } `json:"schedule"` + Scope *struct { + Alerting struct { + Query struct { + // Kql A filter written in Kibana Query Language (KQL). + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + } `json:"scope,omitempty"` + + // Status The current status of the maintenance window. + Status PatchMaintenanceWindowId200Status `json:"status"` + + // Title The name of the maintenance window. + Title string `json:"title"` + + // UpdatedAt The date and time when the maintenance window was last updated. + UpdatedAt string `json:"updated_at"` + + // UpdatedBy The identifier for the user that last updated this maintenance window. + UpdatedBy *string `json:"updated_by"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} diff --git a/generated/kbapi/transform_schema.go b/generated/kbapi/transform_schema.go index 11453eb8a..99681a123 100644 --- a/generated/kbapi/transform_schema.go +++ b/generated/kbapi/transform_schema.go @@ -108,6 +108,7 @@ func (s Schema) MustGetPath(path string) *Path { type Path struct { Parameters []Map `yaml:"parameters,omitempty"` Get Map `yaml:"get,omitempty"` + Patch Map `yaml:"patch,omitempty"` Post Map `yaml:"post,omitempty"` Put Map `yaml:"put,omitempty"` Delete Map `yaml:"delete,omitempty"` @@ -123,6 +124,9 @@ func (p Path) Endpoints(yield func(key string, endpoint Map) bool) { if p.Put != nil { yield("put", p.Put) } + if p.Patch != nil { + yield("patch", p.Patch) + } if p.Delete != nil { yield("delete", p.Delete) } @@ -136,6 +140,8 @@ func (p Path) GetEndpoint(method string) Map { return p.Post case "put": return p.Put + case "patch": + return p.Patch case "delete": return p.Delete default: @@ -160,6 +166,8 @@ func (p *Path) SetEndpoint(method string, endpoint Map) { p.Post = endpoint case "put": p.Put = endpoint + case "patch": + p.Put = endpoint case "delete": p.Delete = endpoint default: @@ -571,6 +579,8 @@ func transformFilterPaths(schema *Schema) { "/api/synthetics/params": {"post"}, "/api/synthetics/params/{id}": {"get", "put", "delete"}, "/api/apm/settings/agent-configuration": {"get", "put", "delete"}, + "/api/maintenance_window": {"post"}, + "/api/maintenance_window/{id}": {"delete", "get", "patch"}, } for path, pathInfo := range schema.Paths { @@ -717,6 +727,8 @@ func transformKibanaPaths(schema *Schema) { "/api/data_views", "/api/data_views/data_view", "/api/data_views/data_view/{viewId}", + "/api/maintenance_window", + "/api/maintenance_window/{id}", } // Add a spaceId parameter if not already present diff --git a/internal/clients/kibana_oapi/maintenance_window.go b/internal/clients/kibana_oapi/maintenance_window.go new file mode 100644 index 000000000..0f27040a3 --- /dev/null +++ b/internal/clients/kibana_oapi/maintenance_window.go @@ -0,0 +1,73 @@ +package kibana_oapi + +import ( + "context" + "net/http" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +// GetMaintenanceWindow reads a maintenance window from the API by ID +func GetMaintenanceWindow(ctx context.Context, client *Client, spaceID string, maintenanceWindowID string) (*kbapi.GetMaintenanceWindowIdResponse, diag.Diagnostics) { + resp, err := client.API.GetMaintenanceWindowIdWithResponse(ctx, spaceID, maintenanceWindowID) + + if err != nil { + return nil, utils.FrameworkDiagFromError(err) + } + + switch resp.StatusCode() { + case http.StatusOK: + return resp, nil + default: + return nil, reportUnknownError(resp.StatusCode(), resp.Body) + } +} + +// CreateMaintenanceWindow creates a new maintenance window. +func CreateMaintenanceWindow(ctx context.Context, client *Client, spaceID string, body kbapi.PostMaintenanceWindowJSONRequestBody) (*kbapi.PostMaintenanceWindowResponse, diag.Diagnostics) { + resp, err := client.API.PostMaintenanceWindowWithResponse(ctx, spaceID, body) + if err != nil { + return nil, utils.FrameworkDiagFromError(err) + } + + switch resp.StatusCode() { + case http.StatusOK: + return resp, nil + default: + return nil, reportUnknownError(resp.StatusCode(), resp.Body) + } +} + +// UpdateMaintenanceWindow updates an existing maintenance window. +func UpdateMaintenanceWindow(ctx context.Context, client *Client, spaceID string, maintenanceWindowID string, req kbapi.PatchMaintenanceWindowIdJSONRequestBody) (*kbapi.PatchMaintenanceWindowIdResponse, diag.Diagnostics) { + resp, err := client.API.PatchMaintenanceWindowIdWithResponse(ctx, spaceID, maintenanceWindowID, req) + if err != nil { + return nil, utils.FrameworkDiagFromError(err) + } + + switch resp.StatusCode() { + case http.StatusOK: + return resp, nil + default: + return nil, reportUnknownError(resp.StatusCode(), resp.Body) + } +} + +// DeleteMaintenanceWindow deletes an existing maintenance window. +func DeleteMaintenanceWindow(ctx context.Context, client *Client, spaceID string, maintenanceWindowID string) diag.Diagnostics { + resp, err := client.API.DeleteMaintenanceWindowIdWithResponse(ctx, spaceID, maintenanceWindowID) + if err != nil { + return utils.FrameworkDiagFromError(err) + } + + switch resp.StatusCode() { + case http.StatusNoContent: + return nil + case http.StatusNotFound: + return nil + default: + return reportUnknownError(resp.StatusCode(), resp.Body) + } +} diff --git a/internal/kibana/maintenance_window/acc_test.go b/internal/kibana/maintenance_window/acc_test.go new file mode 100644 index 000000000..009a44848 --- /dev/null +++ b/internal/kibana/maintenance_window/acc_test.go @@ -0,0 +1,153 @@ +package maintenance_window_test + +import ( + "fmt" + "testing" + + "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" + "github.com/hashicorp/go-version" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +var minDataViewAPISupport = version.Must(version.NewVersion("8.1.0")) +var minFullDataviewSupport = version.Must(version.NewVersion("8.8.0")) + +func TestAccResourceDataView(t *testing.T) { + indexName := "my-index-" + sdkacctest.RandStringFromCharSet(4, sdkacctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minDataViewAPISupport), + Config: testAccResourceDataViewPre8_8DV(indexName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("elasticstack_kibana_data_view.dv", "id"), + ), + }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minFullDataviewSupport), + Config: testAccResourceDataViewBasicDV(indexName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("elasticstack_kibana_data_view.dv", "id"), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "override", "true"), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.name", indexName), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.source_filters.#", "2"), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.field_formats.event_time.id", "date_nanos"), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.field_formats.machine.ram.params.pattern", "0,0.[000] b"), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.runtime_field_map.runtime_shape_name.script_source", "emit(doc['shape_name'].value)"), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.field_attrs.ingest_failure.custom_label", "error.ingest_failure"), + ), + }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minFullDataviewSupport), + Config: testAccResourceDataViewBasicDVUpdated(indexName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("elasticstack_kibana_data_view.dv", "id"), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "override", "false"), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.name", indexName), + resource.TestCheckNoResourceAttr("elasticstack_kibana_data_view.dv", "data_view.source_filters"), + resource.TestCheckNoResourceAttr("elasticstack_kibana_data_view.dv", "data_view.field_formats"), + resource.TestCheckNoResourceAttr("elasticstack_kibana_data_view.dv", "data_view.runtime_field_map"), + ), + }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minFullDataviewSupport), + Config: testAccResourceDataViewBasicDVUpdated(indexName), + ImportState: true, + ImportStateVerify: true, + ResourceName: "elasticstack_kibana_data_view.dv", + }, + }, + }) +} + +func testAccResourceDataViewPre8_8DV(indexName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_elasticsearch_index" "my_index" { + name = "%s" + deletion_protection = false +} + +resource "elasticstack_kibana_data_view" "dv" { + data_view = { + title = "%s*" + } +}`, indexName, indexName) +} + +func testAccResourceDataViewBasicDV(indexName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_elasticsearch_index" "my_index" { + name = "%s" + deletion_protection = false +} + +resource "elasticstack_kibana_data_view" "dv" { + override = true + data_view = { + title = "%s*" + name = "%s" + time_field_name = "@timestamp" + source_filters = ["event_time", "machine.ram"] + allow_no_index = true + namespaces = ["default", "foo", "bar"] + field_formats = { + event_time = { + id = "date_nanos" + } + "machine.ram" = { + id = "number" + params = { + pattern = "0,0.[000] b" + } + } + } + runtime_field_map = { + runtime_shape_name = { + type = "keyword" + script_source = "emit(doc['shape_name'].value)" + } + } + field_attrs = { + ingest_failure = { custom_label = "error.ingest_failure", count = 6 }, + } + } +}`, indexName, indexName, indexName) +} + +func testAccResourceDataViewBasicDVUpdated(indexName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_elasticsearch_index" "my_index" { + name = "%s" + deletion_protection = false +} + +resource "elasticstack_kibana_data_view" "dv" { + override = false + data_view = { + title = "%s*" + name = "%s" + time_field_name = "@timestamp" + allow_no_index = true + } +}`, indexName, indexName, indexName) +} diff --git a/internal/kibana/maintenance_window/create.go b/internal/kibana/maintenance_window/create.go new file mode 100644 index 000000000..3a82547f9 --- /dev/null +++ b/internal/kibana/maintenance_window/create.go @@ -0,0 +1,50 @@ +package maintenance_window + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *MaintenanceWindowResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var planMaintenanceWindow MaintenanceWindowModel + + diags := req.Plan.Get(ctx, &planMaintenanceWindow) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from plan + body, diags := planMaintenanceWindow.toAPICreateRequest(ctx) + + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + spaceID := planMaintenanceWindow.SpaceID.ValueString() + maintenanceWindowAPIResponse, diags := kibana_oapi.CreateMaintenanceWindow(ctx, client, spaceID, body) + + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = planMaintenanceWindow.fromAPICreateResponse(ctx, maintenanceWindowAPIResponse) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, planMaintenanceWindow) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/kibana/maintenance_window/delete.go b/internal/kibana/maintenance_window/delete.go new file mode 100644 index 000000000..8d4316d83 --- /dev/null +++ b/internal/kibana/maintenance_window/delete.go @@ -0,0 +1,28 @@ +package maintenance_window + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *MaintenanceWindowResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var stateModel MaintenanceWindowModel + + diags := req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + viewID, spaceID := stateModel.getMaintenanceWindowIDAndSpaceID() + diags = kibana_oapi.DeleteMaintenanceWindow(ctx, client, spaceID, viewID) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go new file mode 100644 index 000000000..f6ecdfd6f --- /dev/null +++ b/internal/kibana/maintenance_window/models.go @@ -0,0 +1,446 @@ +package maintenance_window + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type MaintenanceWindowModel struct { + ID types.String `tfsdk:"id"` + SpaceID types.String `tfsdk:"space_id"` + Title types.String `tfsdk:"title"` + Enabled types.Bool `tfsdk:"enabled"` + CustomSchedule MaintenanceWindowSchedule `tfsdk:"custom_schedule"` + Scope *MaintenanceWindowScope `tfsdk:"scope"` +} + +type MaintenanceWindowScope struct { + Alerting MaintenanceWindowAlertingScope `tfsdk:"alerting"` +} + +type MaintenanceWindowAlertingScope struct { + Kql types.String `tfsdk:"kql"` +} + +type MaintenanceWindowSchedule struct { + Start types.String `tfsdk:"start"` + Duration types.String `tfsdk:"duration"` + Timezone types.String `tfsdk:"timezone"` + Recurring *MaintenanceWindowScheduleRecurring `tfsdk:"recurring"` +} + +type MaintenanceWindowScheduleRecurring struct { + End types.String `tfsdk:"end"` + Every types.String `tfsdk:"every"` + Occurrences types.Float32 `tfsdk:"occurrences"` + OnWeekDay types.List `tfsdk:"on_week_day"` + OnMonthDay types.List `tfsdk:"on_month_day"` + OnMonth types.List `tfsdk:"on_month"` +} + +/* CREATE */ + +func (model MaintenanceWindowModel) toAPICreateRequest(ctx context.Context) (kbapi.PostMaintenanceWindowJSONRequestBody, diag.Diagnostics) { + var diags diag.Diagnostics + + body := kbapi.PostMaintenanceWindowJSONRequestBody{ + Enabled: model.Enabled.ValueBoolPointer(), + Title: *model.Title.ValueStringPointer(), + } + + body.Schedule.Custom.Duration = model.CustomSchedule.Duration.ValueString() + body.Schedule.Custom.Start = model.CustomSchedule.Start.ValueString() + + if !model.CustomSchedule.Timezone.IsNull() && !model.CustomSchedule.Timezone.IsUnknown() { + body.Schedule.Custom.Timezone = model.CustomSchedule.Timezone.ValueStringPointer() + } + + if model.CustomSchedule.Recurring != nil { + + body.Schedule.Custom.Recurring = &struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + }{ + End: model.CustomSchedule.Recurring.End.ValueStringPointer(), + Every: model.CustomSchedule.Recurring.Every.ValueStringPointer(), + } + + if !model.CustomSchedule.Recurring.Occurrences.IsNull() && !model.CustomSchedule.Recurring.Occurrences.IsUnknown() && model.CustomSchedule.Recurring.Occurrences.ValueFloat32() > 0 { + body.Schedule.Custom.Recurring.Occurrences = model.CustomSchedule.Recurring.Occurrences.ValueFloat32Pointer() + } + + if !model.CustomSchedule.Recurring.OnWeekDay.IsNull() && !model.CustomSchedule.Recurring.OnWeekDay.IsUnknown() { + var onWeekDay []string + diags = model.CustomSchedule.Recurring.OnWeekDay.ElementsAs(ctx, &onWeekDay, true) + body.Schedule.Custom.Recurring.OnWeekDay = &onWeekDay + } + + if !model.CustomSchedule.Recurring.OnMonth.IsNull() && !model.CustomSchedule.Recurring.OnMonth.IsUnknown() { + var onMonth []float32 + diags = model.CustomSchedule.Recurring.OnMonth.ElementsAs(ctx, &onMonth, true) + body.Schedule.Custom.Recurring.OnMonth = &onMonth + } + + if !model.CustomSchedule.Recurring.OnMonthDay.IsNull() && !model.CustomSchedule.Recurring.OnMonthDay.IsUnknown() { + var onMonthDay []float32 + diags = model.CustomSchedule.Recurring.OnMonthDay.ElementsAs(ctx, &onMonthDay, true) + body.Schedule.Custom.Recurring.OnMonthDay = &onMonthDay + } + } + + if model.Scope != nil { + // Yes, I hate it too + body.Scope = &struct { + Alerting struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + }{ + Alerting: struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + }{ + Query: struct { + Kql string `json:"kql"` + }{ + Kql: model.Scope.Alerting.Kql.ValueString(), + }, + }, + } + } + + return body, diags +} + +func (model *MaintenanceWindowModel) fromAPICreateResponse(ctx context.Context, data *kbapi.PostMaintenanceWindowResponse) diag.Diagnostics { + if data == nil { + return nil + } + + response := data.JSON200 + + var diags diag.Diagnostics + + resourceID := clients.CompositeId{ + ClusterId: model.SpaceID.ValueString(), + ResourceId: response.Id, + } + + model.ID = types.StringValue(resourceID.String()) + model.Title = types.StringValue(response.Title) + model.Enabled = types.BoolValue(response.Enabled) + + model.CustomSchedule = MaintenanceWindowSchedule{ + Start: types.StringValue(response.Schedule.Custom.Start), + Duration: types.StringValue(response.Schedule.Custom.Duration), + Timezone: types.StringPointerValue(response.Schedule.Custom.Timezone), + } + + if response.Schedule.Custom.Recurring != nil { + model.CustomSchedule.Recurring = &MaintenanceWindowScheduleRecurring{ + End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), + Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), + Occurrences: types.Float32PointerValue(response.Schedule.Custom.Recurring.Occurrences), + OnWeekDay: types.ListNull(types.StringType), + OnMonth: types.ListNull(types.Float32Type), + OnMonthDay: types.ListNull(types.Float32Type), + } + + if response.Schedule.Custom.Recurring.OnWeekDay != nil { + onWeekDay, _ := types.ListValueFrom(ctx, types.StringType, response.Schedule.Custom.Recurring.OnWeekDay) + model.CustomSchedule.Recurring.OnWeekDay = onWeekDay + } + + if response.Schedule.Custom.Recurring.OnMonth != nil { + onMonth, _ := types.ListValueFrom(ctx, types.Float32Type, response.Schedule.Custom.Recurring.OnMonth) + model.CustomSchedule.Recurring.OnMonth = onMonth + } + + if response.Schedule.Custom.Recurring.OnMonthDay != nil { + onMonthDay, _ := types.ListValueFrom(ctx, types.Float32Type, response.Schedule.Custom.Recurring.OnMonthDay) + model.CustomSchedule.Recurring.OnMonthDay = onMonthDay + } + } + + if response.Scope != nil { + model.Scope = &MaintenanceWindowScope{ + Alerting: MaintenanceWindowAlertingScope{ + Kql: types.StringValue(response.Scope.Alerting.Query.Kql), + }, + } + } + + return diags +} + +/* READ */ + +func (model *MaintenanceWindowModel) fromAPIReadResponse(ctx context.Context, data *kbapi.GetMaintenanceWindowIdResponse) diag.Diagnostics { + if data == nil { + return nil + } + + response := data.JSON200 + + var diags diag.Diagnostics + + resourceID := clients.CompositeId{ + ClusterId: model.SpaceID.ValueString(), + ResourceId: response.Id, + } + + model.ID = types.StringValue(resourceID.String()) + model.Title = types.StringValue(response.Title) + model.Enabled = types.BoolValue(response.Enabled) + + model.CustomSchedule = MaintenanceWindowSchedule{ + Start: types.StringValue(response.Schedule.Custom.Start), + Duration: types.StringValue(response.Schedule.Custom.Duration), + Timezone: types.StringPointerValue(response.Schedule.Custom.Timezone), + } + + if response.Schedule.Custom.Recurring != nil { + model.CustomSchedule.Recurring = &MaintenanceWindowScheduleRecurring{ + End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), + Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), + Occurrences: types.Float32PointerValue(response.Schedule.Custom.Recurring.Occurrences), + OnWeekDay: types.ListNull(types.StringType), + OnMonth: types.ListNull(types.Float32Type), + OnMonthDay: types.ListNull(types.Float32Type), + } + + if response.Schedule.Custom.Recurring.OnWeekDay != nil { + onWeekDay, _ := types.ListValueFrom(ctx, types.StringType, response.Schedule.Custom.Recurring.OnWeekDay) + model.CustomSchedule.Recurring.OnWeekDay = onWeekDay + } + + if response.Schedule.Custom.Recurring.OnMonth != nil { + onMonth, _ := types.ListValueFrom(ctx, types.Float32Type, response.Schedule.Custom.Recurring.OnMonth) + model.CustomSchedule.Recurring.OnMonth = onMonth + } + + if response.Schedule.Custom.Recurring.OnMonthDay != nil { + onMonthDay, _ := types.ListValueFrom(ctx, types.Float32Type, response.Schedule.Custom.Recurring.OnMonthDay) + model.CustomSchedule.Recurring.OnMonthDay = onMonthDay + } + } + + if response.Scope != nil { + model.Scope = &MaintenanceWindowScope{ + Alerting: MaintenanceWindowAlertingScope{ + Kql: types.StringValue(response.Scope.Alerting.Query.Kql), + }, + } + } + + return diags +} + +/* UPDATE */ + +func (model MaintenanceWindowModel) toAPIUpdateRequest(ctx context.Context) (kbapi.PatchMaintenanceWindowIdJSONRequestBody, diag.Diagnostics) { + var diags diag.Diagnostics + + body := kbapi.PatchMaintenanceWindowIdJSONRequestBody{} + + if !model.Enabled.IsNull() { + body.Enabled = model.Enabled.ValueBoolPointer() + } + + if !model.Title.IsNull() { + body.Title = model.Title.ValueStringPointer() + } + + schedule := struct { + Custom struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + }{ + Custom: struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + }{ + Duration: model.CustomSchedule.Duration.ValueString(), + Start: model.CustomSchedule.Start.ValueString(), + }, + } + + body.Schedule = &schedule + + if !model.CustomSchedule.Timezone.IsNull() && !model.CustomSchedule.Timezone.IsUnknown() { + body.Schedule.Custom.Timezone = model.CustomSchedule.Timezone.ValueStringPointer() + } + + if model.CustomSchedule.Recurring != nil { + + body.Schedule.Custom.Recurring = &struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + }{} + + if !model.CustomSchedule.Recurring.End.IsNull() { + body.Schedule.Custom.Recurring.End = model.CustomSchedule.Recurring.End.ValueStringPointer() + } + + if !model.CustomSchedule.Recurring.Every.IsNull() { + body.Schedule.Custom.Recurring.Every = model.CustomSchedule.Recurring.Every.ValueStringPointer() + } + + if !model.CustomSchedule.Recurring.Occurrences.IsNull() && !model.CustomSchedule.Recurring.Occurrences.IsUnknown() && model.CustomSchedule.Recurring.Occurrences.ValueFloat32() > 0 { + body.Schedule.Custom.Recurring.Occurrences = model.CustomSchedule.Recurring.Occurrences.ValueFloat32Pointer() + } + + if !model.CustomSchedule.Recurring.OnWeekDay.IsNull() && !model.CustomSchedule.Recurring.OnWeekDay.IsUnknown() { + var onWeekDay []string + diags = model.CustomSchedule.Recurring.OnWeekDay.ElementsAs(ctx, &onWeekDay, true) + body.Schedule.Custom.Recurring.OnWeekDay = &onWeekDay + } + + if !model.CustomSchedule.Recurring.OnMonth.IsNull() && !model.CustomSchedule.Recurring.OnMonth.IsUnknown() { + var onMonth []float32 + diags = model.CustomSchedule.Recurring.OnMonth.ElementsAs(ctx, &onMonth, true) + body.Schedule.Custom.Recurring.OnMonth = &onMonth + } + + if !model.CustomSchedule.Recurring.OnMonthDay.IsNull() && !model.CustomSchedule.Recurring.OnMonthDay.IsUnknown() { + var onMonthDay []float32 + diags = model.CustomSchedule.Recurring.OnMonthDay.ElementsAs(ctx, &onMonthDay, true) + body.Schedule.Custom.Recurring.OnMonthDay = &onMonthDay + } + } + + if model.Scope != nil { + // Yes, I hate it too + body.Scope = &struct { + Alerting struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + }{ + Alerting: struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + }{ + Query: struct { + Kql string `json:"kql"` + }{ + Kql: model.Scope.Alerting.Kql.ValueString(), + }, + }, + } + } + + return body, diags +} + +func (model *MaintenanceWindowModel) fromAPIUpdateResponse(ctx context.Context, data *kbapi.PatchMaintenanceWindowIdResponse, spaceID string) diag.Diagnostics { + if data == nil { + return nil + } + + response := data.JSON200 + + var diags diag.Diagnostics + + resourceID := clients.CompositeId{ + ClusterId: model.SpaceID.ValueString(), + ResourceId: response.Id, + } + + model.ID = types.StringValue(resourceID.String()) + model.Title = types.StringValue(response.Title) + model.Enabled = types.BoolValue(response.Enabled) + + model.CustomSchedule = MaintenanceWindowSchedule{ + Start: types.StringValue(response.Schedule.Custom.Start), + Duration: types.StringValue(response.Schedule.Custom.Duration), + Timezone: types.StringPointerValue(response.Schedule.Custom.Timezone), + } + + if response.Schedule.Custom.Recurring != nil { + model.CustomSchedule.Recurring = &MaintenanceWindowScheduleRecurring{ + End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), + Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), + Occurrences: types.Float32PointerValue(response.Schedule.Custom.Recurring.Occurrences), + OnWeekDay: types.ListNull(types.StringType), + OnMonth: types.ListNull(types.Float32Type), + OnMonthDay: types.ListNull(types.Float32Type), + } + + if response.Schedule.Custom.Recurring.OnWeekDay != nil { + onWeekDay, _ := types.ListValueFrom(ctx, types.StringType, response.Schedule.Custom.Recurring.OnWeekDay) + model.CustomSchedule.Recurring.OnWeekDay = onWeekDay + } + + if response.Schedule.Custom.Recurring.OnMonth != nil { + onMonth, _ := types.ListValueFrom(ctx, types.Float32Type, response.Schedule.Custom.Recurring.OnMonth) + model.CustomSchedule.Recurring.OnMonth = onMonth + } + + if response.Schedule.Custom.Recurring.OnMonthDay != nil { + onMonthDay, _ := types.ListValueFrom(ctx, types.Float32Type, response.Schedule.Custom.Recurring.OnMonthDay) + model.CustomSchedule.Recurring.OnMonthDay = onMonthDay + } + } + + if response.Scope != nil { + model.Scope = &MaintenanceWindowScope{ + Alerting: MaintenanceWindowAlertingScope{ + Kql: types.StringValue(response.Scope.Alerting.Query.Kql), + }, + } + } + + return diags +} + +/* DELETE */ + +func (model MaintenanceWindowModel) getMaintenanceWindowIDAndSpaceID() (maintenanceWindowID string, spaceID string) { + maintenanceWindowID = model.ID.ValueString() + spaceID = model.SpaceID.ValueString() + + resourceID := model.ID.ValueString() + maybeCompositeID, _ := clients.CompositeIdFromStr(resourceID) + if maybeCompositeID != nil { + maintenanceWindowID = maybeCompositeID.ResourceId + spaceID = maybeCompositeID.ClusterId + } + + return +} diff --git a/internal/kibana/maintenance_window/read.go b/internal/kibana/maintenance_window/read.go new file mode 100644 index 000000000..52a404570 --- /dev/null +++ b/internal/kibana/maintenance_window/read.go @@ -0,0 +1,45 @@ +package maintenance_window + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *MaintenanceWindowResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var stateModel MaintenanceWindowModel + + diags := req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + viewID, spaceID := stateModel.getMaintenanceWindowIDAndSpaceID() + dataView, diags := kibana_oapi.GetMaintenanceWindow(ctx, client, spaceID, viewID) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if dataView == nil { + resp.State.RemoveResource(ctx) + return + } + + diags = stateModel.fromAPIReadResponse(ctx, dataView) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, stateModel) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/kibana/maintenance_window/resource.go b/internal/kibana/maintenance_window/resource.go new file mode 100644 index 000000000..14d44c686 --- /dev/null +++ b/internal/kibana/maintenance_window/resource.go @@ -0,0 +1,34 @@ +package maintenance_window + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var ( + _ resource.Resource = &MaintenanceWindowResource{} + _ resource.ResourceWithConfigure = &MaintenanceWindowResource{} +) + +// NewResource is a helper function to simplify the provider implementation. +func NewResource() resource.Resource { + return &MaintenanceWindowResource{} +} + +type MaintenanceWindowResource struct { + client *clients.ApiClient +} + +func (r *MaintenanceWindowResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + client, diags := clients.ConvertProviderData(req.ProviderData) + resp.Diagnostics.Append(diags...) + r.client = client +} + +// Metadata returns the provider type name. +func (r *MaintenanceWindowResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "kibana_maintenance_window_2") +} diff --git a/internal/kibana/maintenance_window/schema.go b/internal/kibana/maintenance_window/schema.go new file mode 100644 index 000000000..087edc9d1 --- /dev/null +++ b/internal/kibana/maintenance_window/schema.go @@ -0,0 +1,146 @@ +package maintenance_window + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/float32validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "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" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages Kibana data views", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Generated ID for the data view.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "space_id": schema.StringAttribute{ + Description: "An identifier for the space. If space_id is not provided, the default space is used.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("default"), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "title": schema.StringAttribute{ + Description: "The name of the maintenance window.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "enabled": schema.BoolAttribute{ + Description: "Whether the current maintenance window is enabled.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "custom_schedule": schema.SingleNestedAttribute{ + Description: "A set schedule over which the maintenance window applies.", + Required: true, + Attributes: map[string]schema.Attribute{ + "start": schema.StringAttribute{ + Description: "The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.", + Required: true, + }, + "duration": schema.StringAttribute{ + Description: "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", + Required: true, + Validators: []validator.String{ + StringIsAlertingDuration{}, + }, + }, + "timezone": schema.StringAttribute{ + Description: "The timezone of the schedule. The default timezone is UTC.", + Optional: true, + Computed: true, + }, + "recurring": schema.SingleNestedAttribute{ + Description: "A set schedule over which the maintenance window applies.", + Required: true, + Attributes: map[string]schema.Attribute{ + "end": schema.StringAttribute{ + Description: "The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.", + Optional: true, + }, + "every": schema.StringAttribute{ + Description: "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", + Optional: true, + Validators: []validator.String{ + StringIsMaintenanceWindowIntervalFrequency{}, + }, + }, + "occurrences": schema.Float32Attribute{ + Description: "The total number of recurrences of the schedule.", + Optional: true, + Validators: []validator.Float32{ + float32validator.AtLeast(1), + }, + }, + "on_week_day": schema.ListAttribute{ + Description: "The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule.", + ElementType: types.StringType, + Optional: true, + Validators: []validator.List{ + listvalidator.ValueStringsAre( + StringIsMaintenanceWindowOnWeekDay{}, + ), + }, + }, + "on_month_day": schema.ListAttribute{ + Description: "The specific days of the month for a recurring schedule. Valid values are 1-31.", + ElementType: types.Float32Type, + Optional: true, + Validators: []validator.List{ + listvalidator.ValueFloat32sAre( + float32validator.Between(1, 31), + ), + }, + }, + "on_month": schema.ListAttribute{ + Description: "The specific months for a recurring schedule. Valid values are 1-12.", + ElementType: types.Float32Type, + Optional: true, + Validators: []validator.List{ + listvalidator.ValueFloat32sAre( + float32validator.Between(1, 12), + ), + }, + }, + }, + }, + }, + }, + "scope": schema.SingleNestedAttribute{ + Description: "An object that narrows the scope of what is affected by this maintenance window.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "alerting": schema.SingleNestedAttribute{ + Description: "A set schedule over which the maintenance window applies.", + Required: true, + Attributes: map[string]schema.Attribute{ + "kql": schema.StringAttribute{ + Description: "A filter written in Kibana Query Language (KQL).", + Required: true, + }, + }, + }, + }, + }, + }, + } +} diff --git a/internal/kibana/maintenance_window/update.go b/internal/kibana/maintenance_window/update.go new file mode 100644 index 000000000..975864936 --- /dev/null +++ b/internal/kibana/maintenance_window/update.go @@ -0,0 +1,46 @@ +package maintenance_window + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *MaintenanceWindowResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var planMaintenanceWindow MaintenanceWindowModel + + diags := req.Plan.Get(ctx, &planMaintenanceWindow) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + body, diags := planMaintenanceWindow.toAPIUpdateRequest(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + viewID, spaceID := planMaintenanceWindow.getMaintenanceWindowIDAndSpaceID() + maintenanceWindow, diags := kibana_oapi.UpdateMaintenanceWindow(ctx, client, spaceID, viewID, body) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = planMaintenanceWindow.fromAPIUpdateResponse(ctx, maintenanceWindow, spaceID) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, planMaintenanceWindow) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/kibana/maintenance_window/validators.go b/internal/kibana/maintenance_window/validators.go new file mode 100644 index 000000000..cdb396c12 --- /dev/null +++ b/internal/kibana/maintenance_window/validators.go @@ -0,0 +1,91 @@ +package maintenance_window + +import ( + "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +type StringIsMaintenanceWindowIntervalFrequency struct{} + +func (s StringIsMaintenanceWindowIntervalFrequency) Description(_ context.Context) string { + return "a valid interval/frequency. Allowed values are in the `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`." +} + +func (s StringIsMaintenanceWindowIntervalFrequency) MarkdownDescription(ctx context.Context) string { + return s.Description(ctx) +} + +func (s StringIsMaintenanceWindowIntervalFrequency) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + pattern := `^[1-9][0-9]*(?:d|w|M|y)$` + matched, err := regexp.MatchString(pattern, req.ConfigValue.ValueString()) + + if err != nil || !matched { + resp.Diagnostics.AddAttributeError( + req.Path, + "expected value to be a valid interval/frequency", + fmt.Sprintf("This value must be a valid interval/frequency. Allowed values are in the `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`. %s", err), + ) + return + } +} + +type StringIsMaintenanceWindowOnWeekDay struct{} + +func (s StringIsMaintenanceWindowOnWeekDay) Description(_ context.Context) string { + return "a valid OnWeekDay. Accepted values are specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`)." +} + +func (s StringIsMaintenanceWindowOnWeekDay) MarkdownDescription(ctx context.Context) string { + return s.Description(ctx) +} + +func (s StringIsMaintenanceWindowOnWeekDay) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + pattern := `^(((\+|-)[1-5])?(MO|TU|WE|TH|FR|SA|SU))$` + + if matched, err := regexp.MatchString(pattern, req.ConfigValue.ValueString()); err != nil || !matched { + resp.Diagnostics.AddAttributeError( + req.Path, + "expected value to be a valid OnWeekDay", + fmt.Sprintf("This value must be a valid OnWeekDay. Accepted values are specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`). %s", err), + ) + return + } +} + +type StringIsAlertingDuration struct{} + +func (s StringIsAlertingDuration) Description(_ context.Context) string { + return "a valid alerting duration in seconds (s), minutes (m), hours (h), or days (d)" +} + +func (s StringIsAlertingDuration) MarkdownDescription(ctx context.Context) string { + return s.Description(ctx) +} + +func (s StringIsAlertingDuration) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + pattern := "^[1-9][0-9]*(?:d|h|m|s)$" + + if matched, err := regexp.MatchString(pattern, req.ConfigValue.ValueString()); err != nil || !matched { + resp.Diagnostics.AddAttributeError( + req.Path, + "expected value to be a valid alerting duration", + fmt.Sprintf("This value must be a valid alerting duration in seconds (s), minutes (m), hours (h), or days (d) %s", err), + ) + return + } +} diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go index 1da87c988..bd6717e59 100644 --- a/provider/plugin_framework.go +++ b/provider/plugin_framework.go @@ -20,6 +20,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/fleet/server_host" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/data_view" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/import_saved_objects" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/maintenance_window" "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" @@ -106,5 +107,6 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { output.NewResource, server_host.NewResource, system_user.NewSystemUserResource, + maintenance_window.NewResource, } } From 8591335e64abcc54761b6ba8b985f098878f5fb7 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Fri, 1 Aug 2025 15:42:50 +0200 Subject: [PATCH 02/28] make docs-generate --- docs/resources/kibana_maintenance_window.md | 72 +++++++++++++++++++ .../kibana/maintenance_window/resource.go | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 docs/resources/kibana_maintenance_window.md diff --git a/docs/resources/kibana_maintenance_window.md b/docs/resources/kibana_maintenance_window.md new file mode 100644 index 000000000..64f0f5b13 --- /dev/null +++ b/docs/resources/kibana_maintenance_window.md @@ -0,0 +1,72 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "elasticstack_kibana_maintenance_window Resource - terraform-provider-elasticstack" +subcategory: "" +description: |- + Manages Kibana data views +--- + +# elasticstack_kibana_maintenance_window (Resource) + +Manages Kibana data views + + + + +## Schema + +### Required + +- `custom_schedule` (Attributes) A set schedule over which the maintenance window applies. (see [below for nested schema](#nestedatt--custom_schedule)) +- `title` (String) The name of the maintenance window. + +### Optional + +- `enabled` (Boolean) Whether the current maintenance window is enabled. +- `scope` (Attributes) An object that narrows the scope of what is affected by this maintenance window. (see [below for nested schema](#nestedatt--scope)) +- `space_id` (String) An identifier for the space. If space_id is not provided, the default space is used. + +### Read-Only + +- `id` (String) Generated ID for the data view. + + +### Nested Schema for `custom_schedule` + +Required: + +- `duration` (String) The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. +- `recurring` (Attributes) A set schedule over which the maintenance window applies. (see [below for nested schema](#nestedatt--custom_schedule--recurring)) +- `start` (String) The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. + +Optional: + +- `timezone` (String) The timezone of the schedule. The default timezone is UTC. + + +### Nested Schema for `custom_schedule.recurring` + +Optional: + +- `end` (String) The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. +- `every` (String) The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. +- `occurrences` (Number) The total number of recurrences of the schedule. +- `on_month` (List of Number) The specific months for a recurring schedule. Valid values are 1-12. +- `on_month_day` (List of Number) The specific days of the month for a recurring schedule. Valid values are 1-31. +- `on_week_day` (List of String) The specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`) for a recurring schedule. + + + + +### Nested Schema for `scope` + +Required: + +- `alerting` (Attributes) A set schedule over which the maintenance window applies. (see [below for nested schema](#nestedatt--scope--alerting)) + + +### Nested Schema for `scope.alerting` + +Required: + +- `kql` (String) A filter written in Kibana Query Language (KQL). diff --git a/internal/kibana/maintenance_window/resource.go b/internal/kibana/maintenance_window/resource.go index 14d44c686..f0924cae1 100644 --- a/internal/kibana/maintenance_window/resource.go +++ b/internal/kibana/maintenance_window/resource.go @@ -30,5 +30,5 @@ func (r *MaintenanceWindowResource) Configure(ctx context.Context, req resource. // Metadata returns the provider type name. func (r *MaintenanceWindowResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "kibana_maintenance_window_2") + resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "kibana_maintenance_window") } From 60c005bbe9369871694272fb3c2fd17a80a1c300 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Sat, 2 Aug 2025 11:40:33 +0200 Subject: [PATCH 03/28] Change float32s to int32s --- internal/kibana/maintenance_window/models.go | 72 ++++++++++++-------- internal/kibana/maintenance_window/schema.go | 20 +++--- internal/kibana/maintenance_window/update.go | 2 +- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go index f6ecdfd6f..fcec1a655 100644 --- a/internal/kibana/maintenance_window/models.go +++ b/internal/kibana/maintenance_window/models.go @@ -34,12 +34,12 @@ type MaintenanceWindowSchedule struct { } type MaintenanceWindowScheduleRecurring struct { - End types.String `tfsdk:"end"` - Every types.String `tfsdk:"every"` - Occurrences types.Float32 `tfsdk:"occurrences"` - OnWeekDay types.List `tfsdk:"on_week_day"` - OnMonthDay types.List `tfsdk:"on_month_day"` - OnMonth types.List `tfsdk:"on_month"` + End types.String `tfsdk:"end"` + Every types.String `tfsdk:"every"` + Occurrences types.Int32 `tfsdk:"occurrences"` + OnWeekDay types.List `tfsdk:"on_week_day"` + OnMonthDay types.List `tfsdk:"on_month_day"` + OnMonth types.List `tfsdk:"on_month"` } /* CREATE */ @@ -73,8 +73,9 @@ func (model MaintenanceWindowModel) toAPICreateRequest(ctx context.Context) (kba Every: model.CustomSchedule.Recurring.Every.ValueStringPointer(), } - if !model.CustomSchedule.Recurring.Occurrences.IsNull() && !model.CustomSchedule.Recurring.Occurrences.IsUnknown() && model.CustomSchedule.Recurring.Occurrences.ValueFloat32() > 0 { - body.Schedule.Custom.Recurring.Occurrences = model.CustomSchedule.Recurring.Occurrences.ValueFloat32Pointer() + if !model.CustomSchedule.Recurring.Occurrences.IsNull() && !model.CustomSchedule.Recurring.Occurrences.IsUnknown() && model.CustomSchedule.Recurring.Occurrences.ValueInt32() > 0 { + occurrences := float32(model.CustomSchedule.Recurring.Occurrences.ValueInt32()) + body.Schedule.Custom.Recurring.Occurrences = &occurrences } if !model.CustomSchedule.Recurring.OnWeekDay.IsNull() && !model.CustomSchedule.Recurring.OnWeekDay.IsUnknown() { @@ -148,12 +149,16 @@ func (model *MaintenanceWindowModel) fromAPICreateResponse(ctx context.Context, if response.Schedule.Custom.Recurring != nil { model.CustomSchedule.Recurring = &MaintenanceWindowScheduleRecurring{ - End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), - Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), - Occurrences: types.Float32PointerValue(response.Schedule.Custom.Recurring.Occurrences), - OnWeekDay: types.ListNull(types.StringType), - OnMonth: types.ListNull(types.Float32Type), - OnMonthDay: types.ListNull(types.Float32Type), + End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), + Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), + OnWeekDay: types.ListNull(types.StringType), + OnMonth: types.ListNull(types.Int32Type), + OnMonthDay: types.ListNull(types.Int32Type), + } + + if response.Schedule.Custom.Recurring.Occurrences != nil { + occurrences := int32(*response.Schedule.Custom.Recurring.Occurrences) + model.CustomSchedule.Recurring.Occurrences = types.Int32PointerValue(&occurrences) } if response.Schedule.Custom.Recurring.OnWeekDay != nil { @@ -211,12 +216,16 @@ func (model *MaintenanceWindowModel) fromAPIReadResponse(ctx context.Context, da if response.Schedule.Custom.Recurring != nil { model.CustomSchedule.Recurring = &MaintenanceWindowScheduleRecurring{ - End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), - Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), - Occurrences: types.Float32PointerValue(response.Schedule.Custom.Recurring.Occurrences), - OnWeekDay: types.ListNull(types.StringType), - OnMonth: types.ListNull(types.Float32Type), - OnMonthDay: types.ListNull(types.Float32Type), + End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), + Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), + OnWeekDay: types.ListNull(types.StringType), + OnMonth: types.ListNull(types.Float32Type), + OnMonthDay: types.ListNull(types.Float32Type), + } + + if response.Schedule.Custom.Recurring.Occurrences != nil { + occurrences := int32(*response.Schedule.Custom.Recurring.Occurrences) + model.CustomSchedule.Recurring.Occurrences = types.Int32PointerValue(&occurrences) } if response.Schedule.Custom.Recurring.OnWeekDay != nil { @@ -319,8 +328,9 @@ func (model MaintenanceWindowModel) toAPIUpdateRequest(ctx context.Context) (kba body.Schedule.Custom.Recurring.Every = model.CustomSchedule.Recurring.Every.ValueStringPointer() } - if !model.CustomSchedule.Recurring.Occurrences.IsNull() && !model.CustomSchedule.Recurring.Occurrences.IsUnknown() && model.CustomSchedule.Recurring.Occurrences.ValueFloat32() > 0 { - body.Schedule.Custom.Recurring.Occurrences = model.CustomSchedule.Recurring.Occurrences.ValueFloat32Pointer() + if !model.CustomSchedule.Recurring.Occurrences.IsNull() && !model.CustomSchedule.Recurring.Occurrences.IsUnknown() && model.CustomSchedule.Recurring.Occurrences.ValueInt32() > 0 { + occurrences := float32(model.CustomSchedule.Recurring.Occurrences.ValueInt32()) + body.Schedule.Custom.Recurring.Occurrences = &occurrences } if !model.CustomSchedule.Recurring.OnWeekDay.IsNull() && !model.CustomSchedule.Recurring.OnWeekDay.IsUnknown() { @@ -368,7 +378,7 @@ func (model MaintenanceWindowModel) toAPIUpdateRequest(ctx context.Context) (kba return body, diags } -func (model *MaintenanceWindowModel) fromAPIUpdateResponse(ctx context.Context, data *kbapi.PatchMaintenanceWindowIdResponse, spaceID string) diag.Diagnostics { +func (model *MaintenanceWindowModel) fromAPIUpdateResponse(ctx context.Context, data *kbapi.PatchMaintenanceWindowIdResponse) diag.Diagnostics { if data == nil { return nil } @@ -394,12 +404,16 @@ func (model *MaintenanceWindowModel) fromAPIUpdateResponse(ctx context.Context, if response.Schedule.Custom.Recurring != nil { model.CustomSchedule.Recurring = &MaintenanceWindowScheduleRecurring{ - End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), - Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), - Occurrences: types.Float32PointerValue(response.Schedule.Custom.Recurring.Occurrences), - OnWeekDay: types.ListNull(types.StringType), - OnMonth: types.ListNull(types.Float32Type), - OnMonthDay: types.ListNull(types.Float32Type), + End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), + Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), + OnWeekDay: types.ListNull(types.StringType), + OnMonth: types.ListNull(types.Float32Type), + OnMonthDay: types.ListNull(types.Float32Type), + } + + if response.Schedule.Custom.Recurring.Occurrences != nil { + occurrences := int32(*response.Schedule.Custom.Recurring.Occurrences) + model.CustomSchedule.Recurring.Occurrences = types.Int32PointerValue(&occurrences) } if response.Schedule.Custom.Recurring.OnWeekDay != nil { diff --git a/internal/kibana/maintenance_window/schema.go b/internal/kibana/maintenance_window/schema.go index 087edc9d1..c84c3c66f 100644 --- a/internal/kibana/maintenance_window/schema.go +++ b/internal/kibana/maintenance_window/schema.go @@ -3,7 +3,7 @@ package maintenance_window import ( "context" - "github.com/hashicorp/terraform-plugin-framework-validators/float32validator" + "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -84,11 +84,11 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR StringIsMaintenanceWindowIntervalFrequency{}, }, }, - "occurrences": schema.Float32Attribute{ + "occurrences": schema.Int32Attribute{ Description: "The total number of recurrences of the schedule.", Optional: true, - Validators: []validator.Float32{ - float32validator.AtLeast(1), + Validators: []validator.Int32{ + int32validator.AtLeast(1), }, }, "on_week_day": schema.ListAttribute{ @@ -103,21 +103,21 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR }, "on_month_day": schema.ListAttribute{ Description: "The specific days of the month for a recurring schedule. Valid values are 1-31.", - ElementType: types.Float32Type, + ElementType: types.Int32Type, Optional: true, Validators: []validator.List{ - listvalidator.ValueFloat32sAre( - float32validator.Between(1, 31), + listvalidator.ValueInt32sAre( + int32validator.Between(1, 31), ), }, }, "on_month": schema.ListAttribute{ Description: "The specific months for a recurring schedule. Valid values are 1-12.", - ElementType: types.Float32Type, + ElementType: types.Int32Type, Optional: true, Validators: []validator.List{ - listvalidator.ValueFloat32sAre( - float32validator.Between(1, 12), + listvalidator.ValueInt32sAre( + int32validator.Between(1, 12), ), }, }, diff --git a/internal/kibana/maintenance_window/update.go b/internal/kibana/maintenance_window/update.go index 975864936..04ef52057 100644 --- a/internal/kibana/maintenance_window/update.go +++ b/internal/kibana/maintenance_window/update.go @@ -35,7 +35,7 @@ func (r *MaintenanceWindowResource) Update(ctx context.Context, req resource.Upd return } - diags = planMaintenanceWindow.fromAPIUpdateResponse(ctx, maintenanceWindow, spaceID) + diags = planMaintenanceWindow.fromAPIUpdateResponse(ctx, maintenanceWindow) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return From f62cdab902d32ee5b13f685c2304e1bfdd0f10d3 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Sat, 2 Aug 2025 12:04:43 +0200 Subject: [PATCH 04/28] Simplify model code. --- internal/kibana/maintenance_window/models.go | 194 ++++++------------- 1 file changed, 61 insertions(+), 133 deletions(-) diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go index fcec1a655..61da987ea 100644 --- a/internal/kibana/maintenance_window/models.go +++ b/internal/kibana/maintenance_window/models.go @@ -2,6 +2,7 @@ package maintenance_window import ( "context" + "encoding/json" "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" "github.com/elastic/terraform-provider-elasticstack/internal/clients" @@ -128,64 +129,9 @@ func (model *MaintenanceWindowModel) fromAPICreateResponse(ctx context.Context, return nil } - response := data.JSON200 - - var diags diag.Diagnostics - - resourceID := clients.CompositeId{ - ClusterId: model.SpaceID.ValueString(), - ResourceId: response.Id, - } - - model.ID = types.StringValue(resourceID.String()) - model.Title = types.StringValue(response.Title) - model.Enabled = types.BoolValue(response.Enabled) - - model.CustomSchedule = MaintenanceWindowSchedule{ - Start: types.StringValue(response.Schedule.Custom.Start), - Duration: types.StringValue(response.Schedule.Custom.Duration), - Timezone: types.StringPointerValue(response.Schedule.Custom.Timezone), - } - - if response.Schedule.Custom.Recurring != nil { - model.CustomSchedule.Recurring = &MaintenanceWindowScheduleRecurring{ - End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), - Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), - OnWeekDay: types.ListNull(types.StringType), - OnMonth: types.ListNull(types.Int32Type), - OnMonthDay: types.ListNull(types.Int32Type), - } - - if response.Schedule.Custom.Recurring.Occurrences != nil { - occurrences := int32(*response.Schedule.Custom.Recurring.Occurrences) - model.CustomSchedule.Recurring.Occurrences = types.Int32PointerValue(&occurrences) - } - - if response.Schedule.Custom.Recurring.OnWeekDay != nil { - onWeekDay, _ := types.ListValueFrom(ctx, types.StringType, response.Schedule.Custom.Recurring.OnWeekDay) - model.CustomSchedule.Recurring.OnWeekDay = onWeekDay - } - - if response.Schedule.Custom.Recurring.OnMonth != nil { - onMonth, _ := types.ListValueFrom(ctx, types.Float32Type, response.Schedule.Custom.Recurring.OnMonth) - model.CustomSchedule.Recurring.OnMonth = onMonth - } - - if response.Schedule.Custom.Recurring.OnMonthDay != nil { - onMonthDay, _ := types.ListValueFrom(ctx, types.Float32Type, response.Schedule.Custom.Recurring.OnMonthDay) - model.CustomSchedule.Recurring.OnMonthDay = onMonthDay - } - } - - if response.Scope != nil { - model.Scope = &MaintenanceWindowScope{ - Alerting: MaintenanceWindowAlertingScope{ - Kql: types.StringValue(response.Scope.Alerting.Query.Kql), - }, - } - } - - return diags + var response = &ResponseJson{} + json.Unmarshal(data.Body, response) + return model._fromAPIResponse(ctx, *response) } /* READ */ @@ -195,64 +141,9 @@ func (model *MaintenanceWindowModel) fromAPIReadResponse(ctx context.Context, da return nil } - response := data.JSON200 - - var diags diag.Diagnostics - - resourceID := clients.CompositeId{ - ClusterId: model.SpaceID.ValueString(), - ResourceId: response.Id, - } - - model.ID = types.StringValue(resourceID.String()) - model.Title = types.StringValue(response.Title) - model.Enabled = types.BoolValue(response.Enabled) - - model.CustomSchedule = MaintenanceWindowSchedule{ - Start: types.StringValue(response.Schedule.Custom.Start), - Duration: types.StringValue(response.Schedule.Custom.Duration), - Timezone: types.StringPointerValue(response.Schedule.Custom.Timezone), - } - - if response.Schedule.Custom.Recurring != nil { - model.CustomSchedule.Recurring = &MaintenanceWindowScheduleRecurring{ - End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), - Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), - OnWeekDay: types.ListNull(types.StringType), - OnMonth: types.ListNull(types.Float32Type), - OnMonthDay: types.ListNull(types.Float32Type), - } - - if response.Schedule.Custom.Recurring.Occurrences != nil { - occurrences := int32(*response.Schedule.Custom.Recurring.Occurrences) - model.CustomSchedule.Recurring.Occurrences = types.Int32PointerValue(&occurrences) - } - - if response.Schedule.Custom.Recurring.OnWeekDay != nil { - onWeekDay, _ := types.ListValueFrom(ctx, types.StringType, response.Schedule.Custom.Recurring.OnWeekDay) - model.CustomSchedule.Recurring.OnWeekDay = onWeekDay - } - - if response.Schedule.Custom.Recurring.OnMonth != nil { - onMonth, _ := types.ListValueFrom(ctx, types.Float32Type, response.Schedule.Custom.Recurring.OnMonth) - model.CustomSchedule.Recurring.OnMonth = onMonth - } - - if response.Schedule.Custom.Recurring.OnMonthDay != nil { - onMonthDay, _ := types.ListValueFrom(ctx, types.Float32Type, response.Schedule.Custom.Recurring.OnMonthDay) - model.CustomSchedule.Recurring.OnMonthDay = onMonthDay - } - } - - if response.Scope != nil { - model.Scope = &MaintenanceWindowScope{ - Alerting: MaintenanceWindowAlertingScope{ - Kql: types.StringValue(response.Scope.Alerting.Query.Kql), - }, - } - } - - return diags + var response = &ResponseJson{} + json.Unmarshal(data.Body, response) + return model._fromAPIResponse(ctx, *response) } /* UPDATE */ @@ -383,7 +274,60 @@ func (model *MaintenanceWindowModel) fromAPIUpdateResponse(ctx context.Context, return nil } - response := data.JSON200 + var response = &ResponseJson{} + json.Unmarshal(data.Body, response) + return model._fromAPIResponse(ctx, *response) +} + +/* DELETE */ + +func (model MaintenanceWindowModel) getMaintenanceWindowIDAndSpaceID() (maintenanceWindowID string, spaceID string) { + maintenanceWindowID = model.ID.ValueString() + spaceID = model.SpaceID.ValueString() + + resourceID := model.ID.ValueString() + maybeCompositeID, _ := clients.CompositeIdFromStr(resourceID) + if maybeCompositeID != nil { + maintenanceWindowID = maybeCompositeID.ResourceId + spaceID = maybeCompositeID.ClusterId + } + + return +} + +/* UTILS */ + +type ResponseJson struct { + CreatedAt string `json:"created_at"` + CreatedBy *string `json:"created_by"` + Enabled bool `json:"enabled"` + Id string `json:"id"` + Schedule struct { + Custom struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + } `json:"schedule"` + Scope *struct { + Alerting struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + } `json:"scope,omitempty"` + Title string `json:"title"` +} + +func (model *MaintenanceWindowModel) _fromAPIResponse(ctx context.Context, response ResponseJson) diag.Diagnostics { var diags diag.Diagnostics @@ -442,19 +386,3 @@ func (model *MaintenanceWindowModel) fromAPIUpdateResponse(ctx context.Context, return diags } - -/* DELETE */ - -func (model MaintenanceWindowModel) getMaintenanceWindowIDAndSpaceID() (maintenanceWindowID string, spaceID string) { - maintenanceWindowID = model.ID.ValueString() - spaceID = model.SpaceID.ValueString() - - resourceID := model.ID.ValueString() - maybeCompositeID, _ := clients.CompositeIdFromStr(resourceID) - if maybeCompositeID != nil { - maintenanceWindowID = maybeCompositeID.ResourceId - spaceID = maybeCompositeID.ClusterId - } - - return -} From 882ab659bb6bfc97d9d7a1730738bb23fb558b1d Mon Sep 17 00:00:00 2001 From: adcoelho Date: Sat, 2 Aug 2025 12:52:19 +0200 Subject: [PATCH 05/28] Resource tests --- .../kibana/maintenance_window/acc_test.go | 186 +++++++----------- internal/kibana/maintenance_window/models.go | 24 ++- 2 files changed, 96 insertions(+), 114 deletions(-) diff --git a/internal/kibana/maintenance_window/acc_test.go b/internal/kibana/maintenance_window/acc_test.go index 009a44848..7c4a48ef5 100644 --- a/internal/kibana/maintenance_window/acc_test.go +++ b/internal/kibana/maintenance_window/acc_test.go @@ -1,153 +1,117 @@ package maintenance_window_test import ( - "fmt" "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" "github.com/hashicorp/go-version" - sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -var minDataViewAPISupport = version.Must(version.NewVersion("8.1.0")) -var minFullDataviewSupport = version.Must(version.NewVersion("8.8.0")) +var minMaintenanceWindowAPISupport = version.Must(version.NewVersion("9.1.0")) -func TestAccResourceDataView(t *testing.T) { - indexName := "my-index-" + sdkacctest.RandStringFromCharSet(4, sdkacctest.CharSetAlphaNum) +func TestAccResourceMaintenanceWindow(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ProtoV6ProviderFactories: acctest.Providers, Steps: []resource.TestStep{ { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(minDataViewAPISupport), - Config: testAccResourceDataViewPre8_8DV(indexName), + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minMaintenanceWindowAPISupport), + Config: testAccResourceMaintenanceWindowCreate, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet("elasticstack_kibana_data_view.dv", "id"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "title", "Terraform Maintenance Window"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "enabled", "true"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.start", "1992-01-01T05:00:00.200Z"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.duration", "10d"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.timezone", "UTC"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.every", "20d"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.end", "2029-05-17T05:05:00.000Z"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_week_day.0", "MO"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_week_day.1", "TU"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "scope.0.alerting.0.kql", "_id: '1234'"), ), }, { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(minFullDataviewSupport), - Config: testAccResourceDataViewBasicDV(indexName), + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minMaintenanceWindowAPISupport), + Config: testAccResourceMaintenanceWindowUpdate, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet("elasticstack_kibana_data_view.dv", "id"), - resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "override", "true"), - resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.name", indexName), - resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.source_filters.#", "2"), - resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.field_formats.event_time.id", "date_nanos"), - resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.field_formats.machine.ram.params.pattern", "0,0.[000] b"), - resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.runtime_field_map.runtime_shape_name.script_source", "emit(doc['shape_name'].value)"), - resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.field_attrs.ingest_failure.custom_label", "error.ingest_failure"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "title", "Terraform Maintenance Window UPDATED"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "enabled", "false"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.start", "1999-02-02T05:00:00.200Z"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.duration", "12d"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.timezone", "Asia/Taipei"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.every", "21d"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.end", ""), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_month_day.0", "1"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_month_day.1", "2"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_month_day.2", "3"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_month.0", "4"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_month.1", "5"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "scope.0.alerting.0.kql", "_id: 'foobar'"), ), }, - { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(minFullDataviewSupport), - Config: testAccResourceDataViewBasicDVUpdated(indexName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet("elasticstack_kibana_data_view.dv", "id"), - resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "override", "false"), - resource.TestCheckResourceAttr("elasticstack_kibana_data_view.dv", "data_view.name", indexName), - resource.TestCheckNoResourceAttr("elasticstack_kibana_data_view.dv", "data_view.source_filters"), - resource.TestCheckNoResourceAttr("elasticstack_kibana_data_view.dv", "data_view.field_formats"), - resource.TestCheckNoResourceAttr("elasticstack_kibana_data_view.dv", "data_view.runtime_field_map"), - ), - }, - { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(minFullDataviewSupport), - Config: testAccResourceDataViewBasicDVUpdated(indexName), - ImportState: true, - ImportStateVerify: true, - ResourceName: "elasticstack_kibana_data_view.dv", - }, }, }) } -func testAccResourceDataViewPre8_8DV(indexName string) string { - return fmt.Sprintf(` +const testAccResourceMaintenanceWindowCreate = ` provider "elasticstack" { - elasticsearch {} - kibana {} + elasticsearch {} + kibana {} } -resource "elasticstack_elasticsearch_index" "my_index" { - name = "%s" - deletion_protection = false -} +resource "elasticstack_kibana_maintenance_window" "test_maintenance_window" { + title = "Terraform Maintenance Window" + enabled = true -resource "elasticstack_kibana_data_view" "dv" { - data_view = { - title = "%s*" - } -}`, indexName, indexName) -} + custom_schedule = { + start = "1992-01-01T05:00:00.200Z" + duration = "10d" + timezone = "UTC" -func testAccResourceDataViewBasicDV(indexName string) string { - return fmt.Sprintf(` -provider "elasticstack" { - elasticsearch {} - kibana {} -} - -resource "elasticstack_elasticsearch_index" "my_index" { - name = "%s" - deletion_protection = false -} + recurring = { + every = "20d" + end = "2029-05-17T05:05:00.000Z" + on_week_day = ["MO", "TU"] + } + } -resource "elasticstack_kibana_data_view" "dv" { - override = true - data_view = { - title = "%s*" - name = "%s" - time_field_name = "@timestamp" - source_filters = ["event_time", "machine.ram"] - allow_no_index = true - namespaces = ["default", "foo", "bar"] - field_formats = { - event_time = { - id = "date_nanos" - } - "machine.ram" = { - id = "number" - params = { - pattern = "0,0.[000] b" - } - } - } - runtime_field_map = { - runtime_shape_name = { - type = "keyword" - script_source = "emit(doc['shape_name'].value)" - } - } - field_attrs = { - ingest_failure = { custom_label = "error.ingest_failure", count = 6 }, - } - } -}`, indexName, indexName, indexName) + scope = { + alerting = { + kql = "_id: '1234'" + } + } } +` -func testAccResourceDataViewBasicDVUpdated(indexName string) string { - return fmt.Sprintf(` +const testAccResourceMaintenanceWindowUpdate = ` provider "elasticstack" { - elasticsearch {} - kibana {} + elasticsearch {} + kibana {} } -resource "elasticstack_elasticsearch_index" "my_index" { - name = "%s" - deletion_protection = false -} +resource "elasticstack_kibana_maintenance_window" "test_maintenance_window" { + title = "Terraform Maintenance Window UPDATED" + enabled = false + + custom_schedule = { + start = "1999-02-02T05:00:00.200Z" + duration = "12d" + timezone = "Asia/Taipei" + + recurring = { + every = "21d" + on_month_day = [1, 2, 3] + on_month = [4, 5] + } + } -resource "elasticstack_kibana_data_view" "dv" { - override = false - data_view = { - title = "%s*" - name = "%s" - time_field_name = "@timestamp" - allow_no_index = true - } -}`, indexName, indexName, indexName) + scope = { + alerting = { + kql = "_id: 'foobar'" + } + } } +` diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go index 61da987ea..17f352017 100644 --- a/internal/kibana/maintenance_window/models.go +++ b/internal/kibana/maintenance_window/models.go @@ -129,8 +129,14 @@ func (model *MaintenanceWindowModel) fromAPICreateResponse(ctx context.Context, return nil } + var diags diag.Diagnostics var response = &ResponseJson{} - json.Unmarshal(data.Body, response) + + if err := json.Unmarshal(data.Body, response); err != nil { + diags.AddError(err.Error(), "cannot unmarshal PostMaintenanceWindowResponse") + return diags + } + return model._fromAPIResponse(ctx, *response) } @@ -141,8 +147,14 @@ func (model *MaintenanceWindowModel) fromAPIReadResponse(ctx context.Context, da return nil } + var diags diag.Diagnostics var response = &ResponseJson{} - json.Unmarshal(data.Body, response) + + if err := json.Unmarshal(data.Body, response); err != nil { + diags.AddError(err.Error(), "cannot unmarshal GetMaintenanceWindowIdResponse") + return diags + } + return model._fromAPIResponse(ctx, *response) } @@ -274,8 +286,14 @@ func (model *MaintenanceWindowModel) fromAPIUpdateResponse(ctx context.Context, return nil } + var diags diag.Diagnostics var response = &ResponseJson{} - json.Unmarshal(data.Body, response) + + if err := json.Unmarshal(data.Body, response); err != nil { + diags.AddError(err.Error(), "cannot unmarshal PatchMaintenanceWindowIdResponse") + return diags + } + return model._fromAPIResponse(ctx, *response) } From 862b8702f11dd8b0d62c047b68dee74a9740c99d Mon Sep 17 00:00:00 2001 From: adcoelho Date: Sat, 2 Aug 2025 13:12:16 +0200 Subject: [PATCH 06/28] Update the changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5e418e54..e018ba4af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [Unreleased] +- Create `elasticstack_kibana_maintenance_window` resource. ([#1224](https://github.com/elastic/terraform-provider-elasticstack/pull/1224)) - Add `slo_id` validation to `elasticstack_kibana_slo` ([#1221](https://github.com/elastic/terraform-provider-elasticstack/pull/1221)) - Add `ignore_missing_component_templates` to `elasticstack_elasticsearch_index_template` ([#1206](https://github.com/elastic/terraform-provider-elasticstack/pull/1206)) - Prevent provider panic when a script exists in state, but not in Elasticsearch ([#1218](https://github.com/elastic/terraform-provider-elasticstack/pull/1218)) From f059ff2b74ae2b7e090802f5bedb66a3f071fec0 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Sat, 2 Aug 2025 16:59:45 +0200 Subject: [PATCH 07/28] Fix diags and resource tests. --- .../kibana/maintenance_window/acc_test.go | 37 +++++++++---------- internal/kibana/maintenance_window/models.go | 8 ++-- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/internal/kibana/maintenance_window/acc_test.go b/internal/kibana/maintenance_window/acc_test.go index 7c4a48ef5..c27531e2c 100644 --- a/internal/kibana/maintenance_window/acc_test.go +++ b/internal/kibana/maintenance_window/acc_test.go @@ -23,14 +23,14 @@ func TestAccResourceMaintenanceWindow(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "title", "Terraform Maintenance Window"), resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "enabled", "true"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.start", "1992-01-01T05:00:00.200Z"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.duration", "10d"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.timezone", "UTC"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.every", "20d"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.end", "2029-05-17T05:05:00.000Z"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_week_day.0", "MO"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_week_day.1", "TU"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "scope.0.alerting.0.kql", "_id: '1234'"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.start", "1992-01-01T05:00:00.200Z"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.duration", "10d"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.timezone", "UTC"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.recurring.every", "20d"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.recurring.end", "2029-05-17T05:05:00.000Z"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.recurring.on_week_day.0", "MO"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.recurring.on_week_day.1", "TU"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "scope.alerting.kql", "_id: '1234'"), ), }, { @@ -39,17 +39,16 @@ func TestAccResourceMaintenanceWindow(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "title", "Terraform Maintenance Window UPDATED"), resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "enabled", "false"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.start", "1999-02-02T05:00:00.200Z"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.duration", "12d"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.timezone", "Asia/Taipei"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.every", "21d"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.end", ""), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_month_day.0", "1"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_month_day.1", "2"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_month_day.2", "3"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_month.0", "4"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.0.recurring.0.on_month.1", "5"), - resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "scope.0.alerting.0.kql", "_id: 'foobar'"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.start", "1999-02-02T05:00:00.200Z"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.duration", "12d"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.timezone", "Asia/Taipei"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.recurring.every", "21d"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.recurring.on_month_day.0", "1"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.recurring.on_month_day.1", "2"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.recurring.on_month_day.2", "3"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.recurring.on_month.0", "4"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "custom_schedule.recurring.on_month.1", "5"), + resource.TestCheckResourceAttr("elasticstack_kibana_maintenance_window.test_maintenance_window", "scope.alerting.kql", "_id: 'foobar'"), ), }, }, diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go index 17f352017..c47743515 100644 --- a/internal/kibana/maintenance_window/models.go +++ b/internal/kibana/maintenance_window/models.go @@ -129,7 +129,7 @@ func (model *MaintenanceWindowModel) fromAPICreateResponse(ctx context.Context, return nil } - var diags diag.Diagnostics + var diags = diag.Diagnostics{} var response = &ResponseJson{} if err := json.Unmarshal(data.Body, response); err != nil { @@ -147,7 +147,7 @@ func (model *MaintenanceWindowModel) fromAPIReadResponse(ctx context.Context, da return nil } - var diags diag.Diagnostics + var diags = diag.Diagnostics{} var response = &ResponseJson{} if err := json.Unmarshal(data.Body, response); err != nil { @@ -286,7 +286,7 @@ func (model *MaintenanceWindowModel) fromAPIUpdateResponse(ctx context.Context, return nil } - var diags diag.Diagnostics + var diags = diag.Diagnostics{} var response = &ResponseJson{} if err := json.Unmarshal(data.Body, response); err != nil { @@ -313,7 +313,7 @@ func (model MaintenanceWindowModel) getMaintenanceWindowIDAndSpaceID() (maintena return } -/* UTILS */ +/* RESPONSE HANDLER */ type ResponseJson struct { CreatedAt string `json:"created_at"` From a1bb3bfdba1d8d202d5c0f36191a484f71bafd8b Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 4 Aug 2025 15:57:32 +0200 Subject: [PATCH 08/28] Missing tests. --- internal/kibana/maintenance_window/models.go | 39 +- .../kibana/maintenance_window/models_test.go | 507 ++++++++++++++++++ .../maintenance_window/response_types.go | 49 ++ .../kibana/maintenance_window/validators.go | 29 +- .../maintenance_window/validators_test.go | 187 +++++++ 5 files changed, 766 insertions(+), 45 deletions(-) create mode 100644 internal/kibana/maintenance_window/models_test.go create mode 100644 internal/kibana/maintenance_window/response_types.go create mode 100644 internal/kibana/maintenance_window/validators_test.go diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go index c47743515..a91c34e7a 100644 --- a/internal/kibana/maintenance_window/models.go +++ b/internal/kibana/maintenance_window/models.go @@ -256,7 +256,6 @@ func (model MaintenanceWindowModel) toAPIUpdateRequest(ctx context.Context) (kba } if model.Scope != nil { - // Yes, I hate it too body.Scope = &struct { Alerting struct { Query struct { @@ -315,36 +314,6 @@ func (model MaintenanceWindowModel) getMaintenanceWindowIDAndSpaceID() (maintena /* RESPONSE HANDLER */ -type ResponseJson struct { - CreatedAt string `json:"created_at"` - CreatedBy *string `json:"created_by"` - Enabled bool `json:"enabled"` - Id string `json:"id"` - Schedule struct { - Custom struct { - Duration string `json:"duration"` - Recurring *struct { - End *string `json:"end,omitempty"` - Every *string `json:"every,omitempty"` - Occurrences *float32 `json:"occurrences,omitempty"` - OnMonth *[]float32 `json:"onMonth,omitempty"` - OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` - OnWeekDay *[]string `json:"onWeekDay,omitempty"` - } `json:"recurring,omitempty"` - Start string `json:"start"` - Timezone *string `json:"timezone,omitempty"` - } `json:"custom"` - } `json:"schedule"` - Scope *struct { - Alerting struct { - Query struct { - Kql string `json:"kql"` - } `json:"query"` - } `json:"alerting"` - } `json:"scope,omitempty"` - Title string `json:"title"` -} - func (model *MaintenanceWindowModel) _fromAPIResponse(ctx context.Context, response ResponseJson) diag.Diagnostics { var diags diag.Diagnostics @@ -369,8 +338,8 @@ func (model *MaintenanceWindowModel) _fromAPIResponse(ctx context.Context, respo End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), OnWeekDay: types.ListNull(types.StringType), - OnMonth: types.ListNull(types.Float32Type), - OnMonthDay: types.ListNull(types.Float32Type), + OnMonth: types.ListNull(types.Int32Type), + OnMonthDay: types.ListNull(types.Int32Type), } if response.Schedule.Custom.Recurring.Occurrences != nil { @@ -384,12 +353,12 @@ func (model *MaintenanceWindowModel) _fromAPIResponse(ctx context.Context, respo } if response.Schedule.Custom.Recurring.OnMonth != nil { - onMonth, _ := types.ListValueFrom(ctx, types.Float32Type, response.Schedule.Custom.Recurring.OnMonth) + onMonth, _ := types.ListValueFrom(ctx, types.Int32Type, response.Schedule.Custom.Recurring.OnMonth) model.CustomSchedule.Recurring.OnMonth = onMonth } if response.Schedule.Custom.Recurring.OnMonthDay != nil { - onMonthDay, _ := types.ListValueFrom(ctx, types.Float32Type, response.Schedule.Custom.Recurring.OnMonthDay) + onMonthDay, _ := types.ListValueFrom(ctx, types.Int32Type, response.Schedule.Custom.Recurring.OnMonthDay) model.CustomSchedule.Recurring.OnMonthDay = onMonthDay } } diff --git a/internal/kibana/maintenance_window/models_test.go b/internal/kibana/maintenance_window/models_test.go new file mode 100644 index 000000000..994e34314 --- /dev/null +++ b/internal/kibana/maintenance_window/models_test.go @@ -0,0 +1,507 @@ +package maintenance_window + +import ( + "context" + "testing" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "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/types" + "github.com/stretchr/testify/require" +) + +var modelWithAllFields = MaintenanceWindowModel{ + ID: types.StringValue("/existing-space-id/id"), + Title: types.StringValue("test response"), + Enabled: types.BoolValue(true), + + CustomSchedule: MaintenanceWindowSchedule{ + Start: types.StringValue("1993-01-01T05:00:00.200Z"), + Duration: types.StringValue("13d"), + Timezone: types.StringValue("America/Martinique"), + + Recurring: &MaintenanceWindowScheduleRecurring{ + Every: types.StringValue("21d"), + End: types.StringValue("2029-05-17T05:05:00.000Z"), + Occurrences: types.Int32Null(), + OnWeekDay: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("MO"), types.StringValue("-2FR"), types.StringValue("+4SA")}), + OnMonth: types.ListValueMust(types.Int32Type, []attr.Value{types.Int32Value(6)}), + OnMonthDay: types.ListValueMust(types.Int32Type, []attr.Value{types.Int32Value(1), types.Int32Value(2), types.Int32Value(3)}), + }, + }, + + Scope: &MaintenanceWindowScope{ + Alerting: MaintenanceWindowAlertingScope{ + Kql: types.StringValue("_id: '1234'"), + }, + }, +} + +var modelOccurrencesNoScope = MaintenanceWindowModel{ + ID: types.StringValue("/existing-space-id/id"), + Title: types.StringValue("test response"), + Enabled: types.BoolValue(true), + + CustomSchedule: MaintenanceWindowSchedule{ + Start: types.StringValue("1993-01-01T05:00:00.200Z"), + Duration: types.StringValue("13d"), + Timezone: types.StringNull(), + + Recurring: &MaintenanceWindowScheduleRecurring{ + Every: types.StringValue("21d"), + End: types.StringNull(), + Occurrences: types.Int32Value(42), + OnWeekDay: types.ListNull(types.StringType), + OnMonth: types.ListNull(types.Int32Type), + OnMonthDay: types.ListNull(types.Int32Type), + }, + }, + + Scope: nil, +} + +func TestMaintenanceWindowFromAPI(t *testing.T) { + ctx := context.Background() + var diags diag.Diagnostics + + tests := []struct { + name string + response ResponseJson + existingModel MaintenanceWindowModel + expectedModel MaintenanceWindowModel + }{ + { + name: "all fields", + existingModel: MaintenanceWindowModel{}, + response: ResponseJson{ + Id: "existing-space-id/id", + CreatedAt: "created_at", + Enabled: true, + Title: "test response", + Schedule: ResponseJsonSchedule{ + Custom: ResponseJsonCustomSchedule{ + Start: "1993-01-01T05:00:00.200Z", + Duration: "13d", + Timezone: utils.Pointer("America/Martinique"), + Recurring: &ResponseJsonRecurring{ + Every: utils.Pointer("21d"), + End: utils.Pointer("2029-05-17T05:05:00.000Z"), + OnWeekDay: utils.Pointer([]string{"MO", "-2FR", "+4SA"}), + OnMonth: utils.Pointer([]float32{6}), + OnMonthDay: utils.Pointer([]float32{1, 2, 3}), + }, + }, + }, + Scope: &ResponseJsonScope{ + Alerting: ResponseJsonAlerting{ + Query: ResponseJsonAlertingQuery{ + Kql: "_id: '1234'", + }, + }, + }, + }, + expectedModel: modelWithAllFields, + }, + { + name: "occurrences and no scope", + existingModel: MaintenanceWindowModel{}, + response: ResponseJson{ + Id: "existing-space-id/id", + CreatedAt: "created_at", + Enabled: true, + Title: "test response", + Schedule: ResponseJsonSchedule{ + Custom: ResponseJsonCustomSchedule{ + Start: "1993-01-01T05:00:00.200Z", + Duration: "13d", + Recurring: &ResponseJsonRecurring{ + Every: utils.Pointer("21d"), + Occurrences: utils.Pointer(float32(42)), + }, + }, + }, + Scope: nil, + }, + expectedModel: modelOccurrencesNoScope, + }, + } + + require.Empty(t, diags) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + diags := tt.existingModel._fromAPIResponse(ctx, tt.response) + + require.Equal(t, tt.expectedModel, tt.existingModel) + require.Empty(t, diags) + }) + } +} + +func TestMaintenanceWindowToAPICreateRequest(t *testing.T) { + ctx := context.Background() + var diags diag.Diagnostics + + tests := []struct { + name string + model MaintenanceWindowModel + expectedRequest kbapi.PostMaintenanceWindowJSONRequestBody + }{ + { + name: "all fields", + model: modelWithAllFields, + expectedRequest: kbapi.PostMaintenanceWindowJSONRequestBody{ + Enabled: utils.Pointer(true), + Title: "test response", + Schedule: struct { + Custom struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + }{ + Custom: struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + }{ + Start: "1993-01-01T05:00:00.200Z", + Duration: "13d", + Timezone: utils.Pointer("America/Martinique"), + Recurring: utils.Pointer(struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + }{ + Every: utils.Pointer("21d"), + End: utils.Pointer("2029-05-17T05:05:00.000Z"), + OnWeekDay: utils.Pointer([]string{"MO", "-2FR", "+4SA"}), + OnMonth: utils.Pointer([]float32{6}), + OnMonthDay: utils.Pointer([]float32{1, 2, 3}), + }), + }, + }, + Scope: utils.Pointer(struct { + Alerting struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + }{ + Alerting: struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + }{ + Query: struct { + Kql string `json:"kql"` + }{ + Kql: "_id: '1234'", + }, + }, + }, + ), + }, + }, + { + name: "occurrences and no scope", + model: modelOccurrencesNoScope, + expectedRequest: kbapi.PostMaintenanceWindowJSONRequestBody{ + Enabled: utils.Pointer(true), + Title: "test response", + Schedule: struct { + Custom struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + }{ + Custom: struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + }{ + Start: "1993-01-01T05:00:00.200Z", + Duration: "13d", + + Recurring: utils.Pointer(struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + }{ + Every: utils.Pointer("21d"), + Occurrences: utils.Pointer(float32(42)), + }), + }, + }, + }, + }, + } + + require.Empty(t, diags) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + request, diags := tt.model.toAPICreateRequest(ctx) + require.Equal(t, request, tt.expectedRequest) + require.Empty(t, diags) + }) + } +} + +func TestMaintenanceWindowToAPIUpdateRequest(t *testing.T) { + ctx := context.Background() + var diags diag.Diagnostics + + tests := []struct { + name string + model MaintenanceWindowModel + expectedRequest kbapi.PatchMaintenanceWindowIdJSONRequestBody + }{ + { + name: "all fields", + model: modelWithAllFields, + expectedRequest: kbapi.PatchMaintenanceWindowIdJSONRequestBody{ + Enabled: utils.Pointer(true), + Title: utils.Pointer("test response"), + Schedule: utils.Pointer(struct { + Custom struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + }{ + Custom: struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + }{ + Start: "1993-01-01T05:00:00.200Z", + Duration: "13d", + Timezone: utils.Pointer("America/Martinique"), + Recurring: utils.Pointer(struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + }{ + Every: utils.Pointer("21d"), + End: utils.Pointer("2029-05-17T05:05:00.000Z"), + OnWeekDay: utils.Pointer([]string{"MO", "-2FR", "+4SA"}), + OnMonth: utils.Pointer([]float32{6}), + OnMonthDay: utils.Pointer([]float32{1, 2, 3}), + }), + }, + }), + Scope: utils.Pointer(struct { + Alerting struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + }{ + Alerting: struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + }{ + Query: struct { + Kql string `json:"kql"` + }{ + Kql: "_id: '1234'", + }, + }, + }, + ), + }, + }, + { + name: "just title, enabled and schedule", + model: MaintenanceWindowModel{ + ID: types.StringValue("/existing-space-id/id"), + Title: types.StringValue("test response"), + Enabled: types.BoolValue(true), + CustomSchedule: MaintenanceWindowSchedule{ + Start: types.StringValue("1993-01-01T05:00:00.200Z"), + Duration: types.StringValue("13d"), + }, + }, + expectedRequest: kbapi.PatchMaintenanceWindowIdJSONRequestBody{ + Enabled: utils.Pointer(true), + Title: utils.Pointer("test response"), + Schedule: utils.Pointer(struct { + Custom struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + }{ + Custom: struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + }{ + Start: "1993-01-01T05:00:00.200Z", + Duration: "13d", + }, + }), + }, + }, + { + name: "just the scope and schedule", + model: MaintenanceWindowModel{ + ID: types.StringValue("/existing-space-id/id"), + + CustomSchedule: MaintenanceWindowSchedule{ + Start: types.StringValue("1993-01-01T05:00:00.200Z"), + Duration: types.StringValue("13d"), + }, + + Scope: &MaintenanceWindowScope{ + Alerting: MaintenanceWindowAlertingScope{ + Kql: types.StringValue("_id: '1234'"), + }, + }, + }, + expectedRequest: kbapi.PatchMaintenanceWindowIdJSONRequestBody{ + Schedule: utils.Pointer(struct { + Custom struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + } `json:"custom"` + }{ + Custom: struct { + Duration string `json:"duration"` + Recurring *struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + } `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` + }{ + Start: "1993-01-01T05:00:00.200Z", + Duration: "13d", + }, + }), + + Scope: utils.Pointer(struct { + Alerting struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + }{ + Alerting: struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + }{ + Query: struct { + Kql string `json:"kql"` + }{ + Kql: "_id: '1234'", + }, + }, + }, + ), + }, + }, + } + + require.Empty(t, diags) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + request, diags := tt.model.toAPIUpdateRequest(ctx) + require.Equal(t, request, tt.expectedRequest) + require.Empty(t, diags) + }) + } +} diff --git a/internal/kibana/maintenance_window/response_types.go b/internal/kibana/maintenance_window/response_types.go new file mode 100644 index 000000000..e8c9198b3 --- /dev/null +++ b/internal/kibana/maintenance_window/response_types.go @@ -0,0 +1,49 @@ +package maintenance_window + +/* +* The types generated automatically for kibana_oapi are deeply nested a very hard to use. +* This file defines convenience types that can be used to define these neestes objects +* when needed. + */ + +type ResponseJson struct { + CreatedAt string `json:"created_at"` + CreatedBy *string `json:"created_by"` + Enabled bool `json:"enabled"` + Id string `json:"id"` + Schedule ResponseJsonSchedule `json:"schedule"` + Scope *ResponseJsonScope `json:"scope,omitempty"` + Title string `json:"title"` +} + +type ResponseJsonSchedule struct { + Custom ResponseJsonCustomSchedule `json:"custom"` +} + +type ResponseJsonCustomSchedule struct { + Duration string `json:"duration"` + Recurring *ResponseJsonRecurring `json:"recurring,omitempty"` + Start string `json:"start"` + Timezone *string `json:"timezone,omitempty"` +} + +type ResponseJsonRecurring struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` +} + +type ResponseJsonScope struct { + Alerting ResponseJsonAlerting `json:"alerting"` +} + +type ResponseJsonAlerting struct { + Query ResponseJsonAlertingQuery `json:"query"` +} + +type ResponseJsonAlertingQuery struct { + Kql string `json:"kql"` +} diff --git a/internal/kibana/maintenance_window/validators.go b/internal/kibana/maintenance_window/validators.go index cdb396c12..bedca202b 100644 --- a/internal/kibana/maintenance_window/validators.go +++ b/internal/kibana/maintenance_window/validators.go @@ -8,6 +8,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) +func StringMatchesIntervalFrequencyRegex(s string) (matched bool, err error) { + pattern := `^[1-9][0-9]*(?:d|w|M|y)$` + return regexp.MatchString(pattern, s) +} + type StringIsMaintenanceWindowIntervalFrequency struct{} func (s StringIsMaintenanceWindowIntervalFrequency) Description(_ context.Context) string { @@ -23,10 +28,7 @@ func (s StringIsMaintenanceWindowIntervalFrequency) ValidateString(_ context.Con return } - pattern := `^[1-9][0-9]*(?:d|w|M|y)$` - matched, err := regexp.MatchString(pattern, req.ConfigValue.ValueString()) - - if err != nil || !matched { + if matched, err := StringMatchesIntervalFrequencyRegex(req.ConfigValue.ValueString()); err != nil || !matched { resp.Diagnostics.AddAttributeError( req.Path, "expected value to be a valid interval/frequency", @@ -36,6 +38,11 @@ func (s StringIsMaintenanceWindowIntervalFrequency) ValidateString(_ context.Con } } +func StringMatchesOnWeekDayRegex(s string) (matched bool, err error) { + pattern := `^(((\+|-)[1-5])?(MO|TU|WE|TH|FR|SA|SU))$` + return regexp.MatchString(pattern, s) +} + type StringIsMaintenanceWindowOnWeekDay struct{} func (s StringIsMaintenanceWindowOnWeekDay) Description(_ context.Context) string { @@ -51,9 +58,7 @@ func (s StringIsMaintenanceWindowOnWeekDay) ValidateString(_ context.Context, re return } - pattern := `^(((\+|-)[1-5])?(MO|TU|WE|TH|FR|SA|SU))$` - - if matched, err := regexp.MatchString(pattern, req.ConfigValue.ValueString()); err != nil || !matched { + if matched, err := StringMatchesOnWeekDayRegex(req.ConfigValue.ValueString()); err != nil || !matched { resp.Diagnostics.AddAttributeError( req.Path, "expected value to be a valid OnWeekDay", @@ -63,6 +68,12 @@ func (s StringIsMaintenanceWindowOnWeekDay) ValidateString(_ context.Context, re } } +func StringMatchesAlertingDurationRegex(s string) (matched bool, err error) { + pattern := "^[1-9][0-9]*(?:d|h|m|s)$" + + return regexp.MatchString(pattern, s) +} + type StringIsAlertingDuration struct{} func (s StringIsAlertingDuration) Description(_ context.Context) string { @@ -78,9 +89,7 @@ func (s StringIsAlertingDuration) ValidateString(_ context.Context, req validato return } - pattern := "^[1-9][0-9]*(?:d|h|m|s)$" - - if matched, err := regexp.MatchString(pattern, req.ConfigValue.ValueString()); err != nil || !matched { + if matched, err := StringMatchesAlertingDurationRegex(req.ConfigValue.ValueString()); err != nil || !matched { resp.Diagnostics.AddAttributeError( req.Path, "expected value to be a valid alerting duration", diff --git a/internal/kibana/maintenance_window/validators_test.go b/internal/kibana/maintenance_window/validators_test.go new file mode 100644 index 000000000..91e2d7fec --- /dev/null +++ b/internal/kibana/maintenance_window/validators_test.go @@ -0,0 +1,187 @@ +package maintenance_window + +import ( + "reflect" + "testing" +) + +func TestStringMatchesAlertingDuration(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + duration string + matched bool + }{ + { + name: "valid Alerting duration string (30d)", + duration: "30d", + matched: true, + }, + { + name: "invalid Alerting duration unit (0s)", + duration: "0s", + matched: false, + }, + { + name: "invalid Alerting duration value (.12y)", + duration: ".12y", + matched: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matched, _ := StringMatchesAlertingDurationRegex(tt.duration) + if !reflect.DeepEqual(matched, tt.matched) { + t.Errorf("StringMatchesAlertingDurationRegex() failed match = %v, want %v", matched, tt.matched) + } + }) + } +} + +func TestStringMatchesOnWeekDay(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + onWeekDay string + matched bool + }{ + { + name: "valid on_week_day string (+1MO)", + onWeekDay: "+1MO", + matched: true, + }, + { + name: "valid on_week_day string (+2TU)", + onWeekDay: "+2TU", + matched: true, + }, + { + name: "valid on_week_day string (+3WE)", + onWeekDay: "+3WE", + matched: true, + }, + { + name: "valid on_week_day string (+4TH)", + onWeekDay: "+4TH", + matched: true, + }, + { + name: "valid on_week_day string (+5FR)", + onWeekDay: "+5FR", + matched: true, + }, + { + name: "valid on_week_day string (-5SA)", + onWeekDay: "-5SA", + matched: true, + }, + { + name: "valid on_week_day string (-4SU)", + onWeekDay: "-4SU", + matched: true, + }, + { + name: "valid on_week_day string (-3MO)", + onWeekDay: "-3MO", + matched: true, + }, + { + name: "valid on_week_day string (-2TU)", + onWeekDay: "-2TU", + matched: true, + }, + { + name: "valid on_week_day string (-1WE)", + onWeekDay: "-1WE", + matched: true, + }, + { + name: "invalid on_week_day unit (FOOBAR)", + onWeekDay: "FOOBAR", + matched: false, + }, + { + name: "invalid on_week_day string (+9MO)", + onWeekDay: "+9MO", + matched: false, + }, + { + name: "invalid on_week_day string (-7FR)", + onWeekDay: "-7FR", + matched: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matched, _ := StringMatchesOnWeekDayRegex(tt.onWeekDay) + if !reflect.DeepEqual(matched, tt.matched) { + t.Errorf("StringMatchesOnWeekDayRegex() failed match = %v, want %v", matched, tt.matched) + } + }) + } +} + +func TestStringMatchesIntervalFrequencyRegex(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + intervalFrequency string + matched bool + }{ + + { + name: "valid interval/frequency string (2d)", + intervalFrequency: "2d", + matched: true, + }, + { + name: "valid interval/frequency string (5w)", + intervalFrequency: "5w", + matched: true, + }, + { + name: "valid interval/frequency string (3M)", + intervalFrequency: "3M", + matched: true, + }, + { + name: "valid interval/frequency string (1y)", + intervalFrequency: "1y", + matched: true, + }, + { + name: "invalid interval/frequency string (5m)", + intervalFrequency: "5m", + matched: false, + }, + { + name: "invalid interval/frequency string (-1w)", + intervalFrequency: "-1w", + matched: false, + }, + { + name: "invalid interval/frequency string (invalid)", + intervalFrequency: "invalid", + matched: false, + }, + { + name: "invalid interval/frequency empty string", + intervalFrequency: " ", + matched: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matched, _ := StringMatchesIntervalFrequencyRegex(tt.intervalFrequency) + if !reflect.DeepEqual(matched, tt.matched) { + t.Errorf("StringMatchesOnWeekDayRegex() failed match = %v, want %v", matched, tt.matched) + } + }) + } +} From eb56228453cd0c1e3a11cdb927d5fa5b25ca54e4 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 12 Aug 2025 13:43:24 +0200 Subject: [PATCH 09/28] error handling --- internal/kibana/maintenance_window/models.go | 47 +++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go index a91c34e7a..e76d00035 100644 --- a/internal/kibana/maintenance_window/models.go +++ b/internal/kibana/maintenance_window/models.go @@ -46,7 +46,7 @@ type MaintenanceWindowScheduleRecurring struct { /* CREATE */ func (model MaintenanceWindowModel) toAPICreateRequest(ctx context.Context) (kbapi.PostMaintenanceWindowJSONRequestBody, diag.Diagnostics) { - var diags diag.Diagnostics + var diags = diag.Diagnostics{} body := kbapi.PostMaintenanceWindowJSONRequestBody{ Enabled: model.Enabled.ValueBoolPointer(), @@ -81,25 +81,24 @@ func (model MaintenanceWindowModel) toAPICreateRequest(ctx context.Context) (kba if !model.CustomSchedule.Recurring.OnWeekDay.IsNull() && !model.CustomSchedule.Recurring.OnWeekDay.IsUnknown() { var onWeekDay []string - diags = model.CustomSchedule.Recurring.OnWeekDay.ElementsAs(ctx, &onWeekDay, true) + diags.Append(model.CustomSchedule.Recurring.OnWeekDay.ElementsAs(ctx, &onWeekDay, true)...) body.Schedule.Custom.Recurring.OnWeekDay = &onWeekDay } if !model.CustomSchedule.Recurring.OnMonth.IsNull() && !model.CustomSchedule.Recurring.OnMonth.IsUnknown() { var onMonth []float32 - diags = model.CustomSchedule.Recurring.OnMonth.ElementsAs(ctx, &onMonth, true) + diags.Append(model.CustomSchedule.Recurring.OnMonth.ElementsAs(ctx, &onMonth, true)...) body.Schedule.Custom.Recurring.OnMonth = &onMonth } if !model.CustomSchedule.Recurring.OnMonthDay.IsNull() && !model.CustomSchedule.Recurring.OnMonthDay.IsUnknown() { var onMonthDay []float32 - diags = model.CustomSchedule.Recurring.OnMonthDay.ElementsAs(ctx, &onMonthDay, true) + diags.Append(model.CustomSchedule.Recurring.OnMonthDay.ElementsAs(ctx, &onMonthDay, true)...) body.Schedule.Custom.Recurring.OnMonthDay = &onMonthDay } } if model.Scope != nil { - // Yes, I hate it too body.Scope = &struct { Alerting struct { Query struct { @@ -161,7 +160,7 @@ func (model *MaintenanceWindowModel) fromAPIReadResponse(ctx context.Context, da /* UPDATE */ func (model MaintenanceWindowModel) toAPIUpdateRequest(ctx context.Context) (kbapi.PatchMaintenanceWindowIdJSONRequestBody, diag.Diagnostics) { - var diags diag.Diagnostics + var diags = diag.Diagnostics{} body := kbapi.PatchMaintenanceWindowIdJSONRequestBody{} @@ -238,19 +237,19 @@ func (model MaintenanceWindowModel) toAPIUpdateRequest(ctx context.Context) (kba if !model.CustomSchedule.Recurring.OnWeekDay.IsNull() && !model.CustomSchedule.Recurring.OnWeekDay.IsUnknown() { var onWeekDay []string - diags = model.CustomSchedule.Recurring.OnWeekDay.ElementsAs(ctx, &onWeekDay, true) + diags.Append(model.CustomSchedule.Recurring.OnWeekDay.ElementsAs(ctx, &onWeekDay, true)...) body.Schedule.Custom.Recurring.OnWeekDay = &onWeekDay } if !model.CustomSchedule.Recurring.OnMonth.IsNull() && !model.CustomSchedule.Recurring.OnMonth.IsUnknown() { var onMonth []float32 - diags = model.CustomSchedule.Recurring.OnMonth.ElementsAs(ctx, &onMonth, true) + diags.Append(model.CustomSchedule.Recurring.OnMonth.ElementsAs(ctx, &onMonth, true)...) body.Schedule.Custom.Recurring.OnMonth = &onMonth } if !model.CustomSchedule.Recurring.OnMonthDay.IsNull() && !model.CustomSchedule.Recurring.OnMonthDay.IsUnknown() { var onMonthDay []float32 - diags = model.CustomSchedule.Recurring.OnMonthDay.ElementsAs(ctx, &onMonthDay, true) + diags.Append(model.CustomSchedule.Recurring.OnMonthDay.ElementsAs(ctx, &onMonthDay, true)...) body.Schedule.Custom.Recurring.OnMonthDay = &onMonthDay } } @@ -315,8 +314,7 @@ func (model MaintenanceWindowModel) getMaintenanceWindowIDAndSpaceID() (maintena /* RESPONSE HANDLER */ func (model *MaintenanceWindowModel) _fromAPIResponse(ctx context.Context, response ResponseJson) diag.Diagnostics { - - var diags diag.Diagnostics + var diags = diag.Diagnostics{} resourceID := clients.CompositeId{ ClusterId: model.SpaceID.ValueString(), @@ -348,18 +346,33 @@ func (model *MaintenanceWindowModel) _fromAPIResponse(ctx context.Context, respo } if response.Schedule.Custom.Recurring.OnWeekDay != nil { - onWeekDay, _ := types.ListValueFrom(ctx, types.StringType, response.Schedule.Custom.Recurring.OnWeekDay) - model.CustomSchedule.Recurring.OnWeekDay = onWeekDay + onWeekDay, d := types.ListValueFrom(ctx, types.StringType, response.Schedule.Custom.Recurring.OnWeekDay) + + if d.HasError() { + diags.Append(d...) + } else { + model.CustomSchedule.Recurring.OnWeekDay = onWeekDay + } } if response.Schedule.Custom.Recurring.OnMonth != nil { - onMonth, _ := types.ListValueFrom(ctx, types.Int32Type, response.Schedule.Custom.Recurring.OnMonth) - model.CustomSchedule.Recurring.OnMonth = onMonth + onMonth, d := types.ListValueFrom(ctx, types.Int32Type, response.Schedule.Custom.Recurring.OnMonth) + + if d.HasError() { + diags.Append(d...) + } else { + model.CustomSchedule.Recurring.OnMonth = onMonth + } } if response.Schedule.Custom.Recurring.OnMonthDay != nil { - onMonthDay, _ := types.ListValueFrom(ctx, types.Int32Type, response.Schedule.Custom.Recurring.OnMonthDay) - model.CustomSchedule.Recurring.OnMonthDay = onMonthDay + onMonthDay, d := types.ListValueFrom(ctx, types.Int32Type, response.Schedule.Custom.Recurring.OnMonthDay) + + if d.HasError() { + diags.Append(d...) + } else { + model.CustomSchedule.Recurring.OnMonthDay = onMonthDay + } } } From 612bfb70bd89ad7319d8fe0c1e3b5b551ed4d617 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 12 Aug 2025 13:44:32 +0200 Subject: [PATCH 10/28] PR fixes --- generated/kbapi/transform_schema.go | 2 +- internal/kibana/maintenance_window/schema.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/kbapi/transform_schema.go b/generated/kbapi/transform_schema.go index 99681a123..0aad1d142 100644 --- a/generated/kbapi/transform_schema.go +++ b/generated/kbapi/transform_schema.go @@ -167,7 +167,7 @@ func (p *Path) SetEndpoint(method string, endpoint Map) { case "put": p.Put = endpoint case "patch": - p.Put = endpoint + p.Patch = endpoint case "delete": p.Delete = endpoint default: diff --git a/internal/kibana/maintenance_window/schema.go b/internal/kibana/maintenance_window/schema.go index c84c3c66f..92c017f83 100644 --- a/internal/kibana/maintenance_window/schema.go +++ b/internal/kibana/maintenance_window/schema.go @@ -18,11 +18,11 @@ import ( func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Description: "Manages Kibana data views", + Description: "Manages Kibana maintenance windows", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, - MarkdownDescription: "Generated ID for the data view.", + MarkdownDescription: "Generated ID for the maintenance window.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, From caebd0b9e8ff88751ec84ee74e35cbd65e1968ae Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 12 Aug 2025 13:56:22 +0200 Subject: [PATCH 11/28] improved documentation --- docs/resources/kibana_maintenance_window.md | 52 ++++++++++++++++--- .../import.sh | 1 + .../resource.tf | 27 ++++++++++ .../kibana_maintenance_window.md.tmpl | 23 ++++++++ 4 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 examples/resources/elasticstack_kibana_maintenance_window/import.sh create mode 100644 examples/resources/elasticstack_kibana_maintenance_window/resource.tf create mode 100644 templates/resources/kibana_maintenance_window.md.tmpl diff --git a/docs/resources/kibana_maintenance_window.md b/docs/resources/kibana_maintenance_window.md index 64f0f5b13..e252fa610 100644 --- a/docs/resources/kibana_maintenance_window.md +++ b/docs/resources/kibana_maintenance_window.md @@ -1,16 +1,46 @@ --- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "elasticstack_kibana_maintenance_window Resource - terraform-provider-elasticstack" -subcategory: "" +subcategory: "Kibana" +layout: "" +page_title: "Elasticstack: elasticstack_kibana_maintenance_window Resource" description: |- - Manages Kibana data views + Manages Kibana maintenance windows. --- -# elasticstack_kibana_maintenance_window (Resource) +# Resource: elasticstack_kibana_maintenance_window -Manages Kibana data views +Creates and manages Kibana [maintenance windows](https://www.elastic.co/docs/api/doc/kibana/group/endpoint-maintenance-window) +## Example Usage +```terraform +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_kibana_maintenance_window" "my_maintenance_window" { + title = "UPDATE TEST" + enabled = true + + custom_schedule { + start = "1993-01-01T05:00:00.200Z" + duration = "12d" + + recurring { + every = "21d" + on_week_day = ["MO", "+3TU", "-2FR"] + on_month_day = [1, 2, 4, 6, 7] + on_month = [12] + } + } + + scope { + alerting { + kql = "_id: '1234'" + } + } +} +``` ## Schema @@ -28,7 +58,7 @@ Manages Kibana data views ### Read-Only -- `id` (String) Generated ID for the data view. +- `id` (String) Generated ID for the maintenance window. ### Nested Schema for `custom_schedule` @@ -70,3 +100,11 @@ Required: Required: - `kql` (String) A filter written in Kibana Query Language (KQL). + +## Import + +Import is supported using the following syntax: + +```shell +terraform import elasticstack_kibana_maintenance_window.my_maintenance_window / +``` diff --git a/examples/resources/elasticstack_kibana_maintenance_window/import.sh b/examples/resources/elasticstack_kibana_maintenance_window/import.sh new file mode 100644 index 000000000..42d786bab --- /dev/null +++ b/examples/resources/elasticstack_kibana_maintenance_window/import.sh @@ -0,0 +1 @@ +terraform import elasticstack_kibana_maintenance_window.my_maintenance_window / diff --git a/examples/resources/elasticstack_kibana_maintenance_window/resource.tf b/examples/resources/elasticstack_kibana_maintenance_window/resource.tf new file mode 100644 index 000000000..395f29e27 --- /dev/null +++ b/examples/resources/elasticstack_kibana_maintenance_window/resource.tf @@ -0,0 +1,27 @@ +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_kibana_maintenance_window" "my_maintenance_window" { + title = "UPDATE TEST" + enabled = true + + custom_schedule { + start = "1993-01-01T05:00:00.200Z" + duration = "12d" + + recurring { + every = "21d" + on_week_day = ["MO", "+3TU", "-2FR"] + on_month_day = [1, 2, 4, 6, 7] + on_month = [12] + } + } + + scope { + alerting { + kql = "_id: '1234'" + } + } +} diff --git a/templates/resources/kibana_maintenance_window.md.tmpl b/templates/resources/kibana_maintenance_window.md.tmpl new file mode 100644 index 000000000..e7fddee61 --- /dev/null +++ b/templates/resources/kibana_maintenance_window.md.tmpl @@ -0,0 +1,23 @@ +--- +subcategory: "Kibana" +layout: "" +page_title: "Elasticstack: elasticstack_kibana_maintenance_window Resource" +description: |- + Manages Kibana maintenance windows. +--- + +# Resource: elasticstack_kibana_maintenance_window + +Creates and manages Kibana [maintenance windows](https://www.elastic.co/docs/api/doc/kibana/group/endpoint-maintenance-window) + +## Example Usage + +{{ tffile "examples/resources/elasticstack_kibana_maintenance_window/resource.tf" }} + +{{ .SchemaMarkdown | trimspace }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" "examples/resources/elasticstack_kibana_maintenance_window/import.sh" }} From d731442ea661eb2fc19488b26b90142f27939665 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 12 Aug 2025 14:21:49 +0200 Subject: [PATCH 12/28] version and serverless checks --- internal/kibana/maintenance_window/create.go | 15 +++++++++++++ internal/kibana/maintenance_window/delete.go | 15 +++++++++++++ internal/kibana/maintenance_window/read.go | 15 +++++++++++++ internal/kibana/maintenance_window/update.go | 15 +++++++++++++ .../maintenance_window/version_utils.go | 21 +++++++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 internal/kibana/maintenance_window/version_utils.go diff --git a/internal/kibana/maintenance_window/create.go b/internal/kibana/maintenance_window/create.go index 3a82547f9..6eb3c9176 100644 --- a/internal/kibana/maintenance_window/create.go +++ b/internal/kibana/maintenance_window/create.go @@ -25,6 +25,21 @@ func (r *MaintenanceWindowResource) Create(ctx context.Context, req resource.Cre return } + serverVersion, sdkDiags := r.client.ServerVersion(ctx) + if sdkDiags.HasError() { + return + } + + serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) + if sdkDiags.HasError() { + return + } + + diags = validateMaintenanceWindowServer(serverVersion, serverFlavor) + if diags.HasError() { + return + } + client, err := r.client.GetKibanaOapiClient() if err != nil { resp.Diagnostics.AddError(err.Error(), "") diff --git a/internal/kibana/maintenance_window/delete.go b/internal/kibana/maintenance_window/delete.go index 8d4316d83..a8c595d91 100644 --- a/internal/kibana/maintenance_window/delete.go +++ b/internal/kibana/maintenance_window/delete.go @@ -16,6 +16,21 @@ func (r *MaintenanceWindowResource) Delete(ctx context.Context, req resource.Del return } + serverVersion, sdkDiags := r.client.ServerVersion(ctx) + if sdkDiags.HasError() { + return + } + + serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) + if sdkDiags.HasError() { + return + } + + diags = validateMaintenanceWindowServer(serverVersion, serverFlavor) + if diags.HasError() { + return + } + client, err := r.client.GetKibanaOapiClient() if err != nil { resp.Diagnostics.AddError(err.Error(), "") diff --git a/internal/kibana/maintenance_window/read.go b/internal/kibana/maintenance_window/read.go index 52a404570..bcc744207 100644 --- a/internal/kibana/maintenance_window/read.go +++ b/internal/kibana/maintenance_window/read.go @@ -16,6 +16,21 @@ func (r *MaintenanceWindowResource) Read(ctx context.Context, req resource.ReadR return } + serverVersion, sdkDiags := r.client.ServerVersion(ctx) + if sdkDiags.HasError() { + return + } + + serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) + if sdkDiags.HasError() { + return + } + + diags = validateMaintenanceWindowServer(serverVersion, serverFlavor) + if diags.HasError() { + return + } + client, err := r.client.GetKibanaOapiClient() if err != nil { resp.Diagnostics.AddError(err.Error(), "") diff --git a/internal/kibana/maintenance_window/update.go b/internal/kibana/maintenance_window/update.go index 04ef52057..85d7f4458 100644 --- a/internal/kibana/maintenance_window/update.go +++ b/internal/kibana/maintenance_window/update.go @@ -16,6 +16,21 @@ func (r *MaintenanceWindowResource) Update(ctx context.Context, req resource.Upd return } + serverVersion, sdkDiags := r.client.ServerVersion(ctx) + if sdkDiags.HasError() { + return + } + + serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) + if sdkDiags.HasError() { + return + } + + diags = validateMaintenanceWindowServer(serverVersion, serverFlavor) + if diags.HasError() { + return + } + client, err := r.client.GetKibanaOapiClient() if err != nil { resp.Diagnostics.AddError(err.Error(), "") diff --git a/internal/kibana/maintenance_window/version_utils.go b/internal/kibana/maintenance_window/version_utils.go new file mode 100644 index 000000000..4c62d3507 --- /dev/null +++ b/internal/kibana/maintenance_window/version_utils.go @@ -0,0 +1,21 @@ +package maintenance_window + +import ( + "fmt" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +func validateMaintenanceWindowServer(serverVersion *version.Version, serverFlavor string) diag.Diagnostics { + var serverlessFlavor = "serverless" + var maintenanceWindowPublicAPIMinSupportedVersion = version.Must(version.NewVersion("9.1.0")) + var diags diag.Diagnostics + + if serverVersion.LessThan(maintenanceWindowPublicAPIMinSupportedVersion) && serverFlavor != serverlessFlavor { + diags.AddError("Maintenance window API not supported", fmt.Sprintf(`The maintenance Window public API feature requires a minimum Elasticsearch version of "%s" or a serverless Kibana instance.`, maintenanceWindowPublicAPIMinSupportedVersion)) + return diags + } + + return nil +} From 07068c7decfbf05450eca33434ce478a6cbece8e Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 1 Sep 2025 15:01:50 +0100 Subject: [PATCH 13/28] Addressing PR comments 1 --- .../resource.tf | 8 ++++---- internal/kibana/maintenance_window/delete.go | 15 -------------- internal/kibana/maintenance_window/models.go | 20 ++++++++----------- internal/kibana/maintenance_window/read.go | 6 +++--- internal/kibana/maintenance_window/schema.go | 2 +- 5 files changed, 16 insertions(+), 35 deletions(-) diff --git a/examples/resources/elasticstack_kibana_maintenance_window/resource.tf b/examples/resources/elasticstack_kibana_maintenance_window/resource.tf index 395f29e27..2c564de26 100644 --- a/examples/resources/elasticstack_kibana_maintenance_window/resource.tf +++ b/examples/resources/elasticstack_kibana_maintenance_window/resource.tf @@ -7,11 +7,11 @@ resource "elasticstack_kibana_maintenance_window" "my_maintenance_window" { title = "UPDATE TEST" enabled = true - custom_schedule { + custom_schedule = { start = "1993-01-01T05:00:00.200Z" duration = "12d" - recurring { + recurring = { every = "21d" on_week_day = ["MO", "+3TU", "-2FR"] on_month_day = [1, 2, 4, 6, 7] @@ -19,8 +19,8 @@ resource "elasticstack_kibana_maintenance_window" "my_maintenance_window" { } } - scope { - alerting { + scope = { + alerting = { kql = "_id: '1234'" } } diff --git a/internal/kibana/maintenance_window/delete.go b/internal/kibana/maintenance_window/delete.go index a8c595d91..8d4316d83 100644 --- a/internal/kibana/maintenance_window/delete.go +++ b/internal/kibana/maintenance_window/delete.go @@ -16,21 +16,6 @@ func (r *MaintenanceWindowResource) Delete(ctx context.Context, req resource.Del return } - serverVersion, sdkDiags := r.client.ServerVersion(ctx) - if sdkDiags.HasError() { - return - } - - serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) - if sdkDiags.HasError() { - return - } - - diags = validateMaintenanceWindowServer(serverVersion, serverFlavor) - if diags.HasError() { - return - } - client, err := r.client.GetKibanaOapiClient() if err != nil { resp.Diagnostics.AddError(err.Error(), "") diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go index e76d00035..93d9680af 100644 --- a/internal/kibana/maintenance_window/models.go +++ b/internal/kibana/maintenance_window/models.go @@ -6,6 +6,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -79,19 +80,19 @@ func (model MaintenanceWindowModel) toAPICreateRequest(ctx context.Context) (kba body.Schedule.Custom.Recurring.Occurrences = &occurrences } - if !model.CustomSchedule.Recurring.OnWeekDay.IsNull() && !model.CustomSchedule.Recurring.OnWeekDay.IsUnknown() { + if utils.IsKnown(model.CustomSchedule.Recurring.OnWeekDay) { var onWeekDay []string diags.Append(model.CustomSchedule.Recurring.OnWeekDay.ElementsAs(ctx, &onWeekDay, true)...) body.Schedule.Custom.Recurring.OnWeekDay = &onWeekDay } - if !model.CustomSchedule.Recurring.OnMonth.IsNull() && !model.CustomSchedule.Recurring.OnMonth.IsUnknown() { + if utils.IsKnown(model.CustomSchedule.Recurring.OnMonth) { var onMonth []float32 diags.Append(model.CustomSchedule.Recurring.OnMonth.ElementsAs(ctx, &onMonth, true)...) body.Schedule.Custom.Recurring.OnMonth = &onMonth } - if !model.CustomSchedule.Recurring.OnMonthDay.IsNull() && !model.CustomSchedule.Recurring.OnMonthDay.IsUnknown() { + if utils.IsKnown(model.CustomSchedule.Recurring.OnMonthDay) { var onMonthDay []float32 diags.Append(model.CustomSchedule.Recurring.OnMonthDay.ElementsAs(ctx, &onMonthDay, true)...) body.Schedule.Custom.Recurring.OnMonthDay = &onMonthDay @@ -162,14 +163,9 @@ func (model *MaintenanceWindowModel) fromAPIReadResponse(ctx context.Context, da func (model MaintenanceWindowModel) toAPIUpdateRequest(ctx context.Context) (kbapi.PatchMaintenanceWindowIdJSONRequestBody, diag.Diagnostics) { var diags = diag.Diagnostics{} - body := kbapi.PatchMaintenanceWindowIdJSONRequestBody{} - - if !model.Enabled.IsNull() { - body.Enabled = model.Enabled.ValueBoolPointer() - } - - if !model.Title.IsNull() { - body.Title = model.Title.ValueStringPointer() + body := kbapi.PatchMaintenanceWindowIdJSONRequestBody{ + Enabled: model.Enabled.ValueBoolPointer(), + Title: model.Title.ValueStringPointer(), } schedule := struct { @@ -207,7 +203,7 @@ func (model MaintenanceWindowModel) toAPIUpdateRequest(ctx context.Context) (kba body.Schedule = &schedule - if !model.CustomSchedule.Timezone.IsNull() && !model.CustomSchedule.Timezone.IsUnknown() { + if utils.IsKnown(model.CustomSchedule.Timezone) { body.Schedule.Custom.Timezone = model.CustomSchedule.Timezone.ValueStringPointer() } diff --git a/internal/kibana/maintenance_window/read.go b/internal/kibana/maintenance_window/read.go index bcc744207..10ac86c7a 100644 --- a/internal/kibana/maintenance_window/read.go +++ b/internal/kibana/maintenance_window/read.go @@ -38,18 +38,18 @@ func (r *MaintenanceWindowResource) Read(ctx context.Context, req resource.ReadR } viewID, spaceID := stateModel.getMaintenanceWindowIDAndSpaceID() - dataView, diags := kibana_oapi.GetMaintenanceWindow(ctx, client, spaceID, viewID) + maintenanceWindow, diags := kibana_oapi.GetMaintenanceWindow(ctx, client, spaceID, viewID) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if dataView == nil { + if maintenanceWindow == nil { resp.State.RemoveResource(ctx) return } - diags = stateModel.fromAPIReadResponse(ctx, dataView) + diags = stateModel.fromAPIReadResponse(ctx, maintenanceWindow) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/internal/kibana/maintenance_window/schema.go b/internal/kibana/maintenance_window/schema.go index 92c017f83..6bde24888 100644 --- a/internal/kibana/maintenance_window/schema.go +++ b/internal/kibana/maintenance_window/schema.go @@ -74,7 +74,7 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR Required: true, Attributes: map[string]schema.Attribute{ "end": schema.StringAttribute{ - Description: "The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.", + Description: "The end date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.", Optional: true, }, "every": schema.StringAttribute{ From 22d0f07eb036c366b11751d1b8a4ded43e870868 Mon Sep 17 00:00:00 2001 From: Antonio Date: Mon, 1 Sep 2025 15:11:33 +0100 Subject: [PATCH 14/28] Use EnforceMinVersion in maintenance_window/create.go Co-authored-by: Toby Brain --- internal/kibana/maintenance_window/create.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/internal/kibana/maintenance_window/create.go b/internal/kibana/maintenance_window/create.go index 6eb3c9176..57ed564f7 100644 --- a/internal/kibana/maintenance_window/create.go +++ b/internal/kibana/maintenance_window/create.go @@ -25,18 +25,14 @@ func (r *MaintenanceWindowResource) Create(ctx context.Context, req resource.Cre return } - serverVersion, sdkDiags := r.client.ServerVersion(ctx) - if sdkDiags.HasError() { - return - } - - serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) - if sdkDiags.HasError() { + isSupported, sdkDiags := r.client.EnforceMinVersion(ctx, version.Must(version.NewVersion("9.1.0"))) + resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(diags)...) + if resp.Diagnostics.HasError() { return } - - diags = validateMaintenanceWindowServer(serverVersion, serverFlavor) - if diags.HasError() { + + if !isSupported { + resp.Diagnostics.AddError("Unsupported server version", "Maintenance windows are not supported until Elastic Stack v9.0. Upgrade the target server to use this resource") return } From 164e61e60b84a7e7d7ac77653b16f00edbad3ed8 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 1 Sep 2025 15:33:25 +0100 Subject: [PATCH 15/28] Generating docs --- docs/resources/kibana_maintenance_window.md | 10 +++++----- internal/kibana/maintenance_window/models.go | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/resources/kibana_maintenance_window.md b/docs/resources/kibana_maintenance_window.md index e252fa610..04827e125 100644 --- a/docs/resources/kibana_maintenance_window.md +++ b/docs/resources/kibana_maintenance_window.md @@ -22,11 +22,11 @@ resource "elasticstack_kibana_maintenance_window" "my_maintenance_window" { title = "UPDATE TEST" enabled = true - custom_schedule { + custom_schedule = { start = "1993-01-01T05:00:00.200Z" duration = "12d" - recurring { + recurring = { every = "21d" on_week_day = ["MO", "+3TU", "-2FR"] on_month_day = [1, 2, 4, 6, 7] @@ -34,8 +34,8 @@ resource "elasticstack_kibana_maintenance_window" "my_maintenance_window" { } } - scope { - alerting { + scope = { + alerting = { kql = "_id: '1234'" } } @@ -78,7 +78,7 @@ Optional: Optional: -- `end` (String) The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. +- `end` (String) The end date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. - `every` (String) The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. - `occurrences` (Number) The total number of recurrences of the schedule. - `on_month` (List of Number) The specific months for a recurring schedule. Valid values are 1-12. diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go index 93d9680af..b62afe2d0 100644 --- a/internal/kibana/maintenance_window/models.go +++ b/internal/kibana/maintenance_window/models.go @@ -51,7 +51,7 @@ func (model MaintenanceWindowModel) toAPICreateRequest(ctx context.Context) (kba body := kbapi.PostMaintenanceWindowJSONRequestBody{ Enabled: model.Enabled.ValueBoolPointer(), - Title: *model.Title.ValueStringPointer(), + Title: model.Title.ValueString(), } body.Schedule.Custom.Duration = model.CustomSchedule.Duration.ValueString() @@ -75,7 +75,7 @@ func (model MaintenanceWindowModel) toAPICreateRequest(ctx context.Context) (kba Every: model.CustomSchedule.Recurring.Every.ValueStringPointer(), } - if !model.CustomSchedule.Recurring.Occurrences.IsNull() && !model.CustomSchedule.Recurring.Occurrences.IsUnknown() && model.CustomSchedule.Recurring.Occurrences.ValueInt32() > 0 { + if utils.IsKnown(model.CustomSchedule.Recurring.Occurrences) { occurrences := float32(model.CustomSchedule.Recurring.Occurrences.ValueInt32()) body.Schedule.Custom.Recurring.Occurrences = &occurrences } From d10d44ce6957d7ad176091472140aa3942d6ee80 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Mon, 1 Sep 2025 15:41:39 +0100 Subject: [PATCH 16/28] fix linter --- internal/kibana/maintenance_window/create.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/kibana/maintenance_window/create.go b/internal/kibana/maintenance_window/create.go index 57ed564f7..75395c662 100644 --- a/internal/kibana/maintenance_window/create.go +++ b/internal/kibana/maintenance_window/create.go @@ -4,6 +4,8 @@ import ( "context" "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -26,11 +28,11 @@ func (r *MaintenanceWindowResource) Create(ctx context.Context, req resource.Cre } isSupported, sdkDiags := r.client.EnforceMinVersion(ctx, version.Must(version.NewVersion("9.1.0"))) - resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(diags)...) + resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) if resp.Diagnostics.HasError() { return } - + if !isSupported { resp.Diagnostics.AddError("Unsupported server version", "Maintenance windows are not supported until Elastic Stack v9.0. Upgrade the target server to use this resource") return From 6549a9f81d2e14602d1aea2d7ccbbadcfacd76f1 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 2 Sep 2025 10:56:27 +0200 Subject: [PATCH 17/28] Moving validation around. --- internal/kibana/alerting.go | 16 ++++------------ internal/kibana/maintenance_window/schema.go | 7 ++++--- .../validators.go | 18 ++++++++++++++---- .../validators_test.go | 2 +- 4 files changed, 23 insertions(+), 20 deletions(-) rename internal/kibana/{maintenance_window => utils}/validators.go (85%) rename internal/kibana/{maintenance_window => utils}/validators_test.go (99%) diff --git a/internal/kibana/alerting.go b/internal/kibana/alerting.go index ab1b83960..48d206e66 100644 --- a/internal/kibana/alerting.go +++ b/internal/kibana/alerting.go @@ -4,11 +4,11 @@ import ( "context" "encoding/json" "fmt" - "regexp" "strings" "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana" + validation_utils "github.com/elastic/terraform-provider-elasticstack/internal/kibana/utils" "github.com/elastic/terraform-provider-elasticstack/internal/models" "github.com/elastic/terraform-provider-elasticstack/internal/utils" "github.com/hashicorp/go-version" @@ -22,14 +22,6 @@ var frequencyMinSupportedVersion = version.Must(version.NewVersion("8.6.0")) var alertsFilterMinSupportedVersion = version.Must(version.NewVersion("8.9.0")) var alertDelayMinSupportedVersion = version.Must(version.NewVersion("8.13.0")) -// Avoid lint error on deprecated SchemaValidateFunc usage. -// -//nolint:staticcheck -func stringIsAlertingDuration() schema.SchemaValidateFunc { - r := regexp.MustCompile(`^[1-9][0-9]*(?:d|h|m|s)$`) - return validation.StringMatch(r, "string is not a valid Alerting duration in seconds (s), minutes (m), hours (h), or days (d)") -} - func ResourceAlertingRule() *schema.Resource { apikeySchema := map[string]*schema.Schema{ "rule_id": { @@ -80,7 +72,7 @@ func ResourceAlertingRule() *schema.Resource { Description: "The check interval, which specifies how frequently the rule conditions are checked. The interval must be specified in seconds, minutes, hours or days.", Type: schema.TypeString, Required: true, - ValidateFunc: stringIsAlertingDuration(), + ValidateFunc: validation_utils.StringIsAlertingDurationSDKV2(), }, "actions": { Description: "An action that runs under defined conditions.", @@ -129,7 +121,7 @@ func ResourceAlertingRule() *schema.Resource { Description: "Defines how often an alert generates repeated actions. This custom action interval must be specified in seconds, minutes, hours, or days. For example, 10m or 1h. This property is applicable only if `notify_when` is `onThrottleInterval`. NOTE: This is a rule level property; if you update the rule in Kibana, it is automatically changed to use action-specific `throttle` values.", Type: schema.TypeString, Optional: true, - ValidateFunc: stringIsAlertingDuration(), + ValidateFunc: validation_utils.StringIsAlertingDurationSDKV2(), }, }, }, @@ -207,7 +199,7 @@ func ResourceAlertingRule() *schema.Resource { Description: "Deprecated in 8.13.0. Defines how often an alert generates repeated actions. This custom action interval must be specified in seconds, minutes, hours, or days. For example, 10m or 1h. This property is applicable only if `notify_when` is `onThrottleInterval`. NOTE: This is a rule level property; if you update the rule in Kibana, it is automatically changed to use action-specific `throttle` values.", Type: schema.TypeString, Optional: true, - ValidateFunc: stringIsAlertingDuration(), + ValidateFunc: validation_utils.StringIsAlertingDurationSDKV2(), }, "scheduled_task_id": { Description: "ID of the scheduled task that will execute the alert.", diff --git a/internal/kibana/maintenance_window/schema.go b/internal/kibana/maintenance_window/schema.go index 6bde24888..ad870126b 100644 --- a/internal/kibana/maintenance_window/schema.go +++ b/internal/kibana/maintenance_window/schema.go @@ -3,6 +3,7 @@ package maintenance_window import ( "context" + validation_utils "github.com/elastic/terraform-provider-elasticstack/internal/kibana/utils" "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -61,7 +62,7 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR Description: "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", Required: true, Validators: []validator.String{ - StringIsAlertingDuration{}, + validation_utils.StringIsAlertingDuration{}, }, }, "timezone": schema.StringAttribute{ @@ -81,7 +82,7 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR Description: "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", Optional: true, Validators: []validator.String{ - StringIsMaintenanceWindowIntervalFrequency{}, + validation_utils.StringIsMaintenanceWindowIntervalFrequency{}, }, }, "occurrences": schema.Int32Attribute{ @@ -97,7 +98,7 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR Optional: true, Validators: []validator.List{ listvalidator.ValueStringsAre( - StringIsMaintenanceWindowOnWeekDay{}, + validation_utils.StringIsMaintenanceWindowOnWeekDay{}, ), }, }, diff --git a/internal/kibana/maintenance_window/validators.go b/internal/kibana/utils/validators.go similarity index 85% rename from internal/kibana/maintenance_window/validators.go rename to internal/kibana/utils/validators.go index bedca202b..eb29d2499 100644 --- a/internal/kibana/maintenance_window/validators.go +++ b/internal/kibana/utils/validators.go @@ -1,4 +1,4 @@ -package maintenance_window +package validation_utils import ( "context" @@ -6,6 +6,8 @@ import ( "regexp" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func StringMatchesIntervalFrequencyRegex(s string) (matched bool, err error) { @@ -68,10 +70,10 @@ func (s StringIsMaintenanceWindowOnWeekDay) ValidateString(_ context.Context, re } } -func StringMatchesAlertingDurationRegex(s string) (matched bool, err error) { - pattern := "^[1-9][0-9]*(?:d|h|m|s)$" +var alertingDurationPattern = "^[1-9][0-9]*(?:d|h|m|s)$" - return regexp.MatchString(pattern, s) +func StringMatchesAlertingDurationRegex(s string) (matched bool, err error) { + return regexp.MatchString(alertingDurationPattern, s) } type StringIsAlertingDuration struct{} @@ -98,3 +100,11 @@ func (s StringIsAlertingDuration) ValidateString(_ context.Context, req validato return } } + +// Avoid lint error on deprecated SchemaValidateFunc usage. +// +//nolint:staticcheck +func StringIsAlertingDurationSDKV2() schema.SchemaValidateFunc { + r := regexp.MustCompile(alertingDurationPattern) + return validation.StringMatch(r, "string is not a valid Alerting duration in seconds (s), minutes (m), hours (h), or days (d)") +} diff --git a/internal/kibana/maintenance_window/validators_test.go b/internal/kibana/utils/validators_test.go similarity index 99% rename from internal/kibana/maintenance_window/validators_test.go rename to internal/kibana/utils/validators_test.go index 91e2d7fec..cf0371d82 100644 --- a/internal/kibana/maintenance_window/validators_test.go +++ b/internal/kibana/utils/validators_test.go @@ -1,4 +1,4 @@ -package maintenance_window +package validation_utils import ( "reflect" From 78afc778b39bbfb05b98a3c01b2e3a419da4fb0e Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 2 Sep 2025 11:01:03 +0200 Subject: [PATCH 18/28] Use terraform-plugin-testing --- internal/kibana/maintenance_window/acc_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/kibana/maintenance_window/acc_test.go b/internal/kibana/maintenance_window/acc_test.go index c27531e2c..c68db0765 100644 --- a/internal/kibana/maintenance_window/acc_test.go +++ b/internal/kibana/maintenance_window/acc_test.go @@ -6,7 +6,7 @@ import ( "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" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) var minMaintenanceWindowAPISupport = version.Must(version.NewVersion("9.1.0")) From 199d5faf09bff5a26264b3732512c725e876a8fd Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 2 Sep 2025 11:27:56 +0200 Subject: [PATCH 19/28] add iso8601 validation --- internal/kibana/maintenance_window/schema.go | 6 ++ internal/kibana/utils/validators.go | 30 +++++++++ internal/kibana/utils/validators_test.go | 69 ++++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/internal/kibana/maintenance_window/schema.go b/internal/kibana/maintenance_window/schema.go index ad870126b..4448538fb 100644 --- a/internal/kibana/maintenance_window/schema.go +++ b/internal/kibana/maintenance_window/schema.go @@ -57,6 +57,9 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR "start": schema.StringAttribute{ Description: "The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.", Required: true, + Validators: []validator.String{ + validation_utils.StringIsISO8601{}, + }, }, "duration": schema.StringAttribute{ Description: "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", @@ -77,6 +80,9 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR "end": schema.StringAttribute{ Description: "The end date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.", Optional: true, + Validators: []validator.String{ + validation_utils.StringIsISO8601{}, + }, }, "every": schema.StringAttribute{ Description: "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", diff --git a/internal/kibana/utils/validators.go b/internal/kibana/utils/validators.go index eb29d2499..f302cdec7 100644 --- a/internal/kibana/utils/validators.go +++ b/internal/kibana/utils/validators.go @@ -108,3 +108,33 @@ func StringIsAlertingDurationSDKV2() schema.SchemaValidateFunc { r := regexp.MustCompile(alertingDurationPattern) return validation.StringMatch(r, "string is not a valid Alerting duration in seconds (s), minutes (m), hours (h), or days (d)") } + +func StringMatchesISO8601Regex(s string) (matched bool, err error) { + pattern := `(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))` + return regexp.MatchString(pattern, s) +} + +type StringIsISO8601 struct{} + +func (s StringIsISO8601) Description(_ context.Context) string { + return "a valid ISO8601 date and time formatted string" +} + +func (s StringIsISO8601) MarkdownDescription(ctx context.Context) string { + return s.Description(ctx) +} + +func (s StringIsISO8601) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + if matched, err := StringMatchesISO8601Regex(req.ConfigValue.ValueString()); err != nil || !matched { + resp.Diagnostics.AddAttributeError( + req.Path, + "expected value to be a valid ISO8601 string", + fmt.Sprintf("This value must be a valid ISO8601 date and time formatted string %s", err), + ) + return + } +} diff --git a/internal/kibana/utils/validators_test.go b/internal/kibana/utils/validators_test.go index cf0371d82..ba36e903d 100644 --- a/internal/kibana/utils/validators_test.go +++ b/internal/kibana/utils/validators_test.go @@ -40,6 +40,75 @@ func TestStringMatchesAlertingDuration(t *testing.T) { } } +func TestStringMatchesISO8601(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + date string + matched bool + }{ + { + name: "valid complete date 1", + date: "1994-11-05T13:15:30Z", + matched: true, + }, + { + name: "valid complete date 2", + date: "1997-07-04T19:20+01:00", + matched: true, + }, + { + name: "valid complete date 3", + date: "1994-11-05T08:15:30-05:00", + matched: true, + }, + { + name: "valid complete date plus hours, minutes and seconds", + date: "1997-07-16T19:20:30+01:00", + matched: true, + }, + { + name: "valid complete date plus hours, minutes, seconds and a decimal fraction of a second", + date: "1997-07-16T19:20:30.45+01:00", + matched: true, + }, { + name: "invalid year", + date: "1997", + matched: false, + }, + { + name: "invalid year and month", + date: "1997-07", + matched: false, + }, + { + name: "invalid complete date", + date: "1997-07-04", + matched: false, + }, + { + name: "invalid hours and minutes", + date: "1997-40-04T30:220+01:00", + matched: false, + }, + { + name: "invalid seconds", + date: "1997-07-16T19:20:80+01:00", + matched: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matched, _ := StringMatchesISO8601Regex(tt.date) + if !reflect.DeepEqual(matched, tt.matched) { + t.Errorf("StringMatchesISO8601Regex() failed match = %v, want %v", matched, tt.matched) + } + }) + } +} + func TestStringMatchesOnWeekDay(t *testing.T) { t.Parallel() From d6233000c86f57586515d8d0753ecf913bf8e9d4 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 2 Sep 2025 11:32:24 +0200 Subject: [PATCH 20/28] restructure maintenance window model code --- internal/kibana/maintenance_window/models.go | 227 ++++++++----------- 1 file changed, 97 insertions(+), 130 deletions(-) diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go index b62afe2d0..918bd591f 100644 --- a/internal/kibana/maintenance_window/models.go +++ b/internal/kibana/maintenance_window/models.go @@ -47,8 +47,6 @@ type MaintenanceWindowScheduleRecurring struct { /* CREATE */ func (model MaintenanceWindowModel) toAPICreateRequest(ctx context.Context) (kbapi.PostMaintenanceWindowJSONRequestBody, diag.Diagnostics) { - var diags = diag.Diagnostics{} - body := kbapi.PostMaintenanceWindowJSONRequestBody{ Enabled: model.Enabled.ValueBoolPointer(), Title: model.Title.ValueString(), @@ -61,65 +59,9 @@ func (model MaintenanceWindowModel) toAPICreateRequest(ctx context.Context) (kba body.Schedule.Custom.Timezone = model.CustomSchedule.Timezone.ValueStringPointer() } - if model.CustomSchedule.Recurring != nil { - - body.Schedule.Custom.Recurring = &struct { - End *string `json:"end,omitempty"` - Every *string `json:"every,omitempty"` - Occurrences *float32 `json:"occurrences,omitempty"` - OnMonth *[]float32 `json:"onMonth,omitempty"` - OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` - OnWeekDay *[]string `json:"onWeekDay,omitempty"` - }{ - End: model.CustomSchedule.Recurring.End.ValueStringPointer(), - Every: model.CustomSchedule.Recurring.Every.ValueStringPointer(), - } - - if utils.IsKnown(model.CustomSchedule.Recurring.Occurrences) { - occurrences := float32(model.CustomSchedule.Recurring.Occurrences.ValueInt32()) - body.Schedule.Custom.Recurring.Occurrences = &occurrences - } - - if utils.IsKnown(model.CustomSchedule.Recurring.OnWeekDay) { - var onWeekDay []string - diags.Append(model.CustomSchedule.Recurring.OnWeekDay.ElementsAs(ctx, &onWeekDay, true)...) - body.Schedule.Custom.Recurring.OnWeekDay = &onWeekDay - } - - if utils.IsKnown(model.CustomSchedule.Recurring.OnMonth) { - var onMonth []float32 - diags.Append(model.CustomSchedule.Recurring.OnMonth.ElementsAs(ctx, &onMonth, true)...) - body.Schedule.Custom.Recurring.OnMonth = &onMonth - } - - if utils.IsKnown(model.CustomSchedule.Recurring.OnMonthDay) { - var onMonthDay []float32 - diags.Append(model.CustomSchedule.Recurring.OnMonthDay.ElementsAs(ctx, &onMonthDay, true)...) - body.Schedule.Custom.Recurring.OnMonthDay = &onMonthDay - } - } - - if model.Scope != nil { - body.Scope = &struct { - Alerting struct { - Query struct { - Kql string `json:"kql"` - } `json:"query"` - } `json:"alerting"` - }{ - Alerting: struct { - Query struct { - Kql string `json:"kql"` - } `json:"query"` - }{ - Query: struct { - Kql string `json:"kql"` - }{ - Kql: model.Scope.Alerting.Kql.ValueString(), - }, - }, - } - } + customRecurring, diags := model.CustomSchedule.Recurring.toAPIRequest(ctx) + body.Schedule.Custom.Recurring = customRecurring + body.Scope = model.Scope.toAPIRequest() return body, diags } @@ -161,14 +103,12 @@ func (model *MaintenanceWindowModel) fromAPIReadResponse(ctx context.Context, da /* UPDATE */ func (model MaintenanceWindowModel) toAPIUpdateRequest(ctx context.Context) (kbapi.PatchMaintenanceWindowIdJSONRequestBody, diag.Diagnostics) { - var diags = diag.Diagnostics{} - body := kbapi.PatchMaintenanceWindowIdJSONRequestBody{ Enabled: model.Enabled.ValueBoolPointer(), Title: model.Title.ValueStringPointer(), } - schedule := struct { + body.Schedule = &struct { Custom struct { Duration string `json:"duration"` Recurring *struct { @@ -201,76 +141,13 @@ func (model MaintenanceWindowModel) toAPIUpdateRequest(ctx context.Context) (kba }, } - body.Schedule = &schedule - if utils.IsKnown(model.CustomSchedule.Timezone) { body.Schedule.Custom.Timezone = model.CustomSchedule.Timezone.ValueStringPointer() } - if model.CustomSchedule.Recurring != nil { - - body.Schedule.Custom.Recurring = &struct { - End *string `json:"end,omitempty"` - Every *string `json:"every,omitempty"` - Occurrences *float32 `json:"occurrences,omitempty"` - OnMonth *[]float32 `json:"onMonth,omitempty"` - OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` - OnWeekDay *[]string `json:"onWeekDay,omitempty"` - }{} - - if !model.CustomSchedule.Recurring.End.IsNull() { - body.Schedule.Custom.Recurring.End = model.CustomSchedule.Recurring.End.ValueStringPointer() - } - - if !model.CustomSchedule.Recurring.Every.IsNull() { - body.Schedule.Custom.Recurring.Every = model.CustomSchedule.Recurring.Every.ValueStringPointer() - } - - if !model.CustomSchedule.Recurring.Occurrences.IsNull() && !model.CustomSchedule.Recurring.Occurrences.IsUnknown() && model.CustomSchedule.Recurring.Occurrences.ValueInt32() > 0 { - occurrences := float32(model.CustomSchedule.Recurring.Occurrences.ValueInt32()) - body.Schedule.Custom.Recurring.Occurrences = &occurrences - } - - if !model.CustomSchedule.Recurring.OnWeekDay.IsNull() && !model.CustomSchedule.Recurring.OnWeekDay.IsUnknown() { - var onWeekDay []string - diags.Append(model.CustomSchedule.Recurring.OnWeekDay.ElementsAs(ctx, &onWeekDay, true)...) - body.Schedule.Custom.Recurring.OnWeekDay = &onWeekDay - } - - if !model.CustomSchedule.Recurring.OnMonth.IsNull() && !model.CustomSchedule.Recurring.OnMonth.IsUnknown() { - var onMonth []float32 - diags.Append(model.CustomSchedule.Recurring.OnMonth.ElementsAs(ctx, &onMonth, true)...) - body.Schedule.Custom.Recurring.OnMonth = &onMonth - } - - if !model.CustomSchedule.Recurring.OnMonthDay.IsNull() && !model.CustomSchedule.Recurring.OnMonthDay.IsUnknown() { - var onMonthDay []float32 - diags.Append(model.CustomSchedule.Recurring.OnMonthDay.ElementsAs(ctx, &onMonthDay, true)...) - body.Schedule.Custom.Recurring.OnMonthDay = &onMonthDay - } - } - - if model.Scope != nil { - body.Scope = &struct { - Alerting struct { - Query struct { - Kql string `json:"kql"` - } `json:"query"` - } `json:"alerting"` - }{ - Alerting: struct { - Query struct { - Kql string `json:"kql"` - } `json:"query"` - }{ - Query: struct { - Kql string `json:"kql"` - }{ - Kql: model.Scope.Alerting.Kql.ValueString(), - }, - }, - } - } + customRecurring, diags := model.CustomSchedule.Recurring.toAPIRequest(ctx) + body.Schedule.Custom.Recurring = customRecurring + body.Scope = model.Scope.toAPIRequest() return body, diags } @@ -382,3 +259,93 @@ func (model *MaintenanceWindowModel) _fromAPIResponse(ctx context.Context, respo return diags } + +/* HELPERS */ + +func (model *MaintenanceWindowScope) toAPIRequest() *struct { + Alerting struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` +} { + if model == nil { + return nil + } + + return &struct { + Alerting struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + } `json:"alerting"` + }{ + Alerting: struct { + Query struct { + Kql string `json:"kql"` + } `json:"query"` + }{ + Query: struct { + Kql string `json:"kql"` + }{ + Kql: model.Alerting.Kql.ValueString(), + }, + }, + } +} + +func (model *MaintenanceWindowScheduleRecurring) toAPIRequest(ctx context.Context) (*struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` +}, diag.Diagnostics) { + if model == nil { + return nil, nil + } + + var diags diag.Diagnostics + result := &struct { + End *string `json:"end,omitempty"` + Every *string `json:"every,omitempty"` + Occurrences *float32 `json:"occurrences,omitempty"` + OnMonth *[]float32 `json:"onMonth,omitempty"` + OnMonthDay *[]float32 `json:"onMonthDay,omitempty"` + OnWeekDay *[]string `json:"onWeekDay,omitempty"` + }{} + + if utils.IsKnown(model.End) { + result.End = model.End.ValueStringPointer() + } + + if utils.IsKnown(model.Every) { + result.Every = model.Every.ValueStringPointer() + } + + if utils.IsKnown(model.Occurrences) { + occurrences := float32(model.Occurrences.ValueInt32()) + result.Occurrences = &occurrences + } + + if utils.IsKnown(model.OnWeekDay) { + var onWeekDay []string + diags.Append(model.OnWeekDay.ElementsAs(ctx, &onWeekDay, true)...) + result.OnWeekDay = &onWeekDay + } + + if utils.IsKnown(model.OnMonth) { + var onMonth []float32 + diags.Append(model.OnMonth.ElementsAs(ctx, &onMonth, true)...) + result.OnMonth = &onMonth + } + + if utils.IsKnown(model.OnMonthDay) { + var onMonthDay []float32 + diags.Append(model.OnMonthDay.ElementsAs(ctx, &onMonthDay, true)...) + result.OnMonthDay = &onMonthDay + } + + return result, diags +} From bd62e01348d09dd0778b3e8d5c08296d1aa22f0b Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 2 Sep 2025 11:55:21 +0200 Subject: [PATCH 21/28] call read after update/create --- .../clients/kibana_oapi/maintenance_window.go | 8 ++--- internal/kibana/maintenance_window/create.go | 20 ++++++++++-- internal/kibana/maintenance_window/delete.go | 4 +-- internal/kibana/maintenance_window/models.go | 32 ------------------- internal/kibana/maintenance_window/read.go | 4 +-- internal/kibana/maintenance_window/update.go | 21 ++++++++++-- 6 files changed, 44 insertions(+), 45 deletions(-) diff --git a/internal/clients/kibana_oapi/maintenance_window.go b/internal/clients/kibana_oapi/maintenance_window.go index 0f27040a3..8babf2c92 100644 --- a/internal/clients/kibana_oapi/maintenance_window.go +++ b/internal/clients/kibana_oapi/maintenance_window.go @@ -41,17 +41,17 @@ func CreateMaintenanceWindow(ctx context.Context, client *Client, spaceID string } // UpdateMaintenanceWindow updates an existing maintenance window. -func UpdateMaintenanceWindow(ctx context.Context, client *Client, spaceID string, maintenanceWindowID string, req kbapi.PatchMaintenanceWindowIdJSONRequestBody) (*kbapi.PatchMaintenanceWindowIdResponse, diag.Diagnostics) { +func UpdateMaintenanceWindow(ctx context.Context, client *Client, spaceID string, maintenanceWindowID string, req kbapi.PatchMaintenanceWindowIdJSONRequestBody) diag.Diagnostics { resp, err := client.API.PatchMaintenanceWindowIdWithResponse(ctx, spaceID, maintenanceWindowID, req) if err != nil { - return nil, utils.FrameworkDiagFromError(err) + return utils.FrameworkDiagFromError(err) } switch resp.StatusCode() { case http.StatusOK: - return resp, nil + return nil default: - return nil, reportUnknownError(resp.StatusCode(), resp.Body) + return reportUnknownError(resp.StatusCode(), resp.Body) } } diff --git a/internal/kibana/maintenance_window/create.go b/internal/kibana/maintenance_window/create.go index 75395c662..de963d426 100644 --- a/internal/kibana/maintenance_window/create.go +++ b/internal/kibana/maintenance_window/create.go @@ -45,14 +45,30 @@ func (r *MaintenanceWindowResource) Create(ctx context.Context, req resource.Cre } spaceID := planMaintenanceWindow.SpaceID.ValueString() - maintenanceWindowAPIResponse, diags := kibana_oapi.CreateMaintenanceWindow(ctx, client, spaceID, body) + createMaintenanceWindowResponse, diags := kibana_oapi.CreateMaintenanceWindow(ctx, client, spaceID, body) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - diags = planMaintenanceWindow.fromAPICreateResponse(ctx, maintenanceWindowAPIResponse) + /* + * In create/update paths we typically follow the write operation with a read, and then set the state from the read. + * We want to avoid a dirty plan immediately after an apply. + */ + maintenanceWindowID := createMaintenanceWindowResponse.JSON200.Id + readMaintenanceWindowResponse, diags := kibana_oapi.GetMaintenanceWindow(ctx, client, spaceID, maintenanceWindowID) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if readMaintenanceWindowResponse == nil { + resp.State.RemoveResource(ctx) + return + } + + diags = planMaintenanceWindow.fromAPIReadResponse(ctx, readMaintenanceWindowResponse) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/internal/kibana/maintenance_window/delete.go b/internal/kibana/maintenance_window/delete.go index 8d4316d83..52228ee62 100644 --- a/internal/kibana/maintenance_window/delete.go +++ b/internal/kibana/maintenance_window/delete.go @@ -22,7 +22,7 @@ func (r *MaintenanceWindowResource) Delete(ctx context.Context, req resource.Del return } - viewID, spaceID := stateModel.getMaintenanceWindowIDAndSpaceID() - diags = kibana_oapi.DeleteMaintenanceWindow(ctx, client, spaceID, viewID) + maintenanceWindowID, spaceID := stateModel.getMaintenanceWindowIDAndSpaceID() + diags = kibana_oapi.DeleteMaintenanceWindow(ctx, client, spaceID, maintenanceWindowID) resp.Diagnostics.Append(diags...) } diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go index 918bd591f..5cfe4d484 100644 --- a/internal/kibana/maintenance_window/models.go +++ b/internal/kibana/maintenance_window/models.go @@ -66,22 +66,6 @@ func (model MaintenanceWindowModel) toAPICreateRequest(ctx context.Context) (kba return body, diags } -func (model *MaintenanceWindowModel) fromAPICreateResponse(ctx context.Context, data *kbapi.PostMaintenanceWindowResponse) diag.Diagnostics { - if data == nil { - return nil - } - - var diags = diag.Diagnostics{} - var response = &ResponseJson{} - - if err := json.Unmarshal(data.Body, response); err != nil { - diags.AddError(err.Error(), "cannot unmarshal PostMaintenanceWindowResponse") - return diags - } - - return model._fromAPIResponse(ctx, *response) -} - /* READ */ func (model *MaintenanceWindowModel) fromAPIReadResponse(ctx context.Context, data *kbapi.GetMaintenanceWindowIdResponse) diag.Diagnostics { @@ -152,22 +136,6 @@ func (model MaintenanceWindowModel) toAPIUpdateRequest(ctx context.Context) (kba return body, diags } -func (model *MaintenanceWindowModel) fromAPIUpdateResponse(ctx context.Context, data *kbapi.PatchMaintenanceWindowIdResponse) diag.Diagnostics { - if data == nil { - return nil - } - - var diags = diag.Diagnostics{} - var response = &ResponseJson{} - - if err := json.Unmarshal(data.Body, response); err != nil { - diags.AddError(err.Error(), "cannot unmarshal PatchMaintenanceWindowIdResponse") - return diags - } - - return model._fromAPIResponse(ctx, *response) -} - /* DELETE */ func (model MaintenanceWindowModel) getMaintenanceWindowIDAndSpaceID() (maintenanceWindowID string, spaceID string) { diff --git a/internal/kibana/maintenance_window/read.go b/internal/kibana/maintenance_window/read.go index 10ac86c7a..d3bbdd90d 100644 --- a/internal/kibana/maintenance_window/read.go +++ b/internal/kibana/maintenance_window/read.go @@ -37,8 +37,8 @@ func (r *MaintenanceWindowResource) Read(ctx context.Context, req resource.ReadR return } - viewID, spaceID := stateModel.getMaintenanceWindowIDAndSpaceID() - maintenanceWindow, diags := kibana_oapi.GetMaintenanceWindow(ctx, client, spaceID, viewID) + maintenanceWindowID, spaceID := stateModel.getMaintenanceWindowIDAndSpaceID() + maintenanceWindow, diags := kibana_oapi.GetMaintenanceWindow(ctx, client, spaceID, maintenanceWindowID) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/internal/kibana/maintenance_window/update.go b/internal/kibana/maintenance_window/update.go index 85d7f4458..c2136b3ef 100644 --- a/internal/kibana/maintenance_window/update.go +++ b/internal/kibana/maintenance_window/update.go @@ -43,14 +43,29 @@ func (r *MaintenanceWindowResource) Update(ctx context.Context, req resource.Upd return } - viewID, spaceID := planMaintenanceWindow.getMaintenanceWindowIDAndSpaceID() - maintenanceWindow, diags := kibana_oapi.UpdateMaintenanceWindow(ctx, client, spaceID, viewID, body) + maintenanceWindowID, spaceID := planMaintenanceWindow.getMaintenanceWindowIDAndSpaceID() + diags = kibana_oapi.UpdateMaintenanceWindow(ctx, client, spaceID, maintenanceWindowID, body) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - diags = planMaintenanceWindow.fromAPIUpdateResponse(ctx, maintenanceWindow) + /* + * In create/update paths we typically follow the write operation with a read, and then set the state from the read. + * We want to avoid a dirty plan immediately after an apply. + */ + readMaintenanceWindowResponse, diags := kibana_oapi.GetMaintenanceWindow(ctx, client, spaceID, maintenanceWindowID) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if readMaintenanceWindowResponse == nil { + resp.State.RemoveResource(ctx) + return + } + + diags = planMaintenanceWindow.fromAPIReadResponse(ctx, readMaintenanceWindowResponse) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return From ec95b76efd582f5b59c6aff44a32ba05eca96a1f Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 2 Sep 2025 13:29:16 +0200 Subject: [PATCH 22/28] fix empty recurring field --- internal/kibana/maintenance_window/models.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go index 5cfe4d484..98e6d72fe 100644 --- a/internal/kibana/maintenance_window/models.go +++ b/internal/kibana/maintenance_window/models.go @@ -170,16 +170,18 @@ func (model *MaintenanceWindowModel) _fromAPIResponse(ctx context.Context, respo Start: types.StringValue(response.Schedule.Custom.Start), Duration: types.StringValue(response.Schedule.Custom.Duration), Timezone: types.StringPointerValue(response.Schedule.Custom.Timezone), - } - - if response.Schedule.Custom.Recurring != nil { - model.CustomSchedule.Recurring = &MaintenanceWindowScheduleRecurring{ - End: types.StringPointerValue(response.Schedule.Custom.Recurring.End), - Every: types.StringPointerValue(response.Schedule.Custom.Recurring.Every), + Recurring: &MaintenanceWindowScheduleRecurring{ + End: types.StringNull(), + Every: types.StringNull(), OnWeekDay: types.ListNull(types.StringType), OnMonth: types.ListNull(types.Int32Type), OnMonthDay: types.ListNull(types.Int32Type), - } + }, + } + + if response.Schedule.Custom.Recurring != nil { + model.CustomSchedule.Recurring.End = types.StringPointerValue(response.Schedule.Custom.Recurring.End) + model.CustomSchedule.Recurring.Every = types.StringPointerValue(response.Schedule.Custom.Recurring.Every) if response.Schedule.Custom.Recurring.Occurrences != nil { occurrences := int32(*response.Schedule.Custom.Recurring.Occurrences) From 0d590bce4dccc978702a39d0f64527523ac26036 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Tue, 2 Sep 2025 15:08:23 +0200 Subject: [PATCH 23/28] fix terraform import --- internal/kibana/maintenance_window/read.go | 9 +++------ internal/kibana/maintenance_window/resource.go | 5 +++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/kibana/maintenance_window/read.go b/internal/kibana/maintenance_window/read.go index d3bbdd90d..4bb7337a5 100644 --- a/internal/kibana/maintenance_window/read.go +++ b/internal/kibana/maintenance_window/read.go @@ -4,17 +4,14 @@ import ( "context" "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" ) func (r *MaintenanceWindowResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var stateModel MaintenanceWindowModel - diags := req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } + req.State.GetAttribute(ctx, path.Root("id"), &stateModel.ID) serverVersion, sdkDiags := r.client.ServerVersion(ctx) if sdkDiags.HasError() { @@ -26,7 +23,7 @@ func (r *MaintenanceWindowResource) Read(ctx context.Context, req resource.ReadR return } - diags = validateMaintenanceWindowServer(serverVersion, serverFlavor) + diags := validateMaintenanceWindowServer(serverVersion, serverFlavor) if diags.HasError() { return } diff --git a/internal/kibana/maintenance_window/resource.go b/internal/kibana/maintenance_window/resource.go index f0924cae1..8ad59275c 100644 --- a/internal/kibana/maintenance_window/resource.go +++ b/internal/kibana/maintenance_window/resource.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -32,3 +33,7 @@ func (r *MaintenanceWindowResource) Configure(ctx context.Context, req resource. func (r *MaintenanceWindowResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "kibana_maintenance_window") } + +func (r *MaintenanceWindowResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) +} From 2cc23827626e04f444602f5e1d79623b5ee7f4e6 Mon Sep 17 00:00:00 2001 From: Antonio Date: Wed, 3 Sep 2025 11:13:30 +0200 Subject: [PATCH 24/28] Update internal/kibana/maintenance_window/resource.go Co-authored-by: Toby Brain --- internal/kibana/maintenance_window/resource.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/kibana/maintenance_window/resource.go b/internal/kibana/maintenance_window/resource.go index 8ad59275c..5508cb7e7 100644 --- a/internal/kibana/maintenance_window/resource.go +++ b/internal/kibana/maintenance_window/resource.go @@ -12,6 +12,7 @@ import ( var ( _ resource.Resource = &MaintenanceWindowResource{} _ resource.ResourceWithConfigure = &MaintenanceWindowResource{} + _ resource.ResourceWithImportState = &MaintenanceWindowResource{} ) // NewResource is a helper function to simplify the provider implementation. From 5aad68abe22858f275f6fb5964632f0d878af2ec Mon Sep 17 00:00:00 2001 From: adcoelho Date: Wed, 3 Sep 2025 11:44:57 +0200 Subject: [PATCH 25/28] Addressing PR comments 2 --- internal/kibana/alerting.go | 8 +- internal/kibana/maintenance_window/read.go | 1 + .../kibana/maintenance_window/resource.go | 4 +- internal/kibana/maintenance_window/schema.go | 12 +- internal/kibana/utils/validators.go | 140 ------------------ .../kibana/validators/is_alerting_duration.go | 50 +++++++ .../kibana/validators/is_iso8601_string.go | 39 +++++ .../is_maintenance_window_interval.go | 39 +++++ .../is_maintenance_window_week_day.go | 39 +++++ .../{utils => validators}/validators_test.go | 2 +- 10 files changed, 181 insertions(+), 153 deletions(-) delete mode 100644 internal/kibana/utils/validators.go create mode 100644 internal/kibana/validators/is_alerting_duration.go create mode 100644 internal/kibana/validators/is_iso8601_string.go create mode 100644 internal/kibana/validators/is_maintenance_window_interval.go create mode 100644 internal/kibana/validators/is_maintenance_window_week_day.go rename internal/kibana/{utils => validators}/validators_test.go (99%) diff --git a/internal/kibana/alerting.go b/internal/kibana/alerting.go index 48d206e66..b6e5fac13 100644 --- a/internal/kibana/alerting.go +++ b/internal/kibana/alerting.go @@ -8,7 +8,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana" - validation_utils "github.com/elastic/terraform-provider-elasticstack/internal/kibana/utils" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/validators" "github.com/elastic/terraform-provider-elasticstack/internal/models" "github.com/elastic/terraform-provider-elasticstack/internal/utils" "github.com/hashicorp/go-version" @@ -72,7 +72,7 @@ func ResourceAlertingRule() *schema.Resource { Description: "The check interval, which specifies how frequently the rule conditions are checked. The interval must be specified in seconds, minutes, hours or days.", Type: schema.TypeString, Required: true, - ValidateFunc: validation_utils.StringIsAlertingDurationSDKV2(), + ValidateFunc: validators.StringIsAlertingDurationSDKV2(), }, "actions": { Description: "An action that runs under defined conditions.", @@ -121,7 +121,7 @@ func ResourceAlertingRule() *schema.Resource { Description: "Defines how often an alert generates repeated actions. This custom action interval must be specified in seconds, minutes, hours, or days. For example, 10m or 1h. This property is applicable only if `notify_when` is `onThrottleInterval`. NOTE: This is a rule level property; if you update the rule in Kibana, it is automatically changed to use action-specific `throttle` values.", Type: schema.TypeString, Optional: true, - ValidateFunc: validation_utils.StringIsAlertingDurationSDKV2(), + ValidateFunc: validators.StringIsAlertingDurationSDKV2(), }, }, }, @@ -199,7 +199,7 @@ func ResourceAlertingRule() *schema.Resource { Description: "Deprecated in 8.13.0. Defines how often an alert generates repeated actions. This custom action interval must be specified in seconds, minutes, hours, or days. For example, 10m or 1h. This property is applicable only if `notify_when` is `onThrottleInterval`. NOTE: This is a rule level property; if you update the rule in Kibana, it is automatically changed to use action-specific `throttle` values.", Type: schema.TypeString, Optional: true, - ValidateFunc: validation_utils.StringIsAlertingDurationSDKV2(), + ValidateFunc: validators.StringIsAlertingDurationSDKV2(), }, "scheduled_task_id": { Description: "ID of the scheduled task that will execute the alert.", diff --git a/internal/kibana/maintenance_window/read.go b/internal/kibana/maintenance_window/read.go index 4bb7337a5..3e358a617 100644 --- a/internal/kibana/maintenance_window/read.go +++ b/internal/kibana/maintenance_window/read.go @@ -12,6 +12,7 @@ func (r *MaintenanceWindowResource) Read(ctx context.Context, req resource.ReadR var stateModel MaintenanceWindowModel req.State.GetAttribute(ctx, path.Root("id"), &stateModel.ID) + req.State.GetAttribute(ctx, path.Root("space_id"), &stateModel.SpaceID) serverVersion, sdkDiags := r.client.ServerVersion(ctx) if sdkDiags.HasError() { diff --git a/internal/kibana/maintenance_window/resource.go b/internal/kibana/maintenance_window/resource.go index 5508cb7e7..60ffb79a0 100644 --- a/internal/kibana/maintenance_window/resource.go +++ b/internal/kibana/maintenance_window/resource.go @@ -10,8 +10,8 @@ import ( ) var ( - _ resource.Resource = &MaintenanceWindowResource{} - _ resource.ResourceWithConfigure = &MaintenanceWindowResource{} + _ resource.Resource = &MaintenanceWindowResource{} + _ resource.ResourceWithConfigure = &MaintenanceWindowResource{} _ resource.ResourceWithImportState = &MaintenanceWindowResource{} ) diff --git a/internal/kibana/maintenance_window/schema.go b/internal/kibana/maintenance_window/schema.go index 4448538fb..b37da6eb0 100644 --- a/internal/kibana/maintenance_window/schema.go +++ b/internal/kibana/maintenance_window/schema.go @@ -3,7 +3,7 @@ package maintenance_window import ( "context" - validation_utils "github.com/elastic/terraform-provider-elasticstack/internal/kibana/utils" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/validators" "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -58,14 +58,14 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR Description: "The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.", Required: true, Validators: []validator.String{ - validation_utils.StringIsISO8601{}, + validators.StringIsISO8601{}, }, }, "duration": schema.StringAttribute{ Description: "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", Required: true, Validators: []validator.String{ - validation_utils.StringIsAlertingDuration{}, + validators.StringIsAlertingDuration{}, }, }, "timezone": schema.StringAttribute{ @@ -81,14 +81,14 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR Description: "The end date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`.", Optional: true, Validators: []validator.String{ - validation_utils.StringIsISO8601{}, + validators.StringIsISO8601{}, }, }, "every": schema.StringAttribute{ Description: "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", Optional: true, Validators: []validator.String{ - validation_utils.StringIsMaintenanceWindowIntervalFrequency{}, + validators.StringIsMaintenanceWindowIntervalFrequency{}, }, }, "occurrences": schema.Int32Attribute{ @@ -104,7 +104,7 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR Optional: true, Validators: []validator.List{ listvalidator.ValueStringsAre( - validation_utils.StringIsMaintenanceWindowOnWeekDay{}, + validators.StringIsMaintenanceWindowOnWeekDay{}, ), }, }, diff --git a/internal/kibana/utils/validators.go b/internal/kibana/utils/validators.go deleted file mode 100644 index f302cdec7..000000000 --- a/internal/kibana/utils/validators.go +++ /dev/null @@ -1,140 +0,0 @@ -package validation_utils - -import ( - "context" - "fmt" - "regexp" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -func StringMatchesIntervalFrequencyRegex(s string) (matched bool, err error) { - pattern := `^[1-9][0-9]*(?:d|w|M|y)$` - return regexp.MatchString(pattern, s) -} - -type StringIsMaintenanceWindowIntervalFrequency struct{} - -func (s StringIsMaintenanceWindowIntervalFrequency) Description(_ context.Context) string { - return "a valid interval/frequency. Allowed values are in the `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`." -} - -func (s StringIsMaintenanceWindowIntervalFrequency) MarkdownDescription(ctx context.Context) string { - return s.Description(ctx) -} - -func (s StringIsMaintenanceWindowIntervalFrequency) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { - if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { - return - } - - if matched, err := StringMatchesIntervalFrequencyRegex(req.ConfigValue.ValueString()); err != nil || !matched { - resp.Diagnostics.AddAttributeError( - req.Path, - "expected value to be a valid interval/frequency", - fmt.Sprintf("This value must be a valid interval/frequency. Allowed values are in the `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`. %s", err), - ) - return - } -} - -func StringMatchesOnWeekDayRegex(s string) (matched bool, err error) { - pattern := `^(((\+|-)[1-5])?(MO|TU|WE|TH|FR|SA|SU))$` - return regexp.MatchString(pattern, s) -} - -type StringIsMaintenanceWindowOnWeekDay struct{} - -func (s StringIsMaintenanceWindowOnWeekDay) Description(_ context.Context) string { - return "a valid OnWeekDay. Accepted values are specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`)." -} - -func (s StringIsMaintenanceWindowOnWeekDay) MarkdownDescription(ctx context.Context) string { - return s.Description(ctx) -} - -func (s StringIsMaintenanceWindowOnWeekDay) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { - if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { - return - } - - if matched, err := StringMatchesOnWeekDayRegex(req.ConfigValue.ValueString()); err != nil || !matched { - resp.Diagnostics.AddAttributeError( - req.Path, - "expected value to be a valid OnWeekDay", - fmt.Sprintf("This value must be a valid OnWeekDay. Accepted values are specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`). %s", err), - ) - return - } -} - -var alertingDurationPattern = "^[1-9][0-9]*(?:d|h|m|s)$" - -func StringMatchesAlertingDurationRegex(s string) (matched bool, err error) { - return regexp.MatchString(alertingDurationPattern, s) -} - -type StringIsAlertingDuration struct{} - -func (s StringIsAlertingDuration) Description(_ context.Context) string { - return "a valid alerting duration in seconds (s), minutes (m), hours (h), or days (d)" -} - -func (s StringIsAlertingDuration) MarkdownDescription(ctx context.Context) string { - return s.Description(ctx) -} - -func (s StringIsAlertingDuration) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { - if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { - return - } - - if matched, err := StringMatchesAlertingDurationRegex(req.ConfigValue.ValueString()); err != nil || !matched { - resp.Diagnostics.AddAttributeError( - req.Path, - "expected value to be a valid alerting duration", - fmt.Sprintf("This value must be a valid alerting duration in seconds (s), minutes (m), hours (h), or days (d) %s", err), - ) - return - } -} - -// Avoid lint error on deprecated SchemaValidateFunc usage. -// -//nolint:staticcheck -func StringIsAlertingDurationSDKV2() schema.SchemaValidateFunc { - r := regexp.MustCompile(alertingDurationPattern) - return validation.StringMatch(r, "string is not a valid Alerting duration in seconds (s), minutes (m), hours (h), or days (d)") -} - -func StringMatchesISO8601Regex(s string) (matched bool, err error) { - pattern := `(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))` - return regexp.MatchString(pattern, s) -} - -type StringIsISO8601 struct{} - -func (s StringIsISO8601) Description(_ context.Context) string { - return "a valid ISO8601 date and time formatted string" -} - -func (s StringIsISO8601) MarkdownDescription(ctx context.Context) string { - return s.Description(ctx) -} - -func (s StringIsISO8601) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { - if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { - return - } - - if matched, err := StringMatchesISO8601Regex(req.ConfigValue.ValueString()); err != nil || !matched { - resp.Diagnostics.AddAttributeError( - req.Path, - "expected value to be a valid ISO8601 string", - fmt.Sprintf("This value must be a valid ISO8601 date and time formatted string %s", err), - ) - return - } -} diff --git a/internal/kibana/validators/is_alerting_duration.go b/internal/kibana/validators/is_alerting_duration.go new file mode 100644 index 000000000..1d7cc7917 --- /dev/null +++ b/internal/kibana/validators/is_alerting_duration.go @@ -0,0 +1,50 @@ +package validators + +import ( + "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +var alertingDurationPattern = "^[1-9][0-9]*(?:d|h|m|s)$" + +func StringMatchesAlertingDurationRegex(s string) (matched bool, err error) { + return regexp.MatchString(alertingDurationPattern, s) +} + +type StringIsAlertingDuration struct{} + +func (s StringIsAlertingDuration) Description(_ context.Context) string { + return "a valid alerting duration in seconds (s), minutes (m), hours (h), or days (d)" +} + +func (s StringIsAlertingDuration) MarkdownDescription(ctx context.Context) string { + return s.Description(ctx) +} + +func (s StringIsAlertingDuration) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + if matched, err := StringMatchesAlertingDurationRegex(req.ConfigValue.ValueString()); err != nil || !matched { + resp.Diagnostics.AddAttributeError( + req.Path, + "expected value to be a valid alerting duration", + fmt.Sprintf("This value must be a valid alerting duration in seconds (s), minutes (m), hours (h), or days (d) %s", err), + ) + return + } +} + +// Avoid lint error on deprecated SchemaValidateFunc usage. +// +//nolint:staticcheck +func StringIsAlertingDurationSDKV2() schema.SchemaValidateFunc { + r := regexp.MustCompile(alertingDurationPattern) + return validation.StringMatch(r, "string is not a valid Alerting duration in seconds (s), minutes (m), hours (h), or days (d)") +} diff --git a/internal/kibana/validators/is_iso8601_string.go b/internal/kibana/validators/is_iso8601_string.go new file mode 100644 index 000000000..4c6fcd51e --- /dev/null +++ b/internal/kibana/validators/is_iso8601_string.go @@ -0,0 +1,39 @@ +package validators + +import ( + "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func StringMatchesISO8601Regex(s string) (matched bool, err error) { + pattern := `(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))` + return regexp.MatchString(pattern, s) +} + +type StringIsISO8601 struct{} + +func (s StringIsISO8601) Description(_ context.Context) string { + return "a valid ISO8601 date and time formatted string" +} + +func (s StringIsISO8601) MarkdownDescription(ctx context.Context) string { + return s.Description(ctx) +} + +func (s StringIsISO8601) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + if matched, err := StringMatchesISO8601Regex(req.ConfigValue.ValueString()); err != nil || !matched { + resp.Diagnostics.AddAttributeError( + req.Path, + "expected value to be a valid ISO8601 string", + fmt.Sprintf("This value must be a valid ISO8601 date and time formatted string %s", err), + ) + return + } +} diff --git a/internal/kibana/validators/is_maintenance_window_interval.go b/internal/kibana/validators/is_maintenance_window_interval.go new file mode 100644 index 000000000..0e68bba2a --- /dev/null +++ b/internal/kibana/validators/is_maintenance_window_interval.go @@ -0,0 +1,39 @@ +package validators + +import ( + "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func StringMatchesIntervalFrequencyRegex(s string) (matched bool, err error) { + pattern := `^[1-9][0-9]*(?:d|w|M|y)$` + return regexp.MatchString(pattern, s) +} + +type StringIsMaintenanceWindowIntervalFrequency struct{} + +func (s StringIsMaintenanceWindowIntervalFrequency) Description(_ context.Context) string { + return "a valid interval/frequency. Allowed values are in the `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`." +} + +func (s StringIsMaintenanceWindowIntervalFrequency) MarkdownDescription(ctx context.Context) string { + return s.Description(ctx) +} + +func (s StringIsMaintenanceWindowIntervalFrequency) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + if matched, err := StringMatchesIntervalFrequencyRegex(req.ConfigValue.ValueString()); err != nil || !matched { + resp.Diagnostics.AddAttributeError( + req.Path, + "expected value to be a valid interval/frequency", + fmt.Sprintf("This value must be a valid interval/frequency. Allowed values are in the `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`. %s", err), + ) + return + } +} diff --git a/internal/kibana/validators/is_maintenance_window_week_day.go b/internal/kibana/validators/is_maintenance_window_week_day.go new file mode 100644 index 000000000..85839ecd1 --- /dev/null +++ b/internal/kibana/validators/is_maintenance_window_week_day.go @@ -0,0 +1,39 @@ +package validators + +import ( + "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func StringMatchesOnWeekDayRegex(s string) (matched bool, err error) { + pattern := `^(((\+|-)[1-5])?(MO|TU|WE|TH|FR|SA|SU))$` + return regexp.MatchString(pattern, s) +} + +type StringIsMaintenanceWindowOnWeekDay struct{} + +func (s StringIsMaintenanceWindowOnWeekDay) Description(_ context.Context) string { + return "a valid OnWeekDay. Accepted values are specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`)." +} + +func (s StringIsMaintenanceWindowOnWeekDay) MarkdownDescription(ctx context.Context) string { + return s.Description(ctx) +} + +func (s StringIsMaintenanceWindowOnWeekDay) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + if matched, err := StringMatchesOnWeekDayRegex(req.ConfigValue.ValueString()); err != nil || !matched { + resp.Diagnostics.AddAttributeError( + req.Path, + "expected value to be a valid OnWeekDay", + fmt.Sprintf("This value must be a valid OnWeekDay. Accepted values are specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`). %s", err), + ) + return + } +} diff --git a/internal/kibana/utils/validators_test.go b/internal/kibana/validators/validators_test.go similarity index 99% rename from internal/kibana/utils/validators_test.go rename to internal/kibana/validators/validators_test.go index ba36e903d..16ebbab9e 100644 --- a/internal/kibana/utils/validators_test.go +++ b/internal/kibana/validators/validators_test.go @@ -1,4 +1,4 @@ -package validation_utils +package validators import ( "reflect" From ea8e0749bace7db0807ac54f9140b54cdeb9a9c6 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Thu, 4 Sep 2025 14:25:59 +0200 Subject: [PATCH 26/28] change schema description --- docs/resources/kibana_maintenance_window.md | 4 ++-- internal/kibana/maintenance_window/schema.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/resources/kibana_maintenance_window.md b/docs/resources/kibana_maintenance_window.md index 04827e125..adb6d9b66 100644 --- a/docs/resources/kibana_maintenance_window.md +++ b/docs/resources/kibana_maintenance_window.md @@ -65,7 +65,7 @@ resource "elasticstack_kibana_maintenance_window" "my_maintenance_window" { Required: -- `duration` (String) The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. +- `duration` (String) The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for days, hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. - `recurring` (Attributes) A set schedule over which the maintenance window applies. (see [below for nested schema](#nestedatt--custom_schedule--recurring)) - `start` (String) The start date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. @@ -79,7 +79,7 @@ Optional: Optional: - `end` (String) The end date and time of the schedule, provided in ISO 8601 format and set to the UTC timezone. For example: `2025-03-12T12:00:00.000Z`. -- `every` (String) The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. +- `every` (String) The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for days, hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`. - `occurrences` (Number) The total number of recurrences of the schedule. - `on_month` (List of Number) The specific months for a recurring schedule. Valid values are 1-12. - `on_month_day` (List of Number) The specific days of the month for a recurring schedule. Valid values are 1-31. diff --git a/internal/kibana/maintenance_window/schema.go b/internal/kibana/maintenance_window/schema.go index b37da6eb0..4ad24e1c5 100644 --- a/internal/kibana/maintenance_window/schema.go +++ b/internal/kibana/maintenance_window/schema.go @@ -62,7 +62,7 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR }, }, "duration": schema.StringAttribute{ - Description: "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", + Description: "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for days, hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", Required: true, Validators: []validator.String{ validators.StringIsAlertingDuration{}, @@ -85,7 +85,7 @@ func (r *MaintenanceWindowResource) Schema(_ context.Context, _ resource.SchemaR }, }, "every": schema.StringAttribute{ - Description: "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", + Description: "The duration of the schedule. It allows values in `` format. `` is one of `d`, `h`, `m`, or `s` for days, hours, minutes, seconds. For example: `1d`, `5h`, `30m`, `5000s`.", Optional: true, Validators: []validator.String{ validators.StringIsMaintenanceWindowIntervalFrequency{}, From 3c96fec7b337ea0eea2b74ace52000387c5c69d4 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Fri, 5 Sep 2025 14:58:25 +0200 Subject: [PATCH 27/28] Addressing PR comments. --- internal/kibana/maintenance_window/create.go | 4 ++++ internal/kibana/maintenance_window/models.go | 6 ------ internal/kibana/maintenance_window/read.go | 4 ++++ internal/kibana/maintenance_window/update.go | 4 ++++ internal/kibana/validators/is_alerting_duration.go | 5 ++--- internal/kibana/validators/is_iso8601_string.go | 3 +-- .../kibana/validators/is_maintenance_window_interval.go | 3 +-- .../kibana/validators/is_maintenance_window_week_day.go | 3 +-- 8 files changed, 17 insertions(+), 15 deletions(-) diff --git a/internal/kibana/maintenance_window/create.go b/internal/kibana/maintenance_window/create.go index de963d426..a38a14a3c 100644 --- a/internal/kibana/maintenance_window/create.go +++ b/internal/kibana/maintenance_window/create.go @@ -7,6 +7,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/utils" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" ) func (r *MaintenanceWindowResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { @@ -74,6 +75,9 @@ func (r *MaintenanceWindowResource) Create(ctx context.Context, req resource.Cre return } + planMaintenanceWindow.ID = types.StringValue(maintenanceWindowID) + planMaintenanceWindow.SpaceID = types.StringValue(spaceID) + diags = resp.State.Set(ctx, planMaintenanceWindow) resp.Diagnostics.Append(diags...) } diff --git a/internal/kibana/maintenance_window/models.go b/internal/kibana/maintenance_window/models.go index 98e6d72fe..22dff578f 100644 --- a/internal/kibana/maintenance_window/models.go +++ b/internal/kibana/maintenance_window/models.go @@ -157,12 +157,6 @@ func (model MaintenanceWindowModel) getMaintenanceWindowIDAndSpaceID() (maintena func (model *MaintenanceWindowModel) _fromAPIResponse(ctx context.Context, response ResponseJson) diag.Diagnostics { var diags = diag.Diagnostics{} - resourceID := clients.CompositeId{ - ClusterId: model.SpaceID.ValueString(), - ResourceId: response.Id, - } - - model.ID = types.StringValue(resourceID.String()) model.Title = types.StringValue(response.Title) model.Enabled = types.BoolValue(response.Enabled) diff --git a/internal/kibana/maintenance_window/read.go b/internal/kibana/maintenance_window/read.go index 3e358a617..aafc2a315 100644 --- a/internal/kibana/maintenance_window/read.go +++ b/internal/kibana/maintenance_window/read.go @@ -6,6 +6,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" ) func (r *MaintenanceWindowResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { @@ -53,6 +54,9 @@ func (r *MaintenanceWindowResource) Read(ctx context.Context, req resource.ReadR return } + stateModel.ID = types.StringValue(maintenanceWindowID) + stateModel.SpaceID = types.StringValue(spaceID) + diags = resp.State.Set(ctx, stateModel) resp.Diagnostics.Append(diags...) } diff --git a/internal/kibana/maintenance_window/update.go b/internal/kibana/maintenance_window/update.go index c2136b3ef..2c0942fce 100644 --- a/internal/kibana/maintenance_window/update.go +++ b/internal/kibana/maintenance_window/update.go @@ -5,6 +5,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" ) func (r *MaintenanceWindowResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -71,6 +72,9 @@ func (r *MaintenanceWindowResource) Update(ctx context.Context, req resource.Upd return } + planMaintenanceWindow.ID = types.StringValue(maintenanceWindowID) + planMaintenanceWindow.SpaceID = types.StringValue(spaceID) + diags = resp.State.Set(ctx, planMaintenanceWindow) resp.Diagnostics.Append(diags...) } diff --git a/internal/kibana/validators/is_alerting_duration.go b/internal/kibana/validators/is_alerting_duration.go index 1d7cc7917..715b91769 100644 --- a/internal/kibana/validators/is_alerting_duration.go +++ b/internal/kibana/validators/is_alerting_duration.go @@ -2,7 +2,6 @@ package validators import ( "context" - "fmt" "regexp" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -35,7 +34,7 @@ func (s StringIsAlertingDuration) ValidateString(_ context.Context, req validato resp.Diagnostics.AddAttributeError( req.Path, "expected value to be a valid alerting duration", - fmt.Sprintf("This value must be a valid alerting duration in seconds (s), minutes (m), hours (h), or days (d) %s", err), + "This value must be a valid alerting duration in seconds (s), minutes (m), hours (h), or days (d).", ) return } @@ -46,5 +45,5 @@ func (s StringIsAlertingDuration) ValidateString(_ context.Context, req validato //nolint:staticcheck func StringIsAlertingDurationSDKV2() schema.SchemaValidateFunc { r := regexp.MustCompile(alertingDurationPattern) - return validation.StringMatch(r, "string is not a valid Alerting duration in seconds (s), minutes (m), hours (h), or days (d)") + return validation.StringMatch(r, "string is not a valid Alerting duration in seconds (s), minutes (m), hours (h), or days (d).") } diff --git a/internal/kibana/validators/is_iso8601_string.go b/internal/kibana/validators/is_iso8601_string.go index 4c6fcd51e..ba5dd31b0 100644 --- a/internal/kibana/validators/is_iso8601_string.go +++ b/internal/kibana/validators/is_iso8601_string.go @@ -2,7 +2,6 @@ package validators import ( "context" - "fmt" "regexp" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -32,7 +31,7 @@ func (s StringIsISO8601) ValidateString(_ context.Context, req validator.StringR resp.Diagnostics.AddAttributeError( req.Path, "expected value to be a valid ISO8601 string", - fmt.Sprintf("This value must be a valid ISO8601 date and time formatted string %s", err), + "This value must be a valid ISO8601 date and time formatted string.", ) return } diff --git a/internal/kibana/validators/is_maintenance_window_interval.go b/internal/kibana/validators/is_maintenance_window_interval.go index 0e68bba2a..3f0f6c1a6 100644 --- a/internal/kibana/validators/is_maintenance_window_interval.go +++ b/internal/kibana/validators/is_maintenance_window_interval.go @@ -2,7 +2,6 @@ package validators import ( "context" - "fmt" "regexp" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -32,7 +31,7 @@ func (s StringIsMaintenanceWindowIntervalFrequency) ValidateString(_ context.Con resp.Diagnostics.AddAttributeError( req.Path, "expected value to be a valid interval/frequency", - fmt.Sprintf("This value must be a valid interval/frequency. Allowed values are in the `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`. %s", err), + "This value must be a valid interval/frequency. Allowed values are in the `` format. `` is one of `d`, `w`, `M`, or `y` for days, weeks, months, years. For example: `15d`, `2w`, `3m`, `1y`.", ) return } diff --git a/internal/kibana/validators/is_maintenance_window_week_day.go b/internal/kibana/validators/is_maintenance_window_week_day.go index 85839ecd1..07c4cce48 100644 --- a/internal/kibana/validators/is_maintenance_window_week_day.go +++ b/internal/kibana/validators/is_maintenance_window_week_day.go @@ -2,7 +2,6 @@ package validators import ( "context" - "fmt" "regexp" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -32,7 +31,7 @@ func (s StringIsMaintenanceWindowOnWeekDay) ValidateString(_ context.Context, re resp.Diagnostics.AddAttributeError( req.Path, "expected value to be a valid OnWeekDay", - fmt.Sprintf("This value must be a valid OnWeekDay. Accepted values are specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`). %s", err), + "This value must be a valid OnWeekDay. Accepted values are specific days of the week (`[MO,TU,WE,TH,FR,SA,SU]`) or nth day of month (`[+1MO, -3FR, +2WE, -4SA, -5SU]`).", ) return } From cff95bad55ec86f5c5b89bf5fc1fb0dc188b5a45 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Fri, 5 Sep 2025 15:16:11 +0200 Subject: [PATCH 28/28] fix tests --- internal/kibana/maintenance_window/models_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/kibana/maintenance_window/models_test.go b/internal/kibana/maintenance_window/models_test.go index 994e34314..84b641ce2 100644 --- a/internal/kibana/maintenance_window/models_test.go +++ b/internal/kibana/maintenance_window/models_test.go @@ -13,7 +13,6 @@ import ( ) var modelWithAllFields = MaintenanceWindowModel{ - ID: types.StringValue("/existing-space-id/id"), Title: types.StringValue("test response"), Enabled: types.BoolValue(true), @@ -40,7 +39,6 @@ var modelWithAllFields = MaintenanceWindowModel{ } var modelOccurrencesNoScope = MaintenanceWindowModel{ - ID: types.StringValue("/existing-space-id/id"), Title: types.StringValue("test response"), Enabled: types.BoolValue(true),