diff --git a/controllers/alphacontrollers/alert_controller_test.go b/controllers/alphacontrollers/alert_controller_test.go index 80958bd..1c4fc0c 100644 --- a/controllers/alphacontrollers/alert_controller_test.go +++ b/controllers/alphacontrollers/alert_controller_test.go @@ -3,6 +3,7 @@ package alphacontrollers import ( "context" "encoding/json" + "fmt" "testing" utils "github.com/coralogix/coralogix-operator/apis" @@ -116,41 +117,6 @@ func expectedAlertCRD() *coralogixv1alpha1.Alert { } } -var expectedAlertStatus = &coralogixv1alpha1.AlertStatus{ - ID: pointer.String("id"), - Name: "name", - Description: "description", - Active: true, - Severity: "Critical", - Labels: map[string]string{"key": "value", "managed-by": "coralogix-operator"}, - AlertType: coralogixv1alpha1.AlertType{ - Metric: &coralogixv1alpha1.Metric{ - Promql: &coralogixv1alpha1.Promql{ - SearchQuery: "http_requests_total{status!~\"4..\"}", - Conditions: coralogixv1alpha1.PromqlConditions{ - AlertWhen: "MoreThanUsual", - Threshold: utils.FloatToQuantity(3.0), - TimeWindow: coralogixv1alpha1.MetricTimeWindow("TwelveHours"), - MinNonNullValuesPercentage: pointer.Int(10), - ReplaceMissingValueWithZero: false, - }, - }, - }, - }, - NotificationGroups: []coralogixv1alpha1.NotificationGroup{ - { - Notifications: []coralogixv1alpha1.Notification{ - { - RetriggeringPeriodMinutes: 10, - NotifyOn: coralogixv1alpha1.NotifyOnTriggeredAndResolved, - EmailRecipients: []string{"example@coralogix.com"}, - }, - }, - }, - }, - PayloadFilters: []string{"filter"}, -} - func TestFlattenAlerts(t *testing.T) { alert := &alerts.Alert{ UniqueIdentifier: wrapperspb.String("id1"), @@ -228,9 +194,11 @@ func TestAlertReconciler_Reconcile(t *testing.T) { scheme := runtime.NewScheme() utilruntime.Must(coralogixv1alpha1.AddToScheme(scheme)) mgr, _ := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ - Scheme: scheme, + Scheme: scheme, + MetricsBindAddress: "0", }) - ctx := context.Background() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() go mgr.GetCache().Start(ctx) mgr.GetCache().WaitForCacheSync(ctx) withWatch, err := client.NewWithWatch(mgr.GetConfig(), client.Options{ @@ -293,9 +261,11 @@ func TestAlertReconciler_Reconcile_5XX_StatusError(t *testing.T) { scheme := runtime.NewScheme() utilruntime.Must(coralogixv1alpha1.AddToScheme(scheme)) mgr, _ := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ - Scheme: scheme, + Scheme: scheme, + MetricsBindAddress: "0", }) - ctx := context.Background() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() go mgr.GetCache().Start(ctx) mgr.GetCache().WaitForCacheSync(ctx) withWatch, err := client.NewWithWatch(mgr.GetConfig(), client.Options{ @@ -329,6 +299,10 @@ func TestAlertReconciler_Reconcile_5XX_StatusError(t *testing.T) { actualAlertCRD := &coralogixv1alpha1.Alert{} err = r.Client.Get(ctx, namespacedName, actualAlertCRD) assert.NoError(t, err) + + err = r.Client.Delete(ctx, actualAlertCRD) + <-watcher.ResultChan() + r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) } // Creates a mock webhooks client that contains a single webhook with id "id1". @@ -383,7 +357,7 @@ func createMockAlertsClientWith5XXStatusError(mockCtrl *gomock.Controller, alert CreateAlert(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, _ *alerts.CreateAlertRequest) (*alerts.CreateAlertResponse, error) { if !wasCalled { wasCalled = true - return nil, errors.NewBadRequest("bad request") + return nil, errors.NewInternalError(fmt.Errorf("internal error")) } alertExist = true return &alerts.CreateAlertResponse{Alert: alert}, nil diff --git a/controllers/alphacontrollers/recordingrulegroupset_controller.go b/controllers/alphacontrollers/recordingrulegroupset_controller.go index 0921bcc..a46a986 100644 --- a/controllers/alphacontrollers/recordingrulegroupset_controller.go +++ b/controllers/alphacontrollers/recordingrulegroupset_controller.go @@ -147,7 +147,7 @@ func (r *RecordingRuleGroupSetReconciler) Reconcile(ctx context.Context, req ctr log.V(1).Info("Creating RecordingRuleGroupSet", "RecordingRuleGroupSet", jstr) if createRRGResp, err := rRGClient.CreateRecordingRuleGroupSet(ctx, createRuleGroupReq); err == nil { jstr, _ := jsm.MarshalToString(createRRGResp) - log.V(1).Info("RecordingRuleGroupSet was updated", "RecordingRuleGroupSet", jstr) + log.V(1).Info("RecordingRuleGroupSet was created", "RecordingRuleGroupSet", jstr) //To avoid a situation of the operator falling between the creation of the ruleGroup in coralogix and being saved in the cluster (something that would cause it to be created again and again), its id will be saved ASAP. id := createRRGResp.Id diff --git a/controllers/alphacontrollers/recordingrulegroupset_controller_test.go b/controllers/alphacontrollers/recordingrulegroupset_controller_test.go new file mode 100644 index 0000000..898f0df --- /dev/null +++ b/controllers/alphacontrollers/recordingrulegroupset_controller_test.go @@ -0,0 +1,273 @@ +package alphacontrollers + +import ( + "context" + "fmt" + "testing" + + coralogixv1alpha1 "github.com/coralogix/coralogix-operator/apis/coralogix/v1alpha1" + "github.com/coralogix/coralogix-operator/controllers/clientset" + rrg "github.com/coralogix/coralogix-operator/controllers/clientset/grpc/recording-rules-groups/v2" + "github.com/coralogix/coralogix-operator/controllers/mock_clientset" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/emptypb" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +var recordingRuleGroupSetBackendSchema = &rrg.OutRuleGroupSet{ + Id: "id1", + Groups: []*rrg.OutRuleGroup{ + { + Name: "name", + Interval: pointer.Uint32(60), + Limit: pointer.Uint64(100), + Rules: []*rrg.OutRule{ + { + Record: "record", + Expr: "vector(1)", + Labels: map[string]string{"key": "value"}, + }, + }, + }, + }, +} + +func expectedRecordingRuleGroupSetCRD() *coralogixv1alpha1.RecordingRuleGroupSet { + return &coralogixv1alpha1.RecordingRuleGroupSet{ + TypeMeta: metav1.TypeMeta{Kind: "RecordingRuleGroupSet", APIVersion: "coralogix.com/v1alpha1"}, + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + Spec: coralogixv1alpha1.RecordingRuleGroupSetSpec{ + Groups: []coralogixv1alpha1.RecordingRuleGroup{ + { + Name: "name", + IntervalSeconds: 60, + Limit: 100, + Rules: []coralogixv1alpha1.RecordingRule{ + { + Record: "record", + Expr: "vector(1)", + Labels: map[string]string{"key": "value"}, + }, + }, + }, + }, + }, + } +} + +func TestFlattenRecordingRuleGroupSet(t *testing.T) { + actualStatus := flattenRecordingRuleGroupSet(recordingRuleGroupSetBackendSchema) + expectedStatus := coralogixv1alpha1.RecordingRuleGroupSetStatus{ + ID: pointer.String("id1"), + Groups: []coralogixv1alpha1.RecordingRuleGroup{ + { + Name: "name", + IntervalSeconds: 60, + Limit: 100, + Rules: []coralogixv1alpha1.RecordingRule{ + { + Record: "record", + Expr: "vector(1)", + Labels: map[string]string{"key": "value"}, + }, + }, + }, + }, + } + assert.EqualValues(t, expectedStatus, actualStatus) +} + +func TestRecordingRuleGroupSetReconciler_Reconcile(t *testing.T) { + mockCtrl := gomock.NewController(t) + recordingRuleGroupClient := createRecordingRuleGroupClientSimpleMock(mockCtrl) + mockClientSet := mock_clientset.NewMockClientSetInterface(mockCtrl) + mockClientSet.EXPECT().RecordingRuleGroups().Return(recordingRuleGroupClient).AnyTimes() + + scheme := runtime.NewScheme() + utilruntime.Must(coralogixv1alpha1.AddToScheme(scheme)) + mgr, _ := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: "0", + }) + ctx := context.Background() + defer ctx.Done() + go mgr.GetCache().Start(ctx) + mgr.GetCache().WaitForCacheSync(ctx) + withWatch, err := client.NewWithWatch(mgr.GetConfig(), client.Options{ + Scheme: mgr.GetScheme(), + }) + assert.NoError(t, err) + r := RecordingRuleGroupSetReconciler{ + Client: withWatch, + Scheme: mgr.GetScheme(), + CoralogixClientSet: mockClientSet, + } + r.SetupWithManager(mgr) + + watcher, _ := r.Client.(client.WithWatch).Watch(ctx, &coralogixv1alpha1.RecordingRuleGroupSetList{}) + ctrl.SetLogger(zap.New(zap.UseDevMode(true))) + + err = r.Client.Create(ctx, expectedRecordingRuleGroupSetCRD()) + assert.NoError(t, err) + <-watcher.ResultChan() + + result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) + assert.NoError(t, err) + assert.Equal(t, defaultRequeuePeriod, result.RequeueAfter) + + namespacedName := types.NamespacedName{Namespace: "default", Name: "test"} + actualRecordingRuleGroupSetCRD := &coralogixv1alpha1.RecordingRuleGroupSet{} + err = r.Client.Get(ctx, namespacedName, actualRecordingRuleGroupSetCRD) + assert.NoError(t, err) + + id := actualRecordingRuleGroupSetCRD.Status.ID + if !assert.NotNil(t, id) { + return + } + getRecordingRuleGroupSetRequest := &rrg.FetchRuleGroupSet{Id: *id} + actualRecordingRuleGroupSet, err := r.CoralogixClientSet.RecordingRuleGroups().GetRecordingRuleGroupSet(ctx, getRecordingRuleGroupSetRequest) + assert.NoError(t, err) + assert.EqualValues(t, recordingRuleGroupSetBackendSchema, actualRecordingRuleGroupSet) + + err = r.Client.Delete(ctx, actualRecordingRuleGroupSetCRD) + <-watcher.ResultChan() + + result, err = r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) + assert.NoError(t, err) + assert.Equal(t, false, result.Requeue) + + actualRecordingRuleGroupSet, err = r.CoralogixClientSet.RecordingRuleGroups().GetRecordingRuleGroupSet(ctx, getRecordingRuleGroupSetRequest) + assert.Nil(t, actualRecordingRuleGroupSet) + assert.Error(t, err) +} + +func TestRecordingRuleGroupSetReconciler_Reconcile_5XX_StatusError(t *testing.T) { + mockCtrl := gomock.NewController(t) + recordingRuleGroupClient := createRecordingRuleGroupClientMockWith5XXStatusError(mockCtrl) + mockClientSet := mock_clientset.NewMockClientSetInterface(mockCtrl) + mockClientSet.EXPECT().RecordingRuleGroups().Return(recordingRuleGroupClient).AnyTimes() + + scheme := runtime.NewScheme() + utilruntime.Must(coralogixv1alpha1.AddToScheme(scheme)) + mgr, _ := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: "0", + }) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go mgr.GetCache().Start(ctx) + mgr.GetCache().WaitForCacheSync(ctx) + withWatch, err := client.NewWithWatch(mgr.GetConfig(), client.Options{ + Scheme: mgr.GetScheme(), + }) + assert.NoError(t, err) + r := RecordingRuleGroupSetReconciler{ + Client: withWatch, + Scheme: mgr.GetScheme(), + CoralogixClientSet: mockClientSet, + } + r.SetupWithManager(mgr) + + watcher, _ := r.Client.(client.WithWatch).Watch(ctx, &coralogixv1alpha1.RecordingRuleGroupSetList{}) + ctrl.SetLogger(zap.New(zap.UseDevMode(true))) + + err = r.Client.Create(ctx, expectedRecordingRuleGroupSetCRD()) + assert.NoError(t, err) + <-watcher.ResultChan() + + result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) + assert.Error(t, err) + assert.Equal(t, defaultErrRequeuePeriod, result.RequeueAfter) + + result, err = r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) + assert.NoError(t, err) + assert.Equal(t, defaultRequeuePeriod, result.RequeueAfter) + + namespacedName := types.NamespacedName{Namespace: "default", Name: "test"} + actualRecordingRuleGroupSetCRD := &coralogixv1alpha1.RecordingRuleGroupSet{} + err = r.Client.Get(ctx, namespacedName, actualRecordingRuleGroupSetCRD) + assert.NoError(t, err) + + err = r.Client.Delete(ctx, actualRecordingRuleGroupSetCRD) + <-watcher.ResultChan() + r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) +} + +// createRecordingRuleGroupClientSimpleMock creates a simple mock for RecordingRuleGroupsClientInterface which returns a single recording rule group. +func createRecordingRuleGroupClientSimpleMock(mockCtrl *gomock.Controller) clientset.RecordingRulesGroupsClientInterface { + mockRecordingRuleGroupsClient := mock_clientset.NewMockRecordingRulesGroupsClientInterface(mockCtrl) + + var recordingRuleGroupExist bool + + mockRecordingRuleGroupsClient.EXPECT(). + CreateRecordingRuleGroupSet(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, _ *rrg.CreateRuleGroupSet) (*rrg.CreateRuleGroupSetResult, error) { + recordingRuleGroupExist = true + return &rrg.CreateRuleGroupSetResult{Id: "id1"}, nil + }).AnyTimes() + + mockRecordingRuleGroupsClient.EXPECT(). + GetRecordingRuleGroupSet(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req *rrg.FetchRuleGroupSet) (*rrg.OutRuleGroupSet, error) { + if recordingRuleGroupExist { + return recordingRuleGroupSetBackendSchema, nil + } + return nil, errors.NewNotFound(schema.GroupResource{}, "id1") + }).AnyTimes() + + mockRecordingRuleGroupsClient.EXPECT(). + DeleteRecordingRuleGroupSet(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, req *rrg.DeleteRuleGroupSet) (*emptypb.Empty, error) { + if recordingRuleGroupExist { + recordingRuleGroupExist = false + return &emptypb.Empty{}, nil + } + return nil, errors.NewNotFound(schema.GroupResource{}, "id1") + }).AnyTimes() + + return mockRecordingRuleGroupsClient +} + +// createRecordingRuleGroupClientMockWith5XXStatusError creates a simple mock for RecordingRuleGroupsClientInterface which first returns 5xx status error, then returns a single recording rule group. +func createRecordingRuleGroupClientMockWith5XXStatusError(mockCtrl *gomock.Controller) clientset.RecordingRulesGroupsClientInterface { + mockRecordingRuleGroupsClient := mock_clientset.NewMockRecordingRulesGroupsClientInterface(mockCtrl) + + var recordingRuleGroupExist bool + var wasCalled bool + + mockRecordingRuleGroupsClient.EXPECT(). + CreateRecordingRuleGroupSet(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, _ *rrg.CreateRuleGroupSet) (*rrg.CreateRuleGroupSetResult, error) { + if !wasCalled { + wasCalled = true + return nil, errors.NewInternalError(fmt.Errorf("internal error")) + } + recordingRuleGroupExist = true + return &rrg.CreateRuleGroupSetResult{Id: "id1"}, nil + }).AnyTimes() + + mockRecordingRuleGroupsClient.EXPECT(). + GetRecordingRuleGroupSet(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req *rrg.FetchRuleGroupSet) (*rrg.OutRuleGroupSet, error) { + if recordingRuleGroupExist { + return recordingRuleGroupSetBackendSchema, nil + } + return nil, errors.NewNotFound(schema.GroupResource{}, "id1") + }).AnyTimes() + + mockRecordingRuleGroupsClient.EXPECT(). + DeleteRecordingRuleGroupSet(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, req *rrg.DeleteRuleGroupSet) (*emptypb.Empty, error) { + if recordingRuleGroupExist { + recordingRuleGroupExist = false + return &emptypb.Empty{}, nil + } + return nil, errors.NewNotFound(schema.GroupResource{}, "id1") + }).AnyTimes() + + return mockRecordingRuleGroupsClient +} diff --git a/controllers/alphacontrollers/rulegroup_controller_test.go b/controllers/alphacontrollers/rulegroup_controller_test.go index e7be2a5..63157c0 100644 --- a/controllers/alphacontrollers/rulegroup_controller_test.go +++ b/controllers/alphacontrollers/rulegroup_controller_test.go @@ -1,49 +1,96 @@ package alphacontrollers import ( + "context" + "fmt" "testing" coralogixv1alpha1 "github.com/coralogix/coralogix-operator/apis/coralogix/v1alpha1" + "github.com/coralogix/coralogix-operator/controllers/clientset" rulesgroups "github.com/coralogix/coralogix-operator/controllers/clientset/grpc/rules-groups/v1" + "github.com/coralogix/coralogix-operator/controllers/mock_clientset" "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/wrapperspb" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log/zap" ) -func TestFlattenRuleGroupsErrorsOnBadResponse(t *testing.T) { - ruleGroup := &rulesgroups.RuleGroup{ - Id: wrapperspb.String("id"), - Name: wrapperspb.String("name"), - Description: wrapperspb.String("description"), - Creator: wrapperspb.String("creator"), - Enabled: wrapperspb.Bool(true), - Hidden: wrapperspb.Bool(false), - RuleMatchers: []*rulesgroups.RuleMatcher{}, - RuleSubgroups: []*rulesgroups.RuleSubgroup{ - { - Rules: []*rulesgroups.Rule{ - { - Id: wrapperspb.String("rule_id"), - Name: wrapperspb.String("rule_name"), - Description: wrapperspb.String("rule_description"), - SourceField: wrapperspb.String("text"), - Parameters: &rulesgroups.RuleParameters{ - RuleParameters: nil, +var ruleGroupBackendSchema = &rulesgroups.RuleGroup{ + Id: wrapperspb.String("id"), + Name: wrapperspb.String("name"), + Description: wrapperspb.String("description"), + Creator: wrapperspb.String("creator"), + Enabled: wrapperspb.Bool(true), + Hidden: wrapperspb.Bool(false), + RuleMatchers: []*rulesgroups.RuleMatcher{}, + RuleSubgroups: []*rulesgroups.RuleSubgroup{ + { + Id: wrapperspb.String("subgroup_id"), + Order: wrapperspb.UInt32(1), + Rules: []*rulesgroups.Rule{ + { + Id: wrapperspb.String("rule_id"), + Name: wrapperspb.String("rule_name"), + Description: wrapperspb.String("rule_description"), + SourceField: wrapperspb.String("text"), + Parameters: &rulesgroups.RuleParameters{ + RuleParameters: &rulesgroups.RuleParameters_JsonExtractParameters{ + JsonExtractParameters: &rulesgroups.JsonExtractParameters{ + DestinationField: rulesgroups.JsonExtractParameters_DESTINATION_FIELD_SEVERITY, + Rule: wrapperspb.String(`{"severity": "info"}`), + }, }, - Enabled: wrapperspb.Bool(true), - Order: wrapperspb.UInt32(1), }, + Enabled: wrapperspb.Bool(true), + Order: wrapperspb.UInt32(3), }, }, }, - Order: wrapperspb.UInt32(1), - } + }, + Order: wrapperspb.UInt32(1), +} - status, err := flattenRuleGroup(ruleGroup) - assert.Error(t, err) - assert.Nil(t, status) +func expectedRuleGroupCRD() *coralogixv1alpha1.RuleGroup { + return &coralogixv1alpha1.RuleGroup{ + TypeMeta: metav1.TypeMeta{Kind: "RecordingRuleGroupSet", APIVersion: "coralogix.com/v1alpha1"}, + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + Spec: coralogixv1alpha1.RuleGroupSpec{ + Name: "name", + Description: "description", + Creator: "creator", + Active: true, + Hidden: false, + Order: pointer.Int32(1), + RuleSubgroups: []coralogixv1alpha1.RuleSubGroup{ + { + Order: pointer.Int32(1), + Rules: []coralogixv1alpha1.Rule{ + { + Name: "rule_name", + Description: "rule_description", + Active: true, + JsonExtract: &coralogixv1alpha1.JsonExtract{ + DestinationField: coralogixv1alpha1.DestinationFieldRuleSeverity, + JsonKey: "{\"severity\": \"info\"}", + }, + }, + }, + }, + }, + }, + } } -func TestFlattenRuleGroups(t *testing.T) { +func TestFlattenRuleGroupsErrorsOnBadResponse(t *testing.T) { ruleGroup := &rulesgroups.RuleGroup{ Id: wrapperspb.String("id"), Name: wrapperspb.String("name"), @@ -54,8 +101,6 @@ func TestFlattenRuleGroups(t *testing.T) { RuleMatchers: []*rulesgroups.RuleMatcher{}, RuleSubgroups: []*rulesgroups.RuleSubgroup{ { - Id: wrapperspb.String("subgroup_id"), - Order: wrapperspb.UInt32(2), Rules: []*rulesgroups.Rule{ { Id: wrapperspb.String("rule_id"), @@ -63,15 +108,10 @@ func TestFlattenRuleGroups(t *testing.T) { Description: wrapperspb.String("rule_description"), SourceField: wrapperspb.String("text"), Parameters: &rulesgroups.RuleParameters{ - RuleParameters: &rulesgroups.RuleParameters_JsonExtractParameters{ - JsonExtractParameters: &rulesgroups.JsonExtractParameters{ - DestinationField: rulesgroups.JsonExtractParameters_DESTINATION_FIELD_SEVERITY, - Rule: wrapperspb.String(`{"severity": "info"}`), - }, - }, + RuleParameters: nil, }, Enabled: wrapperspb.Bool(true), - Order: wrapperspb.UInt32(3), + Order: wrapperspb.UInt32(1), }, }, }, @@ -80,11 +120,17 @@ func TestFlattenRuleGroups(t *testing.T) { } status, err := flattenRuleGroup(ruleGroup) + assert.Error(t, err) + assert.Nil(t, status) +} + +func TestFlattenRuleGroups(t *testing.T) { + actualStatus, err := flattenRuleGroup(ruleGroupBackendSchema) assert.NoError(t, err) id := "id" subgroupId := "subgroup_id" - expected := &coralogixv1alpha1.RuleGroupStatus{ + expectedStatus := &coralogixv1alpha1.RuleGroupStatus{ ID: &id, Name: "name", Description: "description", @@ -94,12 +140,12 @@ func TestFlattenRuleGroups(t *testing.T) { Severities: nil, Hidden: false, Creator: "creator", - Order: int32Ptr(1), + Order: pointer.Int32(1), RuleSubgroups: []coralogixv1alpha1.RuleSubGroup{ { ID: &subgroupId, Active: false, - Order: int32Ptr(2), + Order: pointer.Int32(1), Rules: []coralogixv1alpha1.Rule{ { Name: "rule_name", @@ -117,7 +163,198 @@ func TestFlattenRuleGroups(t *testing.T) { }, } - assert.Equal(t, expected, status) + assert.Equal(t, expectedStatus, actualStatus) +} + +func TestRuleGroupReconciler_Reconcile(t *testing.T) { + mockCtrl := gomock.NewController(t) + ruleGroupClient := createRuleGroupClientSimpleMock(mockCtrl) + mockClientSet := mock_clientset.NewMockClientSetInterface(mockCtrl) + mockClientSet.EXPECT().RuleGroups().Return(ruleGroupClient).AnyTimes() + + scheme := runtime.NewScheme() + utilruntime.Must(coralogixv1alpha1.AddToScheme(scheme)) + mgr, _ := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: "0", + }) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go mgr.GetCache().Start(ctx) + mgr.GetCache().WaitForCacheSync(ctx) + withWatch, err := client.NewWithWatch(mgr.GetConfig(), client.Options{ + Scheme: mgr.GetScheme(), + }) + assert.NoError(t, err) + r := RuleGroupReconciler{ + Client: withWatch, + Scheme: mgr.GetScheme(), + CoralogixClientSet: mockClientSet, + } + r.SetupWithManager(mgr) + + watcher, _ := r.Client.(client.WithWatch).Watch(ctx, &coralogixv1alpha1.RuleGroupList{}) + ctrl.SetLogger(zap.New(zap.UseDevMode(true))) + + err = r.Client.Create(ctx, expectedRuleGroupCRD()) + assert.NoError(t, err) + <-watcher.ResultChan() + + result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) + assert.NoError(t, err) + assert.Equal(t, defaultRequeuePeriod, result.RequeueAfter) + + namespacedName := types.NamespacedName{Namespace: "default", Name: "test"} + actualRuleGroupCRD := &coralogixv1alpha1.RuleGroup{} + err = r.Client.Get(ctx, namespacedName, actualRuleGroupCRD) + assert.NoError(t, err) + + id := actualRuleGroupCRD.Status.ID + if !assert.NotNil(t, id) { + return + } + getRuleGroupRequest := &rulesgroups.GetRuleGroupRequest{GroupId: *id} + actualRuleGroup, err := r.CoralogixClientSet.RuleGroups().GetRuleGroup(ctx, getRuleGroupRequest) + assert.NoError(t, err) + assert.EqualValues(t, ruleGroupBackendSchema, actualRuleGroup.GetRuleGroup()) + + err = r.Client.Delete(ctx, actualRuleGroupCRD) + <-watcher.ResultChan() + + result, err = r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) + assert.NoError(t, err) + assert.Equal(t, false, result.Requeue) + + actualRuleGroup, err = r.CoralogixClientSet.RuleGroups().GetRuleGroup(ctx, getRuleGroupRequest) + assert.Nil(t, actualRuleGroup) + assert.Error(t, err) +} + +func TestRuleGroupReconciler_Reconcile_5XX_StatusError(t *testing.T) { + mockCtrl := gomock.NewController(t) + ruleGroupClient := createRecordingRuleGroupClientSimpleMockWith5XXStatusError(mockCtrl) + mockClientSet := mock_clientset.NewMockClientSetInterface(mockCtrl) + mockClientSet.EXPECT().RuleGroups().Return(ruleGroupClient).AnyTimes() + + scheme := runtime.NewScheme() + utilruntime.Must(coralogixv1alpha1.AddToScheme(scheme)) + mgr, _ := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: "0", + }) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go mgr.GetCache().Start(ctx) + mgr.GetCache().WaitForCacheSync(ctx) + withWatch, err := client.NewWithWatch(mgr.GetConfig(), client.Options{ + Scheme: mgr.GetScheme(), + }) + assert.NoError(t, err) + r := RuleGroupReconciler{ + Client: withWatch, + Scheme: mgr.GetScheme(), + CoralogixClientSet: mockClientSet, + } + r.SetupWithManager(mgr) + + watcher, _ := r.Client.(client.WithWatch).Watch(ctx, &coralogixv1alpha1.RuleGroupList{}) + ctrl.SetLogger(zap.New(zap.UseDevMode(true))) + + err = r.Client.Create(ctx, expectedRuleGroupCRD()) + assert.NoError(t, err) + <-watcher.ResultChan() + + result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) + assert.Error(t, err) + assert.Equal(t, defaultErrRequeuePeriod, result.RequeueAfter) + + result, err = r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) + assert.NoError(t, err) + assert.Equal(t, defaultRequeuePeriod, result.RequeueAfter) + + namespacedName := types.NamespacedName{Namespace: "default", Name: "test"} + actualRuleGroupCRD := &coralogixv1alpha1.RuleGroup{} + err = r.Client.Get(ctx, namespacedName, actualRuleGroupCRD) + assert.NoError(t, err) + + id := actualRuleGroupCRD.Status.ID + if !assert.NotNil(t, id) { + return + } + getRuleGroupRequest := &rulesgroups.GetRuleGroupRequest{GroupId: *id} + actualRuleGroup, err := r.CoralogixClientSet.RuleGroups().GetRuleGroup(ctx, getRuleGroupRequest) + assert.NoError(t, err) + assert.EqualValues(t, ruleGroupBackendSchema, actualRuleGroup.GetRuleGroup()) + + err = r.Client.Delete(ctx, actualRuleGroupCRD) + <-watcher.ResultChan() + r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) +} + +// createRuleGroupClientSimpleMock creates a simple mock for RuleGroupsClientInterface which returns a single rule group. +func createRuleGroupClientSimpleMock(mockCtrl *gomock.Controller) clientset.RuleGroupsClientInterface { + mockRuleGroupsClient := mock_clientset.NewMockRuleGroupsClientInterface(mockCtrl) + + var ruleGroupExist bool + + mockRuleGroupsClient.EXPECT(). + CreateRuleGroup(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, _ *rulesgroups.CreateRuleGroupRequest) (*rulesgroups.CreateRuleGroupResponse, error) { + ruleGroupExist = true + return &rulesgroups.CreateRuleGroupResponse{RuleGroup: ruleGroupBackendSchema}, nil + }).AnyTimes() + + mockRuleGroupsClient.EXPECT(). + GetRuleGroup(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req *rulesgroups.GetRuleGroupRequest) (*rulesgroups.GetRuleGroupResponse, error) { + if ruleGroupExist { + return &rulesgroups.GetRuleGroupResponse{RuleGroup: ruleGroupBackendSchema}, nil + } + return nil, errors.NewNotFound(schema.GroupResource{}, "id1") + }).AnyTimes() + + mockRuleGroupsClient.EXPECT(). + DeleteRuleGroup(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, req *rulesgroups.DeleteRuleGroupRequest) (*rulesgroups.DeleteRuleGroupResponse, error) { + if ruleGroupExist { + ruleGroupExist = false + return &rulesgroups.DeleteRuleGroupResponse{}, nil + } + return nil, errors.NewNotFound(schema.GroupResource{}, "id1") + }).AnyTimes() + + return mockRuleGroupsClient } -func int32Ptr(i int32) *int32 { return &i } +// createRecordingRuleGroupClientSimpleMockWith5XXStatusError creates a simple mock for RecordingRuleGroupsClientInterface which first returns 5xx status error, then returns a single recording rule group. +func createRecordingRuleGroupClientSimpleMockWith5XXStatusError(mockCtrl *gomock.Controller) clientset.RuleGroupsClientInterface { + mockRuleGroupsClient := mock_clientset.NewMockRuleGroupsClientInterface(mockCtrl) + + var ruleGroupExist, wasCalled bool + + mockRuleGroupsClient.EXPECT(). + CreateRuleGroup(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, _ *rulesgroups.CreateRuleGroupRequest) (*rulesgroups.CreateRuleGroupResponse, error) { + if !wasCalled { + wasCalled = true + return nil, errors.NewInternalError(fmt.Errorf("internal error")) + } + ruleGroupExist = true + return &rulesgroups.CreateRuleGroupResponse{RuleGroup: ruleGroupBackendSchema}, nil + }).AnyTimes() + + mockRuleGroupsClient.EXPECT(). + GetRuleGroup(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req *rulesgroups.GetRuleGroupRequest) (*rulesgroups.GetRuleGroupResponse, error) { + if ruleGroupExist { + return &rulesgroups.GetRuleGroupResponse{RuleGroup: ruleGroupBackendSchema}, nil + } + return nil, errors.NewNotFound(schema.GroupResource{}, "id1") + }).AnyTimes() + + mockRuleGroupsClient.EXPECT(). + DeleteRuleGroup(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, req *rulesgroups.DeleteRuleGroupRequest) (*rulesgroups.DeleteRuleGroupResponse, error) { + if ruleGroupExist { + ruleGroupExist = false + return &rulesgroups.DeleteRuleGroupResponse{}, nil + } + return nil, errors.NewNotFound(schema.GroupResource{}, "id1") + }).AnyTimes() + + return mockRuleGroupsClient +} diff --git a/controllers/clientset/recording-rules-groups-client.go b/controllers/clientset/recording-rules-groups-client.go index d6fd4dd..fec13c5 100644 --- a/controllers/clientset/recording-rules-groups-client.go +++ b/controllers/clientset/recording-rules-groups-client.go @@ -7,6 +7,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" ) +//go:generate mockgen -destination=../mock_clientset/mock_recordingrulesgroups-client.go -package=mock_clientset github.com/coralogix/coralogix-operator/controllers/clientset RecordingRulesGroupsClientInterface type RecordingRulesGroupsClientInterface interface { CreateRecordingRuleGroupSet(ctx context.Context, req *rrg.CreateRuleGroupSet) (*rrg.CreateRuleGroupSetResult, error) GetRecordingRuleGroupSet(ctx context.Context, req *rrg.FetchRuleGroupSet) (*rrg.OutRuleGroupSet, error) diff --git a/controllers/clientset/rules-groups-client.go b/controllers/clientset/rules-groups-client.go index fea2a50..72192b2 100644 --- a/controllers/clientset/rules-groups-client.go +++ b/controllers/clientset/rules-groups-client.go @@ -6,6 +6,7 @@ import ( rulesgroups "github.com/coralogix/coralogix-operator/controllers/clientset/grpc/rules-groups/v1" ) +//go:generate mockgen -destination=../mock_clientset/mock_rulegroups-client.go -package=mock_clientset github.com/coralogix/coralogix-operator/controllers/clientset RuleGroupsClientInterface type RuleGroupsClientInterface interface { CreateRuleGroup(ctx context.Context, req *rulesgroups.CreateRuleGroupRequest) (*rulesgroups.CreateRuleGroupResponse, error) GetRuleGroup(ctx context.Context, req *rulesgroups.GetRuleGroupRequest) (*rulesgroups.GetRuleGroupResponse, error) diff --git a/controllers/mock_clientset/mock_recordingrulesgroups-client.go b/controllers/mock_clientset/mock_recordingrulesgroups-client.go new file mode 100644 index 0000000..7ab771b --- /dev/null +++ b/controllers/mock_clientset/mock_recordingrulesgroups-client.go @@ -0,0 +1,101 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/coralogix/coralogix-operator/controllers/clientset (interfaces: RecordingRulesGroupsClientInterface) +// +// Generated by this command: +// +// mockgen -destination=../mock_clientset/mock_recordingrulesgroups-client.go -package=mock_clientset github.com/coralogix/coralogix-operator/controllers/clientset RecordingRulesGroupsClientInterface +// +// Package mock_clientset is a generated GoMock package. +package mock_clientset + +import ( + context "context" + reflect "reflect" + + __ "github.com/coralogix/coralogix-operator/controllers/clientset/grpc/recording-rules-groups/v2" + gomock "go.uber.org/mock/gomock" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// MockRecordingRulesGroupsClientInterface is a mock of RecordingRulesGroupsClientInterface interface. +type MockRecordingRulesGroupsClientInterface struct { + ctrl *gomock.Controller + recorder *MockRecordingRulesGroupsClientInterfaceMockRecorder +} + +// MockRecordingRulesGroupsClientInterfaceMockRecorder is the mock recorder for MockRecordingRulesGroupsClientInterface. +type MockRecordingRulesGroupsClientInterfaceMockRecorder struct { + mock *MockRecordingRulesGroupsClientInterface +} + +// NewMockRecordingRulesGroupsClientInterface creates a new mock instance. +func NewMockRecordingRulesGroupsClientInterface(ctrl *gomock.Controller) *MockRecordingRulesGroupsClientInterface { + mock := &MockRecordingRulesGroupsClientInterface{ctrl: ctrl} + mock.recorder = &MockRecordingRulesGroupsClientInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRecordingRulesGroupsClientInterface) EXPECT() *MockRecordingRulesGroupsClientInterfaceMockRecorder { + return m.recorder +} + +// CreateRecordingRuleGroupSet mocks base method. +func (m *MockRecordingRulesGroupsClientInterface) CreateRecordingRuleGroupSet(arg0 context.Context, arg1 *__.CreateRuleGroupSet) (*__.CreateRuleGroupSetResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateRecordingRuleGroupSet", arg0, arg1) + ret0, _ := ret[0].(*__.CreateRuleGroupSetResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRecordingRuleGroupSet indicates an expected call of CreateRecordingRuleGroupSet. +func (mr *MockRecordingRulesGroupsClientInterfaceMockRecorder) CreateRecordingRuleGroupSet(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRecordingRuleGroupSet", reflect.TypeOf((*MockRecordingRulesGroupsClientInterface)(nil).CreateRecordingRuleGroupSet), arg0, arg1) +} + +// DeleteRecordingRuleGroupSet mocks base method. +func (m *MockRecordingRulesGroupsClientInterface) DeleteRecordingRuleGroupSet(arg0 context.Context, arg1 *__.DeleteRuleGroupSet) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteRecordingRuleGroupSet", arg0, arg1) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteRecordingRuleGroupSet indicates an expected call of DeleteRecordingRuleGroupSet. +func (mr *MockRecordingRulesGroupsClientInterfaceMockRecorder) DeleteRecordingRuleGroupSet(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRecordingRuleGroupSet", reflect.TypeOf((*MockRecordingRulesGroupsClientInterface)(nil).DeleteRecordingRuleGroupSet), arg0, arg1) +} + +// GetRecordingRuleGroupSet mocks base method. +func (m *MockRecordingRulesGroupsClientInterface) GetRecordingRuleGroupSet(arg0 context.Context, arg1 *__.FetchRuleGroupSet) (*__.OutRuleGroupSet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRecordingRuleGroupSet", arg0, arg1) + ret0, _ := ret[0].(*__.OutRuleGroupSet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRecordingRuleGroupSet indicates an expected call of GetRecordingRuleGroupSet. +func (mr *MockRecordingRulesGroupsClientInterfaceMockRecorder) GetRecordingRuleGroupSet(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRecordingRuleGroupSet", reflect.TypeOf((*MockRecordingRulesGroupsClientInterface)(nil).GetRecordingRuleGroupSet), arg0, arg1) +} + +// UpdateRecordingRuleGroupSet mocks base method. +func (m *MockRecordingRulesGroupsClientInterface) UpdateRecordingRuleGroupSet(arg0 context.Context, arg1 *__.UpdateRuleGroupSet) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateRecordingRuleGroupSet", arg0, arg1) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateRecordingRuleGroupSet indicates an expected call of UpdateRecordingRuleGroupSet. +func (mr *MockRecordingRulesGroupsClientInterfaceMockRecorder) UpdateRecordingRuleGroupSet(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRecordingRuleGroupSet", reflect.TypeOf((*MockRecordingRulesGroupsClientInterface)(nil).UpdateRecordingRuleGroupSet), arg0, arg1) +} diff --git a/controllers/mock_clientset/mock_rulegroups-client.go b/controllers/mock_clientset/mock_rulegroups-client.go new file mode 100644 index 0000000..1723ced --- /dev/null +++ b/controllers/mock_clientset/mock_rulegroups-client.go @@ -0,0 +1,100 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/coralogix/coralogix-operator/controllers/clientset (interfaces: RuleGroupsClientInterface) +// +// Generated by this command: +// +// mockgen -destination=../mock_clientset/mock_rulegroups-client.go -package=mock_clientset github.com/coralogix/coralogix-operator/controllers/clientset RuleGroupsClientInterface +// +// Package mock_clientset is a generated GoMock package. +package mock_clientset + +import ( + context "context" + reflect "reflect" + + __ "github.com/coralogix/coralogix-operator/controllers/clientset/grpc/rules-groups/v1" + gomock "go.uber.org/mock/gomock" +) + +// MockRuleGroupsClientInterface is a mock of RuleGroupsClientInterface interface. +type MockRuleGroupsClientInterface struct { + ctrl *gomock.Controller + recorder *MockRuleGroupsClientInterfaceMockRecorder +} + +// MockRuleGroupsClientInterfaceMockRecorder is the mock recorder for MockRuleGroupsClientInterface. +type MockRuleGroupsClientInterfaceMockRecorder struct { + mock *MockRuleGroupsClientInterface +} + +// NewMockRuleGroupsClientInterface creates a new mock instance. +func NewMockRuleGroupsClientInterface(ctrl *gomock.Controller) *MockRuleGroupsClientInterface { + mock := &MockRuleGroupsClientInterface{ctrl: ctrl} + mock.recorder = &MockRuleGroupsClientInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRuleGroupsClientInterface) EXPECT() *MockRuleGroupsClientInterfaceMockRecorder { + return m.recorder +} + +// CreateRuleGroup mocks base method. +func (m *MockRuleGroupsClientInterface) CreateRuleGroup(arg0 context.Context, arg1 *__.CreateRuleGroupRequest) (*__.CreateRuleGroupResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateRuleGroup", arg0, arg1) + ret0, _ := ret[0].(*__.CreateRuleGroupResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRuleGroup indicates an expected call of CreateRuleGroup. +func (mr *MockRuleGroupsClientInterfaceMockRecorder) CreateRuleGroup(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRuleGroup", reflect.TypeOf((*MockRuleGroupsClientInterface)(nil).CreateRuleGroup), arg0, arg1) +} + +// DeleteRuleGroup mocks base method. +func (m *MockRuleGroupsClientInterface) DeleteRuleGroup(arg0 context.Context, arg1 *__.DeleteRuleGroupRequest) (*__.DeleteRuleGroupResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteRuleGroup", arg0, arg1) + ret0, _ := ret[0].(*__.DeleteRuleGroupResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteRuleGroup indicates an expected call of DeleteRuleGroup. +func (mr *MockRuleGroupsClientInterfaceMockRecorder) DeleteRuleGroup(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRuleGroup", reflect.TypeOf((*MockRuleGroupsClientInterface)(nil).DeleteRuleGroup), arg0, arg1) +} + +// GetRuleGroup mocks base method. +func (m *MockRuleGroupsClientInterface) GetRuleGroup(arg0 context.Context, arg1 *__.GetRuleGroupRequest) (*__.GetRuleGroupResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRuleGroup", arg0, arg1) + ret0, _ := ret[0].(*__.GetRuleGroupResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRuleGroup indicates an expected call of GetRuleGroup. +func (mr *MockRuleGroupsClientInterfaceMockRecorder) GetRuleGroup(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRuleGroup", reflect.TypeOf((*MockRuleGroupsClientInterface)(nil).GetRuleGroup), arg0, arg1) +} + +// UpdateRuleGroup mocks base method. +func (m *MockRuleGroupsClientInterface) UpdateRuleGroup(arg0 context.Context, arg1 *__.UpdateRuleGroupRequest) (*__.UpdateRuleGroupResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateRuleGroup", arg0, arg1) + ret0, _ := ret[0].(*__.UpdateRuleGroupResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateRuleGroup indicates an expected call of UpdateRuleGroup. +func (mr *MockRuleGroupsClientInterfaceMockRecorder) UpdateRuleGroup(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRuleGroup", reflect.TypeOf((*MockRuleGroupsClientInterface)(nil).UpdateRuleGroup), arg0, arg1) +} diff --git a/controllers/prometheusrule_controller_test.go b/controllers/prometheusrule_controller_test.go new file mode 100644 index 0000000..b89afb3 --- /dev/null +++ b/controllers/prometheusrule_controller_test.go @@ -0,0 +1,326 @@ +package controllers + +import ( + "context" + "fmt" + "log" + "testing" + + coralogixv1alpha1 "github.com/coralogix/coralogix-operator/apis/coralogix/v1alpha1" + "github.com/coralogix/coralogix-operator/controllers/alphacontrollers" + alerts "github.com/coralogix/coralogix-operator/controllers/clientset/grpc/alerts/v2" + rrg "github.com/coralogix/coralogix-operator/controllers/clientset/grpc/recording-rules-groups/v2" + "github.com/coralogix/coralogix-operator/controllers/mock_clientset" + prometheus "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "golang.org/x/exp/rand" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/wrapperspb" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/watch" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +func TestPrometheusRuleReconciler_Reconcile(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockAlertsClient := createAlertsClientMock(mockCtrl) + mockRecordingRuleGroupsClient := createRecordingRuleGroupClientMock(mockCtrl) + mockWebhooksClient := mock_clientset.NewMockWebhooksClientInterface(mockCtrl) + mockClientSet := mock_clientset.NewMockClientSetInterface(mockCtrl) + mockClientSet.EXPECT().Alerts().Return(mockAlertsClient).AnyTimes() + mockClientSet.EXPECT().RecordingRuleGroups().Return(mockRecordingRuleGroupsClient).AnyTimes() + mockClientSet.EXPECT().Webhooks().Return(mockWebhooksClient).AnyTimes() + ctrl.SetLogger(zap.New(zap.UseDevMode(true))) + + scheme := runtime.NewScheme() + utilruntime.Must(coralogixv1alpha1.AddToScheme(scheme)) + utilruntime.Must(prometheus.AddToScheme(scheme)) + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + mgr, _ := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: "0", + }) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go mgr.GetCache().Start(ctx) + mgr.GetCache().WaitForCacheSync(ctx) + withWatch, err := client.NewWithWatch(mgr.GetConfig(), client.Options{ + Scheme: mgr.GetScheme(), + HTTPClient: mgr.GetHTTPClient(), + Mapper: mgr.GetRESTMapper(), + Cache: &client.CacheOptions{Reader: mgr.GetCache()}, + }) + + alertReconciler := alphacontrollers.AlertReconciler{ + Client: withWatch, + Scheme: mgr.GetScheme(), + CoralogixClientSet: mockClientSet, + } + alertReconciler.SetupWithManager(mgr) + recordingRuleGroupReconciler := alphacontrollers.RecordingRuleGroupSetReconciler{ + Client: withWatch, + Scheme: mgr.GetScheme(), + CoralogixClientSet: mockClientSet, + } + recordingRuleGroupReconciler.SetupWithManager(mgr) + prometheusRuleReconciler := PrometheusRuleReconciler{ + Client: withWatch, + Scheme: mgr.GetScheme(), + CoralogixClientSet: mockClientSet, + } + prometheusRuleReconciler.SetupWithManager(mgr) + + alertsWatcher, _ := alertReconciler.Client.(client.WithWatch).Watch(ctx, &coralogixv1alpha1.AlertList{}) + recordingRuleGroupWatcher, _ := recordingRuleGroupReconciler.Client.(client.WithWatch).Watch(ctx, &coralogixv1alpha1.RecordingRuleGroupSetList{}) + prometheusRulesWatcher, _ := prometheusRuleReconciler.Client.(client.WithWatch).Watch(ctx, &prometheus.PrometheusRuleList{}) + err = prometheusRuleReconciler.Client.Create(ctx, expectedPrometheusRuleCRD()) + assert.NoError(t, err) + <-prometheusRulesWatcher.ResultChan() + prometheusRuleCrd := &prometheus.PrometheusRule{} + prometheusRuleReconciler.Client.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test"}, prometheusRuleCrd) + result, err := prometheusRuleReconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) + assert.NoError(t, err) + assert.Equal(t, defaultRequeuePeriod, result.RequeueAfter) + + <-recordingRuleGroupWatcher.ResultChan() + result, err = recordingRuleGroupReconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) + assert.NoError(t, err) + assert.Equal(t, defaultRequeuePeriod, result.RequeueAfter) + actualRecordingRuleGroupSetCRD := &coralogixv1alpha1.RecordingRuleGroupSet{} + err = recordingRuleGroupReconciler.Client.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test"}, actualRecordingRuleGroupSetCRD) + assert.NoError(t, err) + + <-alertsWatcher.ResultChan() + <-alertsWatcher.ResultChan() + _, err = alertReconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test-app-latency-0"}}) + assert.NoError(t, err) + _, err = alertReconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test-app-latency-1"}}) + assert.NoError(t, err) + + actualAlertCRD1 := &coralogixv1alpha1.Alert{} + err = alertReconciler.Client.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test-app-latency-0"}, actualAlertCRD1) + assert.NoError(t, err) + + actualAlertCRD2 := &coralogixv1alpha1.Alert{} + err = alertReconciler.Client.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test-app-latency-1"}, actualAlertCRD2) + assert.NoError(t, err) + + err = prometheusRuleReconciler.Client.Delete(ctx, prometheusRuleCrd) + assert.NoError(t, err) + <-prometheusRulesWatcher.ResultChan() + result, err = prometheusRuleReconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) + assert.NoError(t, err) + + assert.Equal(t, defaultRequeuePeriod, result.RequeueAfter) + err = prometheusRuleReconciler.Client.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test"}, prometheusRuleCrd) + assert.Error(t, err) + + for event := <-recordingRuleGroupWatcher.ResultChan(); event.Type != watch.Deleted; event = <-recordingRuleGroupWatcher.ResultChan() { + result, err = recordingRuleGroupReconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test"}}) + assert.NoError(t, err) + } + + rrg, err := recordingRuleGroupReconciler.CoralogixClientSet.RecordingRuleGroups().GetRecordingRuleGroupSet(ctx, &rrg.FetchRuleGroupSet{Id: *actualRecordingRuleGroupSetCRD.Status.ID}) + assert.Error(t, err) + assert.Nil(t, rrg) + + for deletedAlertsEvents := 0; deletedAlertsEvents < 2; { + event := <-alertsWatcher.ResultChan() + result, err = alertReconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: event.Object.(*coralogixv1alpha1.Alert).Name}}) + if event.Type == watch.Deleted { + deletedAlertsEvents++ + } + log.Print(fmt.Sprintf("#%v", event)) + } + + alert, err := alertReconciler.CoralogixClientSet.Alerts().GetAlert(ctx, &alerts.GetAlertByUniqueIdRequest{Id: wrapperspb.String(*actualAlertCRD1.Status.ID)}) + assert.Error(t, err) + assert.Nil(t, alert) + alert, err = alertReconciler.CoralogixClientSet.Alerts().GetAlert(ctx, &alerts.GetAlertByUniqueIdRequest{Id: wrapperspb.String(*actualAlertCRD2.Status.ID)}) + assert.Error(t, err) + assert.Nil(t, alert) +} + +func expectedPrometheusRuleCRD() *prometheus.PrometheusRule { + return &prometheus.PrometheusRule{ + ObjectMeta: ctrl.ObjectMeta{ + Name: "test", + Namespace: "default", + Labels: map[string]string{ + "app.coralogix.com/track-recording-rules": "true", + "app.coralogix.com/track-alerting-rules": "true", + }, + }, + Spec: prometheus.PrometheusRuleSpec{ + Groups: []prometheus.RuleGroup{ + { + Name: "test_1", + Interval: "60s", + Rules: []prometheus.Rule{ + { + Record: "ExampleRecord", + Expr: intstr.FromString("vector(1)"), + }, + { + Record: "ExampleRecord2", + Expr: intstr.FromString("vector(2)"), + }, + { + Alert: "app-latency", + Expr: intstr.FromString("histogram_quantile(0.99, sum(irate(istio_request_duration_seconds_bucket{reporter=\"source\",destination_service=~\"ingress-annotation-test-svc.example-app.svc.cluster.local\"}[1m])) by (le, destination_workload)) > 0.2"), + For: "5m", + Annotations: map[string]string{ + "cxMinNonNullValuesPercentage": "20", + }, + }, + }, + }, + { + Name: "test_2", + Interval: "70s", + Rules: []prometheus.Rule{ + { + Record: "ExampleRecord", + Expr: intstr.FromString("vector(3)"), + }, + { + Record: "ExampleRecord", + Expr: intstr.FromString("vector(4)"), + }, + { + Alert: "app-latency", + Expr: intstr.FromString("histogram_quantile(0.99, sum(irate(istio_request_duration_seconds_bucket{reporter=\"source\",destination_service=~\"ingress-annotation-test-svc.example-app.svc.cluster.local\"}[1m])) by (le, destination_workload)) > 0.2"), + For: "5m", + Annotations: map[string]string{ + "cxMinNonNullValuesPercentage": "20", + }, + }, + }, + }, + }, + }, + } +} + +func createAlertsClientMock(mockCtrl *gomock.Controller) *mock_clientset.MockAlertsClientInterface { + mockAlertsClient := mock_clientset.NewMockAlertsClientInterface(mockCtrl) + alertsMap := make(map[string]*alerts.Alert) + + mockAlertsClient.EXPECT(). + CreateAlert(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, req *alerts.CreateAlertRequest) (*alerts.CreateAlertResponse, error) { + alert := flattenAlertCreateRequest(req) + alertsMap[alert.UniqueIdentifier.GetValue()] = alert + return &alerts.CreateAlertResponse{Alert: alert}, nil + }).AnyTimes() + + mockAlertsClient.EXPECT(). + GetAlert(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, req *alerts.GetAlertByUniqueIdRequest) (*alerts.GetAlertByUniqueIdResponse, error) { + if alert, ok := alertsMap[req.GetId().GetValue()]; ok { + return &alerts.GetAlertByUniqueIdResponse{Alert: alert}, nil + } + return nil, errors.NewNotFound(schema.GroupResource{}, req.GetId().GetValue()) + }).AnyTimes() + + mockAlertsClient.EXPECT(). + DeleteAlert(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, req *alerts.DeleteAlertByUniqueIdRequest) (*alerts.DeleteAlertByUniqueIdResponse, error) { + if _, ok := alertsMap[req.GetId().GetValue()]; ok { + delete(alertsMap, req.GetId().GetValue()) + return &alerts.DeleteAlertByUniqueIdResponse{}, nil + } + return nil, errors.NewNotFound(schema.GroupResource{}, req.GetId().GetValue()) + }).AnyTimes() + + return mockAlertsClient +} + +func flattenAlertCreateRequest(req *alerts.CreateAlertRequest) *alerts.Alert { + return &alerts.Alert{ + Name: req.Name, + Description: req.Description, + IsActive: req.IsActive, + Severity: req.Severity, + Expiration: req.Expiration, + Condition: req.Condition, + ShowInInsight: req.ShowInInsight, + NotificationGroups: req.NotificationGroups, + Filters: req.Filters, + ActiveWhen: req.ActiveWhen, + NotificationPayloadFilters: req.NotificationPayloadFilters, + MetaLabels: req.MetaLabels, + MetaLabelsStrings: req.MetaLabelsStrings, + TracingAlert: req.TracingAlert, + UniqueIdentifier: wrapperspb.String(randomId()), + } +} + +func randomId() string { + return fmt.Sprintf("%d", rand.Int()) +} + +func createRecordingRuleGroupClientMock(mockCtrl *gomock.Controller) *mock_clientset.MockRecordingRulesGroupsClientInterface { + mockRecordingRuleGroupsClient := mock_clientset.NewMockRecordingRulesGroupsClientInterface(mockCtrl) + recordingRuleGroupsMap := make(map[string]*rrg.OutRuleGroupSet) + + mockRecordingRuleGroupsClient.EXPECT(). + CreateRecordingRuleGroupSet(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, req *rrg.CreateRuleGroupSet) (*rrg.CreateRuleGroupSetResult, error) { + id := randomId() + recordingRuleGroupsMap[id] = &rrg.OutRuleGroupSet{ + Id: id, + Groups: inToOutRecordingRuleGroup(req.Groups), + } + return &rrg.CreateRuleGroupSetResult{Id: id}, nil + }).AnyTimes() + + mockRecordingRuleGroupsClient.EXPECT(). + GetRecordingRuleGroupSet(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req *rrg.FetchRuleGroupSet) (*rrg.OutRuleGroupSet, error) { + if recordingRuleGroup, ok := recordingRuleGroupsMap[req.GetId()]; ok { + return recordingRuleGroup, nil + } + return nil, errors.NewNotFound(schema.GroupResource{}, req.GetId()) + }).AnyTimes() + + mockRecordingRuleGroupsClient.EXPECT(). + DeleteRecordingRuleGroupSet(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, req *rrg.DeleteRuleGroupSet) (*emptypb.Empty, error) { + if _, ok := recordingRuleGroupsMap[req.GetId()]; ok { + delete(recordingRuleGroupsMap, req.GetId()) + return &emptypb.Empty{}, nil + } + return nil, errors.NewNotFound(schema.GroupResource{}, req.GetId()) + }).AnyTimes() + + return mockRecordingRuleGroupsClient +} + +func inToOutRecordingRuleGroup(in []*rrg.InRuleGroup) []*rrg.OutRuleGroup { + out := make([]*rrg.OutRuleGroup, 0, len(in)) + for _, group := range in { + out = append(out, &rrg.OutRuleGroup{ + Name: group.Name, + Interval: group.Interval, + Limit: group.Limit, + Rules: inToOutRecordingRule(group.Rules), + }) + } + return out +} + +func inToOutRecordingRule(in []*rrg.InRule) []*rrg.OutRule { + out := make([]*rrg.OutRule, len(in)) + for i, rule := range in { + out[i] = &rrg.OutRule{ + Record: rule.Record, + Expr: rule.Expr, + Labels: rule.Labels, + } + } + return out +} diff --git a/go.mod b/go.mod index 7bd29d9..d0eef23 100644 --- a/go.mod +++ b/go.mod @@ -58,13 +58,14 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/net v0.15.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/net v0.16.0 // indirect golang.org/x/oauth2 v0.6.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/term v0.12.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.13.0 // indirect + golang.org/x/tools v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect diff --git a/go.sum b/go.sum index 02b19a4..d05fce4 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,7 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -106,8 +107,10 @@ github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -131,6 +134,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= @@ -199,6 +203,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -207,6 +213,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -222,6 +229,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= @@ -246,9 +255,13 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -269,6 +282,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=