Skip to content

[ResponseOps] Maintenance Window Resource #1037

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Prev Previous commit
Next Next commit
Validation and tests.
  • Loading branch information
adcoelho committed Apr 8, 2025
commit 42a71d9c7eb1d01f5c52dd0f1ffb1393f81a4926
2 changes: 2 additions & 0 deletions internal/clients/kibana/maintenance_window.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func CreateMaintenanceWindow(ctx context.Context, apiClient ApiClient, maintenan
Custom: alerting.CreateMaintenanceWindowRequestScheduleCustom{
Start: maintenanceWindow.CustomSchedule.Start,
Duration: maintenanceWindow.CustomSchedule.Duration,
Timezone: maintenanceWindow.CustomSchedule.Timezone,
},
}

Expand Down Expand Up @@ -199,6 +200,7 @@ func UpdateMaintenanceWindow(ctx context.Context, apiClient ApiClient, maintenan
Custom: alerting.CreateMaintenanceWindowRequestScheduleCustom{
Start: maintenanceWindow.CustomSchedule.Start,
Duration: maintenanceWindow.CustomSchedule.Duration,
Timezone: maintenanceWindow.CustomSchedule.Timezone,
},
}

Expand Down
255 changes: 255 additions & 0 deletions internal/clients/kibana/maintenance_window_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package kibana

import (
"context"
"fmt"
"io"
"net/http"
"strings"
"testing"

"github.com/elastic/terraform-provider-elasticstack/generated/alerting"
"github.com/elastic/terraform-provider-elasticstack/internal/models"
"github.com/elastic/terraform-provider-elasticstack/internal/utils"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/stretchr/testify/require"
gomock "go.uber.org/mock/gomock"
)

func Test_maintenanceWindowResponseToModel(t *testing.T) {
tests := []struct {
name string
spaceId string
maintenanceWindowResponse *alerting.MaintenanceWindowResponseProperties
expectedModel *models.MaintenanceWindow
}{
{
name: "nil response should return a nil model",
spaceId: "space-id",
maintenanceWindowResponse: nil,
expectedModel: nil,
},
{
name: "nil optional fields should not blow up the transform",
spaceId: "space-id",
maintenanceWindowResponse: &alerting.MaintenanceWindowResponseProperties{
Id: "some-long-id",
CreatedBy: "me",
CreatedAt: "today",
UpdatedBy: "me",
UpdatedAt: "today",
Enabled: true,
Status: "running",
Title: "maintenance-window-id",
Schedule: alerting.MaintenanceWindowResponsePropertiesSchedule{
Custom: alerting.MaintenanceWindowResponsePropertiesScheduleCustom{
Duration: "12d",
Start: "1999-02-02T05:00:00.200Z",
Recurring: nil,
Timezone: nil,
},
},
Scope: nil,
},
expectedModel: &models.MaintenanceWindow{
MaintenanceWindowId: "some-long-id",
SpaceId: "space-id",
Title: "maintenance-window-id",
Enabled: true,
CustomSchedule: models.MaintenanceWindowSchedule{
Duration: "12d",
Start: "1999-02-02T05:00:00.200Z",
},
},
},
{
name: "a full response should be successfully transformed",
spaceId: "space-id",
maintenanceWindowResponse: &alerting.MaintenanceWindowResponseProperties{
Id: "maintenance-window-id",
Title: "maintenance-window-title",
CreatedBy: "me",
CreatedAt: "today",
UpdatedBy: "me",
UpdatedAt: "today",
Enabled: true,
Status: "running",
Schedule: alerting.MaintenanceWindowResponsePropertiesSchedule{
Custom: alerting.MaintenanceWindowResponsePropertiesScheduleCustom{
Duration: "12d",
Start: "1999-02-02T05:00:00.200Z",
Timezone: utils.Pointer("Asia/Taipei"),
Recurring: &alerting.MaintenanceWindowResponsePropertiesScheduleCustomRecurring{
End: utils.Pointer("2029-05-17T05:05:00.000Z"),
Every: utils.Pointer("20d"),
Occurrences: utils.Pointer(float32(30)),
OnMonth: []float32{2},
OnMonthDay: []float32{1},
OnWeekDay: []string{"WE", "TU"},
},
},
},
Scope: &alerting.MaintenanceWindowResponsePropertiesScope{
Alerting: alerting.MaintenanceWindowResponsePropertiesScopeAlerting{
Query: alerting.MaintenanceWindowResponsePropertiesScopeAlertingQuery{
Kql: "_id: 'foobar'",
},
},
},
},
expectedModel: &models.MaintenanceWindow{
MaintenanceWindowId: "maintenance-window-id",
Title: "maintenance-window-title",
SpaceId: "space-id",
Enabled: true,
CustomSchedule: models.MaintenanceWindowSchedule{
Duration: "12d",
Start: "1999-02-02T05:00:00.200Z",
Timezone: utils.Pointer("Asia/Taipei"),
Recurring: &models.MaintenanceWindowScheduleRecurring{
End: utils.Pointer("2029-05-17T05:05:00.000Z"),
Every: utils.Pointer("20d"),
Occurrences: utils.Pointer(float32(30)),
OnMonth: &[]float32{2},
OnMonthDay: &[]float32{1},
OnWeekDay: &[]string{"WE", "TU"},
},
},
Scope: &models.MaintenanceWindowScope{
Alerting: &models.MaintenanceWindowAlertingScope{
Kql: "_id: 'foobar'",
},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
model := maintenanceWindowResponseToModel(tt.spaceId, tt.maintenanceWindowResponse)

require.Equal(t, tt.expectedModel, model)
})
}
}

func Test_CreateUpdateMaintenanceWindow(t *testing.T) {
ctrl := gomock.NewController(t)

getApiClient := func() (ApiClient, *alerting.MockAlertingAPI) {
apiClient := NewMockApiClient(ctrl)
apiClient.EXPECT().SetAlertingAuthContext(gomock.Any()).DoAndReturn(func(ctx context.Context) context.Context { return ctx })
alertingClient := alerting.NewMockAlertingAPI(ctrl)
apiClient.EXPECT().GetAlertingClient().DoAndReturn(func() (alerting.AlertingAPI, error) { return alertingClient, nil })
return apiClient, alertingClient
}

tests := []struct {
name string
testFunc func(ctx context.Context, apiClient ApiClient, maintenanceWindow models.MaintenanceWindow) (*models.MaintenanceWindow, diag.Diagnostics)
client ApiClient
maintenanceWindow models.MaintenanceWindow
expectedRes *models.MaintenanceWindow
expectedErr string
}{
{
name: "CreateMaintenanceWindow should not crash when backend returns 4xx",
testFunc: CreateMaintenanceWindow,
client: func() ApiClient {
apiClient, alertingClient := getApiClient()
alertingClient.EXPECT().CreateMaintenanceWindow(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spaceId string) alerting.ApiCreateMaintenanceWindowRequest {
return alerting.ApiCreateMaintenanceWindowRequest{ApiService: alertingClient}
})
alertingClient.EXPECT().CreateMaintenanceWindowExecute(gomock.Any()).DoAndReturn(func(r alerting.ApiCreateMaintenanceWindowRequest) (*alerting.MaintenanceWindowResponseProperties, *http.Response, error) {
return nil, &http.Response{
StatusCode: 401,
Body: io.NopCloser(strings.NewReader("some error")),
}, nil
})
return apiClient
}(),
maintenanceWindow: models.MaintenanceWindow{},
expectedRes: nil,
expectedErr: "some error",
},
{
name: "UpdateMaintenanceWindow should not crash when backend returns 4xx",
testFunc: UpdateMaintenanceWindow,
client: func() ApiClient {
apiClient, alertingClient := getApiClient()
alertingClient.EXPECT().UpdateMaintenanceWindow(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, maintenanceWindowId string, spaceId string) alerting.ApiUpdateMaintenanceWindowRequest {
return alerting.ApiUpdateMaintenanceWindowRequest{ApiService: alertingClient}
})
alertingClient.EXPECT().UpdateMaintenanceWindowExecute(gomock.Any()).DoAndReturn(func(r alerting.ApiUpdateMaintenanceWindowRequest) (*alerting.MaintenanceWindowResponseProperties, *http.Response, error) {
return nil, &http.Response{
StatusCode: 401,
Body: io.NopCloser(strings.NewReader("some error")),
}, nil
})
return apiClient
}(),
maintenanceWindow: models.MaintenanceWindow{},
expectedRes: nil,
expectedErr: "some error",
},
{
name: "CreateMaintenanceWindow should not crash when backend returns an empty response and HTTP 200",
testFunc: CreateMaintenanceWindow,
client: func() ApiClient {
apiClient, alertingClient := getApiClient()
alertingClient.EXPECT().CreateMaintenanceWindow(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spaceId string) alerting.ApiCreateMaintenanceWindowRequest {
return alerting.ApiCreateMaintenanceWindowRequest{ApiService: alertingClient}
})
alertingClient.EXPECT().CreateMaintenanceWindowExecute(gomock.Any()).DoAndReturn(func(r alerting.ApiCreateMaintenanceWindowRequest) (*alerting.MaintenanceWindowResponseProperties, *http.Response, error) {
return nil, &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader("everything seems fine")),
}, nil
})
return apiClient
}(),
maintenanceWindow: models.MaintenanceWindow{},
expectedRes: nil,
expectedErr: "empty response",
},
{
name: "UpdateMaintenanceWindow should not crash when backend returns an empty response and HTTP 200",
testFunc: UpdateMaintenanceWindow,
client: func() ApiClient {
apiClient, alertingClient := getApiClient()
alertingClient.EXPECT().UpdateMaintenanceWindow(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, maintenanceWindowId string, spaceId string) alerting.ApiUpdateMaintenanceWindowRequest {
return alerting.ApiUpdateMaintenanceWindowRequest{ApiService: alertingClient}
})
alertingClient.EXPECT().UpdateMaintenanceWindowExecute(gomock.Any()).DoAndReturn(func(r alerting.ApiUpdateMaintenanceWindowRequest) (*alerting.MaintenanceWindowResponseProperties, *http.Response, error) {
return nil, &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader("everything seems fine")),
}, nil
})
return apiClient
}(),
maintenanceWindow: models.MaintenanceWindow{},
expectedRes: nil,
expectedErr: "empty response",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
maintenanceWindow, diags := tt.testFunc(context.Background(), tt.client, tt.maintenanceWindow)

if tt.expectedRes == nil {
require.Nil(t, maintenanceWindow)
} else {
require.Equal(t, tt.expectedRes, maintenanceWindow)
}

if tt.expectedErr != "" {
require.NotEmpty(t, diags)
if !strings.Contains(diags[0].Detail, tt.expectedErr) {
require.Fail(t, fmt.Sprintf("Diags ['%s'] should contain message ['%s']", diags[0].Detail, tt.expectedErr))
}
}
})
}
}
15 changes: 3 additions & 12 deletions internal/kibana/alerting.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"regexp"
"strings"

"github.com/elastic/terraform-provider-elasticstack/internal/clients"
Expand All @@ -22,14 +21,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": {
Expand Down Expand Up @@ -80,7 +71,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: utils.StringIsAlertingDuration(),
},
"actions": {
Description: "An action that runs under defined conditions.",
Expand Down Expand Up @@ -129,7 +120,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: utils.StringIsAlertingDuration(),
},
},
},
Expand Down Expand Up @@ -207,7 +198,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: utils.StringIsAlertingDuration(),
},
"scheduled_task_id": {
Description: "ID of the scheduled task that will execute the alert.",
Expand Down
Loading
Loading