From 287bdab77747fa03ea6a7bbeb10900956db7bee7 Mon Sep 17 00:00:00 2001 From: Richard Hagen Date: Mon, 28 Oct 2024 13:12:00 +0100 Subject: [PATCH 1/3] Update status badge (#696) * feat: Update status badge with equinor colors * chore: cleanup * fix: tests * fix: add colors back --- api/buildstatus/buildstatus_controller.go | 2 + .../models/badges/build-status.svg | 59 +++++++++--------- api/buildstatus/models/buildstatus.go | 62 +++++++++---------- api/buildstatus/models/buildstatus_test.go | 20 +++--- 4 files changed, 71 insertions(+), 72 deletions(-) diff --git a/api/buildstatus/buildstatus_controller.go b/api/buildstatus/buildstatus_controller.go index 31136fc1..9be76a7e 100644 --- a/api/buildstatus/buildstatus_controller.go +++ b/api/buildstatus/buildstatus_controller.go @@ -9,6 +9,7 @@ import ( "github.com/equinor/radix-api/models" radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" "github.com/gorilla/mux" + "github.com/rs/zerolog/log" ) const rootPath = "/applications/{appName}/environments/{envName}" @@ -77,6 +78,7 @@ func (bsc *buildStatusController) GetBuildStatus(accounts models.Accounts, w htt buildStatus, err := buildStatusHandler.GetBuildStatusForApplication(r.Context(), appName, env, pipeline) if err != nil { + log.Error().Err(err).Msg("Error getting build status") w.WriteHeader(http.StatusInternalServerError) return } diff --git a/api/buildstatus/models/badges/build-status.svg b/api/buildstatus/models/badges/build-status.svg index f4d1d69f..f848aa43 100644 --- a/api/buildstatus/models/badges/build-status.svg +++ b/api/buildstatus/models/badges/build-status.svg @@ -1,19 +1,16 @@ - - - - - - - - - - - {{.Operation}} - - - {{.Status}} - - + + {{.Operation}} + + + + + + + + + + + @@ -29,17 +26,21 @@ - - - - - - - - - - - - + + + + + + + {{.Operation}} + + + + {{.Status}} + + + - \ No newline at end of file + diff --git a/api/buildstatus/models/buildstatus.go b/api/buildstatus/models/buildstatus.go index 36ed3a43..4d480571 100644 --- a/api/buildstatus/models/buildstatus.go +++ b/api/buildstatus/models/buildstatus.go @@ -8,7 +8,6 @@ import ( "strings" v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" - "github.com/marstr/guid" ) // embed https://golang.org/pkg/embed/ - For embedding a single file, a variable of type []byte or string is often best @@ -17,16 +16,16 @@ import ( var defaultBadgeTemplate string const ( - buildStatusFailing = "failing" - buildStatusSuccess = "success" - buildStatusStopped = "stopped" - buildStatusPending = "pending" - buildStatusRunning = "running" - buildStatusUnknown = "unknown" + buildStatusFailing = "Failing" + buildStatusSuccess = "Succeeded" + buildStatusStopped = "Stopped" + buildStatusPending = "Pending" + buildStatusRunning = "Running" + buildStatusUnknown = "Unknown" ) const ( - pipelineStatusSuccessColor = "#4c1" + pipelineStatusSuccessColor = "#34d058" pipelineStatusFailedColor = "#e05d44" pipelineStatusStoppedColor = "#e05d44" pipelineStatusRunningColor = "#33cccc" @@ -44,17 +43,11 @@ func NewPipelineBadge() PipelineBadge { } type pipelineBadgeData struct { - Operation string - Status string - ColorLeft string - ColorRight string - ColorShadow string - ColorFont string - Width int - Height int - StatusOffset int - OperationTextId string - StatusTextId string + Operation string + OperationWidth int + Status string + StatusColor string + StatusWidth int } type pipelineBadge struct { @@ -70,23 +63,17 @@ func (rbs *pipelineBadge) getBadge(condition v1.RadixJobCondition, pipeline v1.R status := translateCondition(condition) color := getColor(condition) operationWidth := calculateWidth(10, operation) - statusWidth := calculateWidth(10, status) + 24 - + statusWidth := calculateWidth(10, status) badgeData := pipelineBadgeData{ - Operation: operation, - Status: status, - ColorRight: color, - ColorLeft: "#aaa", - ColorShadow: "#010101", - ColorFont: "#fff", - Width: statusWidth + operationWidth, - Height: 30, - StatusOffset: operationWidth, - OperationTextId: guid.NewGUID().String(), - StatusTextId: guid.NewGUID().String(), + Operation: operation, + OperationWidth: operationWidth, + Status: status, + StatusColor: color, + StatusWidth: statusWidth, } - svgTemplate := template.New("status-badge.svg") + funcMap := template.FuncMap{"sum": TemplateSum} + svgTemplate := template.New("status-badge.svg").Funcs(funcMap) _, err := svgTemplate.Parse(rbs.badgeTemplate) if err != nil { return nil, err @@ -113,6 +100,15 @@ func calculateWidth(charWidth float32, value string) int { return int(width + 5) } +func TemplateSum(arg0 int, args ...int) int { + x := arg0 + for _, arg := range args { + x += arg + } + + return x +} + func translateCondition(condition v1.RadixJobCondition) string { switch condition { case v1.JobSucceeded: diff --git a/api/buildstatus/models/buildstatus_test.go b/api/buildstatus/models/buildstatus_test.go index 1c942775..fc913d1f 100644 --- a/api/buildstatus/models/buildstatus_test.go +++ b/api/buildstatus/models/buildstatus_test.go @@ -13,42 +13,42 @@ func Test_PipelineBadgeBuilder(t *testing.T) { t.Run("failed condition", func(t *testing.T) { t.Parallel() - expected := "-failing" + expected := "-Failing" actual, err := badgeBuilder.GetBadge(v1.JobFailed, v1.RadixPipelineType("")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("queued condition", func(t *testing.T) { t.Parallel() - expected := "-pending" + expected := "-Pending" actual, err := badgeBuilder.GetBadge(v1.JobQueued, v1.RadixPipelineType("")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("running condition", func(t *testing.T) { t.Parallel() - expected := "-running" + expected := "-Running" actual, err := badgeBuilder.GetBadge(v1.JobRunning, v1.RadixPipelineType("")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("stopped condition", func(t *testing.T) { t.Parallel() - expected := "-stopped" + expected := "-Stopped" actual, err := badgeBuilder.GetBadge(v1.JobStopped, v1.RadixPipelineType("")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("succeeded condition", func(t *testing.T) { t.Parallel() - expected := "-success" + expected := "-Succeeded" actual, err := badgeBuilder.GetBadge(v1.JobSucceeded, v1.RadixPipelineType("")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("waiting condition", func(t *testing.T) { t.Parallel() - expected := "-pending" + expected := "-Pending" actual, err := badgeBuilder.GetBadge(v1.JobWaiting, v1.RadixPipelineType("")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) @@ -56,28 +56,28 @@ func Test_PipelineBadgeBuilder(t *testing.T) { t.Run("build-deploy type", func(t *testing.T) { t.Parallel() - expected := "build-deploy-success" + expected := "build-deploy-Succeeded" actual, err := badgeBuilder.GetBadge(v1.JobSucceeded, v1.BuildDeploy) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("promote type", func(t *testing.T) { t.Parallel() - expected := "promote-success" + expected := "promote-Succeeded" actual, err := badgeBuilder.GetBadge(v1.JobSucceeded, v1.Promote) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("deploy type", func(t *testing.T) { t.Parallel() - expected := "deploy-success" + expected := "deploy-Succeeded" actual, err := badgeBuilder.GetBadge(v1.JobSucceeded, v1.Deploy) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("unhandled type", func(t *testing.T) { t.Parallel() - expected := "-success" + expected := "-Succeeded" actual, err := badgeBuilder.GetBadge(v1.JobSucceeded, v1.RadixPipelineType("unhandled")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) From ca999145491a39637807e119d01e8ed88a965c12 Mon Sep 17 00:00:00 2001 From: Richard Hagen Date: Tue, 29 Oct 2024 11:04:46 +0100 Subject: [PATCH 2/3] feat: update style to match github (#697) * feat: update style to match github * fix: tests --- .../build_status_controller_test.go | 45 ++-------- api/buildstatus/buildstatus_handler.go | 2 +- .../models/badges/build-status.svg | 86 +++++++++++-------- api/buildstatus/models/buildstatus.go | 16 ++-- api/buildstatus/models/buildstatus_test.go | 21 ++--- api/test/mock/buildstatus_mock.go | 9 +- 6 files changed, 80 insertions(+), 99 deletions(-) diff --git a/api/buildstatus/build_status_controller_test.go b/api/buildstatus/build_status_controller_test.go index 62c30eb8..8a2556b4 100644 --- a/api/buildstatus/build_status_controller_test.go +++ b/api/buildstatus/build_status_controller_test.go @@ -102,7 +102,7 @@ func TestGetBuildStatus(t *testing.T) { expected := []byte("badge") fakeBuildStatus.EXPECT(). - GetBadge(gomock.Any(), gomock.Any()). + GetBadge(gomock.Any(), gomock.Any(), gomock.Any()). Return(expected, nil). Times(1) @@ -125,16 +125,7 @@ func TestGetBuildStatus(t *testing.T) { fakeBuildStatus := mock.NewMockPipelineBadge(ctrl) - var actualCondition v1.RadixJobCondition - var actualPipeline v1.RadixPipelineType - - fakeBuildStatus.EXPECT(). - GetBadge(gomock.Any(), gomock.Any()). - DoAndReturn(func(c v1.RadixJobCondition, p v1.RadixPipelineType) ([]byte, error) { - actualCondition = c - actualPipeline = p - return nil, nil - }) + fakeBuildStatus.EXPECT().GetBadge(gomock.Any(), v1.JobRunning, v1.BuildDeploy).Return(nil, nil).Times(1) mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) @@ -144,8 +135,6 @@ func TestGetBuildStatus(t *testing.T) { response := <-responseChannel assert.Equal(t, response.Result().StatusCode, 200) - assert.Equal(t, v1.JobRunning, actualCondition) - assert.Equal(t, v1.BuildDeploy, actualPipeline) }) t.Run("deploy in master - JobRunning", func(t *testing.T) { @@ -154,17 +143,7 @@ func TestGetBuildStatus(t *testing.T) { defer ctrl.Finish() fakeBuildStatus := mock.NewMockPipelineBadge(ctrl) - - var actualCondition v1.RadixJobCondition - var actualPipeline v1.RadixPipelineType - - fakeBuildStatus.EXPECT(). - GetBadge(gomock.Any(), gomock.Any()). - DoAndReturn(func(c v1.RadixJobCondition, p v1.RadixPipelineType) ([]byte, error) { - actualCondition = c - actualPipeline = p - return nil, nil - }) + fakeBuildStatus.EXPECT().GetBadge(gomock.Any(), v1.JobSucceeded, v1.Deploy).Return(nil, nil).Times(1) mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) @@ -174,8 +153,6 @@ func TestGetBuildStatus(t *testing.T) { response := <-responseChannel assert.Equal(t, response.Result().StatusCode, 200) - assert.Equal(t, v1.JobSucceeded, actualCondition) - assert.Equal(t, v1.Deploy, actualPipeline) }) t.Run("promote in master - JobFailed", func(t *testing.T) { @@ -184,17 +161,7 @@ func TestGetBuildStatus(t *testing.T) { defer ctrl.Finish() fakeBuildStatus := mock.NewMockPipelineBadge(ctrl) - - var actualCondition v1.RadixJobCondition - var actualPipeline v1.RadixPipelineType - - fakeBuildStatus.EXPECT(). - GetBadge(gomock.Any(), gomock.Any()). - DoAndReturn(func(c v1.RadixJobCondition, p v1.RadixPipelineType) ([]byte, error) { - actualCondition = c - actualPipeline = p - return nil, nil - }) + fakeBuildStatus.EXPECT().GetBadge(gomock.Any(), v1.JobFailed, v1.Promote).Return(nil, nil).Times(1) mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) @@ -204,8 +171,6 @@ func TestGetBuildStatus(t *testing.T) { response := <-responseChannel assert.Equal(t, response.Result().StatusCode, 200) - assert.Equal(t, v1.JobFailed, actualCondition) - assert.Equal(t, v1.Promote, actualPipeline) }) t.Run("return status 500", func(t *testing.T) { @@ -216,7 +181,7 @@ func TestGetBuildStatus(t *testing.T) { fakeBuildStatus := mock.NewMockPipelineBadge(ctrl) fakeBuildStatus.EXPECT(). - GetBadge(gomock.Any(), gomock.Any()). + GetBadge(gomock.Any(), gomock.Any(), gomock.Any()). Return(nil, errors.New("error")). Times(1) diff --git a/api/buildstatus/buildstatus_handler.go b/api/buildstatus/buildstatus_handler.go index a6cdc507..a4f0e7c4 100644 --- a/api/buildstatus/buildstatus_handler.go +++ b/api/buildstatus/buildstatus_handler.go @@ -40,7 +40,7 @@ func (handler BuildStatusHandler) GetBuildStatusForApplication(ctx context.Conte buildCondition = latestPipelineJob.Status.Condition } - output, err = handler.pipelineBadge.GetBadge(buildCondition, v1.RadixPipelineType(pipeline)) + output, err = handler.pipelineBadge.GetBadge(ctx, buildCondition, v1.RadixPipelineType(pipeline)) if err != nil { return nil, err } diff --git a/api/buildstatus/models/badges/build-status.svg b/api/buildstatus/models/badges/build-status.svg index f848aa43..17c7bd27 100644 --- a/api/buildstatus/models/badges/build-status.svg +++ b/api/buildstatus/models/badges/build-status.svg @@ -1,46 +1,56 @@ - - {{.Operation}} + + {{.Operation}}: {{.Status}} + + + + + + + + + + + + + - - - - - - - + - - - - - - - - - - - - - - + + + + {{.Operation}} - - - - - - - {{.Operation}} - - - - {{.Status}} - + + + + + {{.Status}} + - + diff --git a/api/buildstatus/models/buildstatus.go b/api/buildstatus/models/buildstatus.go index 4d480571..a7e5b521 100644 --- a/api/buildstatus/models/buildstatus.go +++ b/api/buildstatus/models/buildstatus.go @@ -2,12 +2,14 @@ package models import ( "bytes" + "context" _ "embed" "errors" "html/template" "strings" v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + "github.com/rs/zerolog/log" ) // embed https://golang.org/pkg/embed/ - For embedding a single file, a variable of type []byte or string is often best @@ -33,7 +35,7 @@ const ( ) type PipelineBadge interface { - GetBadge(condition v1.RadixJobCondition, pipeline v1.RadixPipelineType) ([]byte, error) + GetBadge(ctx context.Context, condition v1.RadixJobCondition, pipeline v1.RadixPipelineType) ([]byte, error) } func NewPipelineBadge() PipelineBadge { @@ -54,16 +56,16 @@ type pipelineBadge struct { badgeTemplate string } -func (rbs *pipelineBadge) GetBadge(condition v1.RadixJobCondition, pipeline v1.RadixPipelineType) ([]byte, error) { - return rbs.getBadge(condition, pipeline) +func (rbs *pipelineBadge) GetBadge(ctx context.Context, condition v1.RadixJobCondition, pipeline v1.RadixPipelineType) ([]byte, error) { + return rbs.getBadge(ctx, condition, pipeline) } -func (rbs *pipelineBadge) getBadge(condition v1.RadixJobCondition, pipeline v1.RadixPipelineType) ([]byte, error) { +func (rbs *pipelineBadge) getBadge(ctx context.Context, condition v1.RadixJobCondition, pipeline v1.RadixPipelineType) ([]byte, error) { operation := translatePipeline(pipeline) status := translateCondition(condition) color := getColor(condition) - operationWidth := calculateWidth(10, operation) - statusWidth := calculateWidth(10, status) + operationWidth := calculateWidth(6, operation) + statusWidth := calculateWidth(6, status) badgeData := pipelineBadgeData{ Operation: operation, OperationWidth: operationWidth, @@ -72,6 +74,8 @@ func (rbs *pipelineBadge) getBadge(condition v1.RadixJobCondition, pipeline v1.R StatusWidth: statusWidth, } + log.Ctx(ctx).Trace().Interface("badge", badgeData).Msg("Rendering badge") + funcMap := template.FuncMap{"sum": TemplateSum} svgTemplate := template.New("status-badge.svg").Funcs(funcMap) _, err := svgTemplate.Parse(rbs.badgeTemplate) diff --git a/api/buildstatus/models/buildstatus_test.go b/api/buildstatus/models/buildstatus_test.go index fc913d1f..e6200b23 100644 --- a/api/buildstatus/models/buildstatus_test.go +++ b/api/buildstatus/models/buildstatus_test.go @@ -1,6 +1,7 @@ package models import ( + "context" "testing" v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" @@ -14,42 +15,42 @@ func Test_PipelineBadgeBuilder(t *testing.T) { t.Run("failed condition", func(t *testing.T) { t.Parallel() expected := "-Failing" - actual, err := badgeBuilder.GetBadge(v1.JobFailed, v1.RadixPipelineType("")) + actual, err := badgeBuilder.GetBadge(context.Background(), v1.JobFailed, v1.RadixPipelineType("")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("queued condition", func(t *testing.T) { t.Parallel() expected := "-Pending" - actual, err := badgeBuilder.GetBadge(v1.JobQueued, v1.RadixPipelineType("")) + actual, err := badgeBuilder.GetBadge(context.Background(), v1.JobQueued, v1.RadixPipelineType("")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("running condition", func(t *testing.T) { t.Parallel() expected := "-Running" - actual, err := badgeBuilder.GetBadge(v1.JobRunning, v1.RadixPipelineType("")) + actual, err := badgeBuilder.GetBadge(context.Background(), v1.JobRunning, v1.RadixPipelineType("")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("stopped condition", func(t *testing.T) { t.Parallel() expected := "-Stopped" - actual, err := badgeBuilder.GetBadge(v1.JobStopped, v1.RadixPipelineType("")) + actual, err := badgeBuilder.GetBadge(context.Background(), v1.JobStopped, v1.RadixPipelineType("")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("succeeded condition", func(t *testing.T) { t.Parallel() expected := "-Succeeded" - actual, err := badgeBuilder.GetBadge(v1.JobSucceeded, v1.RadixPipelineType("")) + actual, err := badgeBuilder.GetBadge(context.Background(), v1.JobSucceeded, v1.RadixPipelineType("")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("waiting condition", func(t *testing.T) { t.Parallel() expected := "-Pending" - actual, err := badgeBuilder.GetBadge(v1.JobWaiting, v1.RadixPipelineType("")) + actual, err := badgeBuilder.GetBadge(context.Background(), v1.JobWaiting, v1.RadixPipelineType("")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) @@ -57,28 +58,28 @@ func Test_PipelineBadgeBuilder(t *testing.T) { t.Run("build-deploy type", func(t *testing.T) { t.Parallel() expected := "build-deploy-Succeeded" - actual, err := badgeBuilder.GetBadge(v1.JobSucceeded, v1.BuildDeploy) + actual, err := badgeBuilder.GetBadge(context.Background(), v1.JobSucceeded, v1.BuildDeploy) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("promote type", func(t *testing.T) { t.Parallel() expected := "promote-Succeeded" - actual, err := badgeBuilder.GetBadge(v1.JobSucceeded, v1.Promote) + actual, err := badgeBuilder.GetBadge(context.Background(), v1.JobSucceeded, v1.Promote) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("deploy type", func(t *testing.T) { t.Parallel() expected := "deploy-Succeeded" - actual, err := badgeBuilder.GetBadge(v1.JobSucceeded, v1.Deploy) + actual, err := badgeBuilder.GetBadge(context.Background(), v1.JobSucceeded, v1.Deploy) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) t.Run("unhandled type", func(t *testing.T) { t.Parallel() expected := "-Succeeded" - actual, err := badgeBuilder.GetBadge(v1.JobSucceeded, v1.RadixPipelineType("unhandled")) + actual, err := badgeBuilder.GetBadge(context.Background(), v1.JobSucceeded, v1.RadixPipelineType("unhandled")) assert.Nil(t, err) assert.Equal(t, expected, string(actual)) }) diff --git a/api/test/mock/buildstatus_mock.go b/api/test/mock/buildstatus_mock.go index 956070fd..25609bc0 100644 --- a/api/test/mock/buildstatus_mock.go +++ b/api/test/mock/buildstatus_mock.go @@ -5,6 +5,7 @@ package mock import ( + context "context" reflect "reflect" v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" @@ -35,16 +36,16 @@ func (m *MockPipelineBadge) EXPECT() *MockPipelineBadgeMockRecorder { } // GetBadge mocks base method. -func (m *MockPipelineBadge) GetBadge(condition v1.RadixJobCondition, pipeline v1.RadixPipelineType) ([]byte, error) { +func (m *MockPipelineBadge) GetBadge(ctx context.Context, condition v1.RadixJobCondition, pipeline v1.RadixPipelineType) ([]byte, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBadge", condition, pipeline) + ret := m.ctrl.Call(m, "GetBadge", ctx, condition, pipeline) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } // GetBadge indicates an expected call of GetBadge. -func (mr *MockPipelineBadgeMockRecorder) GetBadge(condition, pipeline interface{}) *gomock.Call { +func (mr *MockPipelineBadgeMockRecorder) GetBadge(ctx, condition, pipeline interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBadge", reflect.TypeOf((*MockPipelineBadge)(nil).GetBadge), condition, pipeline) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBadge", reflect.TypeOf((*MockPipelineBadge)(nil).GetBadge), ctx, condition, pipeline) } From 45ad500306e108242687426e8110f78a4e38265e Mon Sep 17 00:00:00 2001 From: Sergey Smolnikov Date: Wed, 30 Oct 2024 08:59:37 +0100 Subject: [PATCH 3/3] Show events on components and replicas (#698) * Added component and pod events endpoints * Added get component logic * Added get pod events logic * Improved get pods performance * Extracted event logic to eventHandler. Added ingress events * Added unit-tests * Added unit-tests * Added unit-tests * Fixed unit-tests * Fixed unit-tests * Added unit-tests * Updated ref * Removed outdated env-var * Update api/events/models/ingress_builder.go Co-authored-by: Richard Hagen * Fixed unit-tests * Fixed unit-tests * Fixed unit-tests * Fixed unit-tests --------- Co-authored-by: Richard Hagen --- .../applications_controller_test.go | 4 +- .../build_status_controller_test.go | 4 - api/environments/environment_controller.go | 134 +++- .../environment_controller_test.go | 41 +- api/environments/environment_handler.go | 29 +- api/events/event_handler.go | 260 ++++++-- api/events/event_handler_test.go | 570 ++++++++++++++++-- api/events/mock/event_handler_mock.go | 43 +- api/events/models/event.go | 15 + api/events/models/event_builder.go | 3 +- api/events/models/ingress_builder.go | 59 ++ api/events/models/object_state_builder.go | 15 +- .../environment_configuration_status.go | 2 +- api/utils/predicate/radix.go | 2 +- api/utils/predicate/radix_test.go | 21 +- go.mod | 3 +- go.sum | 6 +- swaggerui/html/swagger.json | 165 +++++ 18 files changed, 1181 insertions(+), 195 deletions(-) create mode 100644 api/events/models/ingress_builder.go diff --git a/api/applications/applications_controller_test.go b/api/applications/applications_controller_test.go index 80ed311c..90838b6b 100644 --- a/api/applications/applications_controller_test.go +++ b/api/applications/applications_controller_test.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" "net/url" - "os" "strings" "testing" "time" @@ -77,7 +76,6 @@ func setupTestWithFactory(t *testing.T, handlerFactory ApplicationHandlerFactory commonTestUtils := commontest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient) err := commonTestUtils.CreateClusterPrerequisites(clusterName, egressIps, subscriptionId) require.NoError(t, err) - _ = os.Setenv(defaults.ActiveClusternameEnvironmentVariable, clusterName) prometheusHandlerMock := createPrometheusHandlerMock(t, radixclient, nil) // controllerTestUtils is used for issuing HTTP request and processing responses @@ -974,6 +972,7 @@ func TestGetApplication_WithEnvironments(t *testing.T) { WithEnvironmentName(anyOrphanedEnvironment)) orphanedRe.Status.Reconciled = metav1.Now() orphanedRe.Status.Orphaned = true + orphanedRe.Status.OrphanedTimestamp = pointers.Ptr(metav1.Now()) _, err = radix.RadixV1().RadixEnvironments().Update(context.Background(), orphanedRe, metav1.UpdateOptions{}) require.NoError(t, err) @@ -1684,7 +1683,6 @@ func TestHandleTriggerPipeline_Promote_JobHasCorrectParameters(t *testing.T) { const ( appName = "an-app" commitId = "475f241c-478b-49da-adfb-3c336aaab8d2" - deploymentName = "a-deployment" fromEnvironment = "origin" toEnvironment = "target" ) diff --git a/api/buildstatus/build_status_controller_test.go b/api/buildstatus/build_status_controller_test.go index 8a2556b4..628b4219 100644 --- a/api/buildstatus/build_status_controller_test.go +++ b/api/buildstatus/build_status_controller_test.go @@ -3,7 +3,6 @@ package buildstatus import ( "errors" "io" - "os" "testing" "time" @@ -15,7 +14,6 @@ import ( certclientfake "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned/fake" controllertest "github.com/equinor/radix-api/api/test" "github.com/equinor/radix-api/api/test/mock" - "github.com/equinor/radix-operator/pkg/apis/defaults" v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" commontest "github.com/equinor/radix-operator/pkg/apis/test" builders "github.com/equinor/radix-operator/pkg/apis/utils" @@ -43,8 +41,6 @@ func setupTest(t *testing.T) (*commontest.Utils, *kubefake.Clientset, *radixfake commonTestUtils := commontest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient) err := commonTestUtils.CreateClusterPrerequisites(clusterName, egressIps, subscriptionId) require.NoError(t, err) - _ = os.Setenv(defaults.ActiveClusternameEnvironmentVariable, clusterName) - return &commonTestUtils, kubeclient, radixclient, kedaClient, secretproviderclient, certClient } diff --git a/api/environments/environment_controller.go b/api/environments/environment_controller.go index 6c0abf53..059b6a4a 100644 --- a/api/environments/environment_controller.go +++ b/api/environments/environment_controller.go @@ -63,6 +63,16 @@ func (c *environmentController) GetRoutes() models.Routes { Method: "GET", HandlerFunc: c.GetEnvironmentEvents, }, + models.Route{ + Path: rootPath + "/environments/{envName}/events/components/{componentName}", + Method: "GET", + HandlerFunc: c.GetComponentEvents, + }, + models.Route{ + Path: rootPath + "/environments/{envName}/events/components/{componentName}/replicas/{podName}", + Method: "GET", + HandlerFunc: c.GetPodEvents, + }, models.Route{ Path: rootPath + "/environments/{envName}/components/{componentName}/stop", Method: "POST", @@ -522,7 +532,66 @@ func (c *environmentController) GetEnvironmentEvents(accounts models.Accounts, w envName := mux.Vars(r)["envName"] environmentHandler := c.environmentHandlerFactory(accounts) - events, err := environmentHandler.GetEnvironmentEvents(r.Context(), appName, envName) + events, err := environmentHandler.eventHandler.GetEnvironmentEvents(r.Context(), appName, envName) + + if err != nil { + c.ErrorResponse(w, r, err) + return + } + + c.JSONResponse(w, r, events) + +} + +// GetComponentEvents Get events for an application environment component +func (c *environmentController) GetComponentEvents(accounts models.Accounts, w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /applications/{appName}/environments/{envName}/events/components/{componentName} environment getComponentEvents + // --- + // summary: Lists events for an application environment + // parameters: + // - name: appName + // in: path + // description: name of Radix application + // type: string + // required: true + // - name: envName + // in: path + // description: name of environment + // type: string + // required: true + // - name: componentName + // in: path + // description: Name of component + // type: string + // required: true + // - name: Impersonate-User + // in: header + // description: Works only with custom setup of cluster. Allow impersonation of test users (Required if Impersonate-Group is set) + // type: string + // required: false + // - name: Impersonate-Group + // in: header + // description: Works only with custom setup of cluster. Allow impersonation of a comma-seperated list of test groups (Required if Impersonate-User is set) + // type: string + // required: false + // responses: + // "200": + // description: "Successful get environment events" + // schema: + // type: "array" + // items: + // "$ref": "#/definitions/Event" + // "401": + // description: "Unauthorized" + // "404": + // description: "Not found" + + appName := mux.Vars(r)["appName"] + envName := mux.Vars(r)["envName"] + componentName := mux.Vars(r)["componentName"] + + environmentHandler := c.environmentHandlerFactory(accounts) + events, err := environmentHandler.eventHandler.GetComponentEvents(r.Context(), appName, envName, componentName) if err != nil { c.ErrorResponse(w, r, err) @@ -530,7 +599,70 @@ func (c *environmentController) GetEnvironmentEvents(accounts models.Accounts, w } c.JSONResponse(w, r, events) +} + +// GetPodEvents Get events for an application environment component +func (c *environmentController) GetPodEvents(accounts models.Accounts, w http.ResponseWriter, r *http.Request) { + // swagger:operation GET /applications/{appName}/environments/{envName}/events/components/{componentName}/replicas/{podName} environment getReplicaEvents + // --- + // summary: Lists events for an application environment + // parameters: + // - name: appName + // in: path + // description: name of Radix application + // type: string + // required: true + // - name: envName + // in: path + // description: name of environment + // type: string + // required: true + // - name: componentName + // in: path + // description: Name of component + // type: string + // required: true + // - name: podName + // in: path + // description: Name of pod + // type: string + // required: true + // - name: Impersonate-User + // in: header + // description: Works only with custom setup of cluster. Allow impersonation of test users (Required if Impersonate-Group is set) + // type: string + // required: false + // - name: Impersonate-Group + // in: header + // description: Works only with custom setup of cluster. Allow impersonation of a comma-seperated list of test groups (Required if Impersonate-User is set) + // type: string + // required: false + // responses: + // "200": + // description: "Successful get environment events" + // schema: + // type: "array" + // items: + // "$ref": "#/definitions/Event" + // "401": + // description: "Unauthorized" + // "404": + // description: "Not found" + + appName := mux.Vars(r)["appName"] + envName := mux.Vars(r)["envName"] + componentName := mux.Vars(r)["componentName"] + podName := mux.Vars(r)["podName"] + environmentHandler := c.environmentHandlerFactory(accounts) + events, err := environmentHandler.eventHandler.GetPodEvents(r.Context(), appName, envName, componentName, podName) + + if err != nil { + c.ErrorResponse(w, r, err) + return + } + + c.JSONResponse(w, r, events) } // StopComponent Stops job diff --git a/api/environments/environment_controller_test.go b/api/environments/environment_controller_test.go index 87ff7c8a..69bfb08b 100644 --- a/api/environments/environment_controller_test.go +++ b/api/environments/environment_controller_test.go @@ -8,12 +8,9 @@ import ( "testing" "time" - certclient "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned" certclientfake "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned/fake" deploymentModels "github.com/equinor/radix-api/api/deployments/models" environmentModels "github.com/equinor/radix-api/api/environments/models" - event "github.com/equinor/radix-api/api/events" - eventMock "github.com/equinor/radix-api/api/events/mock" eventModels "github.com/equinor/radix-api/api/events/models" "github.com/equinor/radix-api/api/secrets" secretModels "github.com/equinor/radix-api/api/secrets/models" @@ -21,7 +18,6 @@ import ( controllertest "github.com/equinor/radix-api/api/test" "github.com/equinor/radix-api/api/utils" authnmock "github.com/equinor/radix-api/api/utils/token/mock" - "github.com/equinor/radix-api/models" radixhttp "github.com/equinor/radix-common/net/http" radixutils "github.com/equinor/radix-common/utils" "github.com/equinor/radix-common/utils/numbers" @@ -47,7 +43,6 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/kubernetes" kubefake "k8s.io/client-go/kubernetes/fake" testing2 "k8s.io/client-go/testing" secretsstorevclient "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" @@ -328,7 +323,8 @@ func TestDeleteEnvironment_OneOrphanedEnvironment_OnlyOrphanedCanBeDeleted(t *te NewEnvironmentBuilder(). WithAppLabel(). WithAppName(anyAppName). - WithEnvironmentName(anyOrphanedEnvironment)) + WithEnvironmentName(anyOrphanedEnvironment). + WithOrphaned(true)) require.NoError(t, err) // Test @@ -987,27 +983,6 @@ func TestCreateSecret(t *testing.T) { assert.Equal(t, http.StatusOK, response.Code) } -func Test_GetEnvironmentEvents_Handler(t *testing.T) { - commonTestUtils, _, _, kubeclient, radixclient, kedaClient, _, secretproviderclient, certClient := setupTest(t, nil) - ctrl := gomock.NewController(t) - defer ctrl.Finish() - eventHandler := eventMock.NewMockEventHandler(ctrl) - handler := initHandler(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, WithEventHandler(eventHandler)) - raBuilder := operatorutils.ARadixApplication().WithAppName(anyAppName).WithEnvironment(anyEnvironment, "master") - - _, err := commonTestUtils.ApplyApplication(raBuilder) - require.NoError(t, err) - nsFunc := event.RadixEnvironmentNamespace(raBuilder.BuildRA(), anyEnvironment) - eventHandler.EXPECT(). - GetEvents(context.Background(), controllertest.EqualsNamespaceFunc(nsFunc)). - Return([]*eventModels.Event{{}, {}}, nil). - Times(1) - - events, err := handler.GetEnvironmentEvents(context.Background(), anyAppName, anyEnvironment) - assert.Nil(t, err) - assert.Len(t, events, 2) -} - func TestRestartAuxiliaryResource(t *testing.T) { auxType := "oauth" called := 0 @@ -2702,18 +2677,6 @@ func Test_DeleteBatch(t *testing.T) { } } -func initHandler(client kubernetes.Interface, - radixclient radixclient.Interface, - kedaClient kedav2.Interface, - secretproviderclient secretsstorevclient.Interface, - certClient certclient.Interface, - handlerConfig ...EnvironmentHandlerOptions) EnvironmentHandler { - accounts := models.NewAccounts(client, radixclient, kedaClient, secretproviderclient, nil, certClient, client, radixclient, kedaClient, secretproviderclient, nil, certClient) - options := []EnvironmentHandlerOptions{WithAccounts(accounts)} - options = append(options, handlerConfig...) - return Init(options...) -} - type ComponentCreatorStruct struct { name string number int diff --git a/api/environments/environment_handler.go b/api/environments/environment_handler.go index edd09999..9c41b582 100644 --- a/api/environments/environment_handler.go +++ b/api/environments/environment_handler.go @@ -10,7 +10,6 @@ import ( deploymentModels "github.com/equinor/radix-api/api/deployments/models" environmentModels "github.com/equinor/radix-api/api/environments/models" "github.com/equinor/radix-api/api/events" - eventModels "github.com/equinor/radix-api/api/events/models" "github.com/equinor/radix-api/api/kubequery" apimodels "github.com/equinor/radix-api/api/models" "github.com/equinor/radix-api/api/pods" @@ -39,7 +38,7 @@ type EnvironmentHandlerOptions func(*EnvironmentHandler) func WithAccounts(accounts models.Accounts) EnvironmentHandlerOptions { return func(eh *EnvironmentHandler) { eh.deployHandler = deployments.Init(accounts) - eh.eventHandler = events.Init(accounts.UserAccount.Client) + eh.eventHandler = events.Init(accounts.UserAccount.Client, accounts.UserAccount.RadixClient) eh.accounts = accounts } } @@ -239,7 +238,7 @@ func (eh EnvironmentHandler) DeleteEnvironment(ctx context.Context, appName, env return err } - if !re.Status.Orphaned { + if !re.Status.Orphaned && re.Status.OrphanedTimestamp == nil { // Must be removed from radix config first return environmentModels.CannotDeleteNonOrphanedEnvironment(appName, envName) } @@ -254,30 +253,6 @@ func (eh EnvironmentHandler) DeleteEnvironment(ctx context.Context, appName, env return nil } -// GetEnvironmentEvents Handler for GetEnvironmentEvents -func (eh EnvironmentHandler) GetEnvironmentEvents(ctx context.Context, appName, envName string) ([]*eventModels.Event, error) { - radixApplication, err := kubequery.GetRadixApplication(ctx, eh.accounts.UserAccount.RadixClient, appName) - if err != nil { - return nil, err - } - - _, err = kubequery.GetRadixEnvironment(ctx, eh.accounts.ServiceAccount.RadixClient, appName, envName) - // _, err = eh.getConfigurationStatus(ctx, envName, radixApplication) - if err != nil { - if errors.IsNotFound(err) { - return nil, environmentModels.NonExistingEnvironment(err, appName, envName) - } - return nil, err - } - - environmentEvents, err := eh.eventHandler.GetEvents(ctx, events.RadixEnvironmentNamespace(radixApplication, envName)) - if err != nil { - return nil, err - } - - return environmentEvents, nil -} - // getNotOrphanedEnvNames returns a slice of non-unique-names of not-orphaned environments func (eh EnvironmentHandler) getNotOrphanedEnvNames(ctx context.Context, appName string) ([]string, error) { reList, err := kubequery.GetRadixEnvironments(ctx, eh.accounts.ServiceAccount.RadixClient, appName) diff --git a/api/events/event_handler.go b/api/events/event_handler.go index 5ae90535..9037949c 100644 --- a/api/events/event_handler.go +++ b/api/events/event_handler.go @@ -2,95 +2,277 @@ package events import ( "context" + "fmt" + "regexp" + environmentModels "github.com/equinor/radix-api/api/environments/models" eventModels "github.com/equinor/radix-api/api/events/models" - v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" - k8sObjectUtils "github.com/equinor/radix-operator/pkg/apis/utils" - k8v1 "k8s.io/api/core/v1" + "github.com/equinor/radix-api/api/kubequery" + "github.com/equinor/radix-common/utils/slice" + radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + "github.com/equinor/radix-operator/pkg/apis/utils" + radixclient "github.com/equinor/radix-operator/pkg/client/clientset/versioned" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sTypes "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" ) +const ( + k8sKindDeployment = "Deployment" + k8sKindReplicaSet = "ReplicaSet" + k8sKindIngress = "Ingress" + k8sKindPod = "Pod" + k8sEventTypeNormal = "Normal" + k8sEventTypeWarning = "Warning" +) + // EventHandler defines methods for interacting with Kubernetes events type EventHandler interface { - GetEvents(ctx context.Context, namespaceFunc NamespaceFunc) ([]*eventModels.Event, error) + GetEnvironmentEvents(ctx context.Context, appName, envName string) ([]*eventModels.Event, error) + GetComponentEvents(ctx context.Context, appName, envName, componentName string) ([]*eventModels.Event, error) + GetPodEvents(ctx context.Context, appName, envName, componentName, podName string) ([]*eventModels.Event, error) } // NamespaceFunc defines a function that returns a namespace // Used as argument in GetEvents to filter events by namespace type NamespaceFunc func() string -// RadixEnvironmentNamespace builds a namespace from a RadixApplication and environment name -func RadixEnvironmentNamespace(ra *v1.RadixApplication, envName string) NamespaceFunc { - return func() string { - return k8sObjectUtils.GetEnvironmentNamespace(ra.Name, envName) +type eventHandler struct { + kubeClient kubernetes.Interface + radixClient radixclient.Interface +} + +// Init creates a new EventHandler +func Init(kubeClient kubernetes.Interface, radixClient radixclient.Interface) EventHandler { + return &eventHandler{kubeClient: kubeClient, radixClient: radixClient} +} + +// GetEnvironmentEvents return events for a namespace defined by a namespace +func (eh *eventHandler) GetEnvironmentEvents(ctx context.Context, appName, envName string) ([]*eventModels.Event, error) { + radixApplication, err := eh.getRadixApplicationAndValidateEnvironment(ctx, appName, envName) + if err != nil { + return nil, err } + environmentEvents, err := eh.getEvents(ctx, radixApplication.Name, envName, "", "") + if err != nil { + return nil, err + } + return environmentEvents, nil } -type eventHandler struct { - kubeClient kubernetes.Interface +// GetComponentEvents return events for a namespace defined by a namespace for a specific component +func (eh *eventHandler) GetComponentEvents(ctx context.Context, appName, envName, componentName string) ([]*eventModels.Event, error) { + if ok, err := eh.existsRadixDeployComponent(ctx, appName, envName, componentName); err != nil || !ok { + return nil, err + } + environmentEvents, err := eh.getEvents(ctx, appName, envName, componentName, "") + if err != nil { + return nil, err + } + return environmentEvents, nil } -// Init creates a new EventHandler -func Init(kubeClient kubernetes.Interface) EventHandler { - return &eventHandler{kubeClient: kubeClient} +// GetPodEvents return events for a namespace defined by a namespace for a specific pod of a component +func (eh *eventHandler) GetPodEvents(ctx context.Context, appName, envName, componentName, podName string) ([]*eventModels.Event, error) { + if ok, err := eh.existsRadixDeployComponent(ctx, appName, envName, componentName); err != nil || !ok { + return nil, err + } + environmentEvents, err := eh.getEvents(ctx, appName, envName, componentName, podName) + if err != nil { + return nil, err + } + return environmentEvents, nil +} + +func (eh *eventHandler) getRadixApplicationAndValidateEnvironment(ctx context.Context, appName string, envName string) (*radixv1.RadixApplication, error) { + radixApplication, err := kubequery.GetRadixApplication(ctx, eh.radixClient, appName) + if err != nil { + return nil, err + } + + _, err = kubequery.GetRadixEnvironment(ctx, eh.radixClient, appName, envName) + if err != nil { + if errors.IsNotFound(err) { + return nil, environmentModels.NonExistingEnvironment(err, appName, envName) + } + return nil, err + } + return radixApplication, err } -// GetEvents return events for a namespace defined by a NamespaceFunc function -func (eh *eventHandler) GetEvents(ctx context.Context, namespaceFunc NamespaceFunc) ([]*eventModels.Event, error) { - namespace := namespaceFunc() - return eh.getEvents(ctx, namespace) +func (eh *eventHandler) existsRadixDeployComponent(ctx context.Context, appName, envName, componentName string) (bool, error) { + _, err := eh.getRadixApplicationAndValidateEnvironment(ctx, appName, envName) + if err != nil { + if errors.IsNotFound(err) { + return false, environmentModels.NonExistingComponent(appName, componentName) + } + return false, err + } + radixDeployments, err := kubequery.GetRadixDeploymentsForEnvironments(ctx, eh.radixClient, appName, []string{envName}, 1) + if err != nil { + return false, err + } + activeRd, ok := slice.FindFirst(radixDeployments, func(rd radixv1.RadixDeployment) bool { return rd.Status.ActiveTo.IsZero() }) + if !ok { + return false, nil + } + return slice.Any(activeRd.Spec.Components, func(c radixv1.RadixDeployComponent) bool { return c.GetName() == componentName }), nil } -func (eh *eventHandler) getEvents(ctx context.Context, namespace string) ([]*eventModels.Event, error) { +func (eh *eventHandler) getEvents(ctx context.Context, appName, envName, componentName, podName string) ([]*eventModels.Event, error) { + namespace := utils.GetEnvironmentNamespace(appName, envName) k8sEvents, err := eh.kubeClient.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{}) if err != nil { return nil, err } + environmentComponentsPodMap, err := eh.getEnvironmentComponentsPodMap(ctx, appName, envName) + if err != nil { + return nil, err + } + + environmentComponentsIngressMap, err := eh.getEnvironmentComponentsIngressMap(ctx, appName, envName) + if err != nil { + return nil, err + } events := make([]*eventModels.Event, 0) for _, ev := range k8sEvents.Items { - builder := eventModels.NewEventBuilder().WithKubernetesEvent(ev) - buildObjectState(ctx, builder, ev, eh.kubeClient) - event := builder.Build() + if len(podName) > 0 && !eventIsRelatedToPod(ev, componentName, podName, environmentComponentsIngressMap) { + continue + } + if len(componentName) > 0 && !eventIsRelatedToComponent(ev, componentName, environmentComponentsIngressMap) { + continue + } + event := eh.buildEvent(ev, componentName, environmentComponentsPodMap, environmentComponentsIngressMap) events = append(events, event) } - return events, nil } -func buildObjectState(ctx context.Context, builder eventModels.EventBuilder, event k8v1.Event, kubeClient kubernetes.Interface) { - if event.Type == "Normal" { - return +func (eh *eventHandler) getEnvironmentComponentsPodMap(ctx context.Context, appName string, envName string) (map[k8sTypes.UID]*corev1.Pod, error) { + componentPods, err := kubequery.GetPodsForEnvironmentComponents(ctx, eh.kubeClient, appName, envName) + if err != nil { + return nil, err } + podMap := slice.Reduce(componentPods, make(map[k8sTypes.UID]*corev1.Pod), func(acc map[k8sTypes.UID]*corev1.Pod, pod corev1.Pod) map[k8sTypes.UID]*corev1.Pod { + acc[pod.GetUID()] = &pod + return acc + }) + return podMap, nil +} - if objectState := getObjectState(ctx, event, kubeClient); objectState != nil { - builder.WithInvolvedObjectState(objectState) +func (eh *eventHandler) getEnvironmentComponentsIngressMap(ctx context.Context, appName string, envName string) (map[string]*networkingv1.Ingress, error) { + ingresses, err := kubequery.GetIngressesForEnvironments(ctx, eh.kubeClient, appName, []string{envName}, 1) + if err != nil { + return nil, err } + ingressMap := slice.Reduce(ingresses, make(map[string]*networkingv1.Ingress), func(acc map[string]*networkingv1.Ingress, ingress networkingv1.Ingress) map[string]*networkingv1.Ingress { + acc[ingress.GetName()] = &ingress + return acc + }) + return ingressMap, nil } -func getObjectState(ctx context.Context, event k8v1.Event, kubeClient kubernetes.Interface) *eventModels.ObjectState { +func (eh *eventHandler) buildEvent(ev corev1.Event, componentName string, podMap map[k8sTypes.UID]*corev1.Pod, ingressMap map[string]*networkingv1.Ingress) *eventModels.Event { + builder := eventModels.NewEventBuilder().WithKubernetesEvent(ev) + if ev.Type != k8sEventTypeNormal || ev.InvolvedObject.Kind == k8sKindIngress { + if objectState := getObjectState(ev, podMap, ingressMap, componentName); objectState != nil { + builder.WithInvolvedObjectState(objectState) + } + } + return builder.Build() +} + +func eventIsRelatedToComponent(ev corev1.Event, componentName string, ingressMap map[string]*networkingv1.Ingress) bool { + if matchingToDeployment(ev, componentName) || + matchingToReplicaSet(ev, componentName, "") || + matchingToIngress(ev, componentName, ingressMap) { + return true + } + podNameRegex, err := regexp.Compile(fmt.Sprintf("^%s-[a-z0-9]{9,10}-[a-z0-9]{5}$", componentName)) + if err != nil { + return false + } + if ev.InvolvedObject.Kind == k8sKindPod && podNameRegex.MatchString(ev.InvolvedObject.Name) { + return true + } + return false +} + +func matchingToDeployment(ev corev1.Event, componentName string) bool { + return ev.InvolvedObject.Kind == k8sKindDeployment && ev.InvolvedObject.Name == componentName +} + +func matchingToReplicaSet(ev corev1.Event, componentName, podName string) bool { + if ev.InvolvedObject.Kind != k8sKindReplicaSet { + return false + } + if replicaSetNameRegex, err := regexp.Compile(fmt.Sprintf("^%s-[a-z0-9]{9,10}$", componentName)); err != nil || !replicaSetNameRegex.MatchString(ev.InvolvedObject.Name) { + return false + } + if len(podName) == 0 || len(ev.Message) == 0 { + return true + } + podNameRegex, err := regexp.Compile(fmt.Sprintf(`^[\w\s]*:\s%s$`, podName)) + if err != nil { + return false + } + return podNameRegex.MatchString(ev.Message) +} + +func matchingToIngress(ev corev1.Event, componentName string, ingressMap map[string]*networkingv1.Ingress) bool { + if ev.InvolvedObject.Kind != k8sKindIngress { + return false + } + ingress, ok := ingressMap[ev.InvolvedObject.Name] + if !ok { + return false + } + for _, ingressRule := range ingress.Spec.Rules { + for _, ingressPath := range ingressRule.HTTP.Paths { + if ingressPath.Backend.Service != nil && ingressPath.Backend.Service.Name == componentName { + return true + } + } + } + return false +} + +func eventIsRelatedToPod(ev corev1.Event, componentName, podName string, ingressMap map[string]*networkingv1.Ingress) bool { + if ev.InvolvedObject.Kind == k8sKindPod && ev.InvolvedObject.Name == podName { + return true + } + return matchingToDeployment(ev, componentName) || matchingToReplicaSet(ev, componentName, podName) || matchingToIngress(ev, componentName, ingressMap) +} + +func getObjectState(ev corev1.Event, podMap map[k8sTypes.UID]*corev1.Pod, ingressMap map[string]*networkingv1.Ingress, componentName string) *eventModels.ObjectState { builder := eventModels.NewObjectStateBuilder() - build := false - obj := event.InvolvedObject + obj := ev.InvolvedObject switch obj.Kind { - case "Pod": - if pod, err := kubeClient.CoreV1().Pods(obj.Namespace).Get(ctx, obj.Name, metav1.GetOptions{}); err == nil { + case k8sKindPod: + if pod, ok := podMap[ev.InvolvedObject.UID]; ok { state := getPodState(pod) builder.WithPodState(state) - build = true + return builder.Build() + } + case k8sKindIngress: + if ingress, ok := ingressMap[ev.InvolvedObject.Name]; ok { + builder.WithIngress(getIngress(ingress, componentName)) + return builder.Build() } } + return nil +} - if !build { - return nil - } - - return builder.Build() +func getIngress(ingress *networkingv1.Ingress, componentName string) []eventModels.IngressRule { + return eventModels.NewIngressBuilder().WithIngress(ingress).WithComponent(componentName).Build() } -func getPodState(pod *k8v1.Pod) *eventModels.PodState { +func getPodState(pod *corev1.Pod) *eventModels.PodState { return eventModels.NewPodStateBuilder(). WithPod(pod). Build() diff --git a/api/events/event_handler_test.go b/api/events/event_handler_test.go index cd811f2c..849b7cc9 100644 --- a/api/events/event_handler_test.go +++ b/api/events/event_handler_test.go @@ -2,118 +2,580 @@ package events import ( "context" + "fmt" "testing" + "time" + eventModels "github.com/equinor/radix-api/api/events/models" + "github.com/equinor/radix-common/utils/pointers" + "github.com/equinor/radix-common/utils/slice" operatorutils "github.com/equinor/radix-operator/pkg/apis/utils" - "github.com/stretchr/testify/require" - + radixlabels "github.com/equinor/radix-operator/pkg/apis/utils/labels" + radixfake "github.com/equinor/radix-operator/pkg/client/clientset/versioned/fake" "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" kubefake "k8s.io/client-go/kubernetes/fake" ) -func Test_EventHandler_Init(t *testing.T) { +const ( + appName1 = "app1" + appName2 = "app2" + envName1 = "env1" + ev1 = "ev1" + ev2 = "ev2" + ev3 = "ev3" + uid1 = "d1bf3ab3-0693-4291-a559-96a12ace9f33" + uid2 = "2dcc9cf7-086d-49a0-abe1-ea3610594eb2" + uid3 = "0c43a075-d174-479e-96b1-70183d67464c" + component1 = "server1" + component2 = "server2" + component3 = "server3" + deploy1 = component1 + deploy2 = component2 + deploy3 = component3 + replicaSetServer1 = deploy1 + "-4bf67cf976" + replicaSetServer2 = deploy2 + "-795977897d" + replicaSetServer3 = deploy3 + "-5c97b4c698" + podServer1 = replicaSetServer1 + "-9v333" + podServer2 = replicaSetServer2 + "-m2sw8" + podServer3 = replicaSetServer3 + "-6x4cg" + ingressName1 = "ingress1" + ingressHost1 = "ingress1.example.com" + ingressName2 = "ingress2" + ingressHost2 = "ingress2.example.com" + ingressName3 = "ingress3" + ingressHost3 = "ingress3.example.com" + port8080 = int32(8080) + port8090 = int32(8090) + port9090 = int32(9090) +) + +type scenario struct { + name string + existingEventProps []eventProps + expectedEvents []eventModels.Event + existingIngressRuleProps []ingressRuleProps +} + +type eventProps struct { + name string + appName string + envName string + componentName string + podName string + eventType string + objectName string + objectUid string + objectKind string +} + +type ingressRuleProps struct { + name string + host string + service string + port int32 + appName string + envName string +} + +func setupTest() (*kubefake.Clientset, *radixfake.Clientset) { kubeClient := kubefake.NewSimpleClientset() - eh := Init(kubeClient).(*eventHandler) + radixClient := radixfake.NewSimpleClientset() + return kubeClient, radixClient +} + +func Test_EventHandler_Init(t *testing.T) { + kubeClient, radixClient := setupTest() + eh := Init(kubeClient, radixClient).(*eventHandler) assert.NotNil(t, eh) assert.Equal(t, kubeClient, eh.kubeClient) } -func Test_EventHandler_RadixEnvironmentNamespace(t *testing.T) { - appName, envName := "app", "env" - expected := operatorutils.GetEnvironmentNamespace(appName, envName) - ra := operatorutils.NewRadixApplicationBuilder().WithAppName(appName).BuildRA() - actual := RadixEnvironmentNamespace(ra, envName)() - assert.Equal(t, expected, actual) +func Test_EventHandler_NoEventsWhenThereIsNoRadixApplication(t *testing.T) { + appName, envName := "app1", "env1" + envNamespace := operatorutils.GetEnvironmentNamespace(appName, envName) + kubeClient, radixClient := setupTest() + + createKubernetesEvent(t, kubeClient, envNamespace, ev1, k8sEventTypeNormal, podServer1, k8sKindPod, uid1) + + eventHandler := Init(kubeClient, radixClient) + events, err := eventHandler.GetEnvironmentEvents(context.Background(), appName, envName) + assert.NotNil(t, err) + assert.Len(t, events, 0) } -func Test_EventHandler_GetEventsForRadixApplication(t *testing.T) { - appName, envName := "app", "env" - appNamespace := operatorutils.GetEnvironmentNamespace(appName, envName) - kubeClient := kubefake.NewSimpleClientset() +func Test_EventHandler_NoEventsWhenThereIsNoRadixEnvironment(t *testing.T) { + appName, envName := "app1", "env1" + envNamespace := operatorutils.GetEnvironmentNamespace(appName, envName) + kubeClient, radixClient := setupTest() - createKubernetesEvent(t, kubeClient, appNamespace, "ev1", "Normal", "pod1", "Pod") - createKubernetesEvent(t, kubeClient, appNamespace, "ev2", "Normal", "pod2", "Pod") - createKubernetesEvent(t, kubeClient, "app2-env", "ev3", "Normal", "pod3", "Pod") + createRadixApp(t, kubeClient, radixClient, appName, envName) + err := radixClient.RadixV1().RadixEnvironments().Delete(context.Background(), fmt.Sprintf("%s-%s", appName, envName), metav1.DeleteOptions{}) + require.NoError(t, err) + createKubernetesEvent(t, kubeClient, envNamespace, ev1, k8sEventTypeNormal, podServer1, k8sKindPod, uid1) - ra := operatorutils.NewRadixApplicationBuilder().WithAppName(appName).BuildRA() - eventHandler := Init(kubeClient) - events, err := eventHandler.GetEvents(context.Background(), RadixEnvironmentNamespace(ra, envName)) - assert.Nil(t, err) - assert.Len(t, events, 2) - assert.ElementsMatch( - t, - []string{"pod1", "pod2"}, - []string{events[0].InvolvedObjectName, events[1].InvolvedObjectName}, - ) + eventHandler := Init(kubeClient, radixClient) + events, err := eventHandler.GetEnvironmentEvents(context.Background(), appName, envName) + assert.NotNil(t, err) + assert.Len(t, events, 0) } func Test_EventHandler_GetEvents_PodState(t *testing.T) { - appName, envName := "app", "env" - appNamespace := operatorutils.GetEnvironmentNamespace(appName, envName) - ra := operatorutils.NewRadixApplicationBuilder().WithAppName(appName).BuildRA() + appName, envName := "app1", "env1" + envNamespace := operatorutils.GetEnvironmentNamespace(appName, envName) t.Run("ObjectState is nil for normal event type", func(t *testing.T) { - kubeClient := kubefake.NewSimpleClientset() - createKubernetesEvent(t, kubeClient, appNamespace, "ev1", "Normal", "pod1", "Pod") - _, err := createKubernetesPod(kubeClient, "pod1", appNamespace, true, true, 0) + kubeClient, radixClient := setupTest() + createRadixApp(t, kubeClient, radixClient, appName, envName) + _, err := createKubernetesPod(kubeClient, podServer1, appName, envName, true, true, 0, uid1) + createKubernetesEvent(t, kubeClient, envNamespace, ev1, k8sEventTypeNormal, podServer1, k8sKindPod, uid1) require.NoError(t, err) - eventHandler := Init(kubeClient) - events, _ := eventHandler.GetEvents(context.Background(), RadixEnvironmentNamespace(ra, envName)) + eventHandler := Init(kubeClient, radixClient) + events, _ := eventHandler.GetEnvironmentEvents(context.Background(), appName, envName) assert.Len(t, events, 1) assert.Nil(t, events[0].InvolvedObjectState) }) t.Run("ObjectState has Pod state for warning event type", func(t *testing.T) { - kubeClient := kubefake.NewSimpleClientset() - createKubernetesEvent(t, kubeClient, appNamespace, "ev1", "Warning", "pod1", "Pod") - _, err := createKubernetesPod(kubeClient, "pod1", appNamespace, true, false, 0) + kubeClient, radixClient := setupTest() + createRadixApp(t, kubeClient, radixClient, appName, envName) + _, err := createKubernetesPod(kubeClient, podServer1, appName, envName, true, false, 0, uid1) + createKubernetesEvent(t, kubeClient, envNamespace, ev1, k8sEventTypeWarning, podServer1, k8sKindPod, uid1) require.NoError(t, err) - eventHandler := Init(kubeClient) - events, _ := eventHandler.GetEvents(context.Background(), RadixEnvironmentNamespace(ra, envName)) + eventHandler := Init(kubeClient, radixClient) + events, _ := eventHandler.GetEnvironmentEvents(context.Background(), appName, envName) assert.Len(t, events, 1) assert.NotNil(t, events[0].InvolvedObjectState) assert.NotNil(t, events[0].InvolvedObjectState.Pod) }) t.Run("ObjectState is nil for warning event type when pod not exist", func(t *testing.T) { - kubeClient := kubefake.NewSimpleClientset() - createKubernetesEvent(t, kubeClient, appNamespace, "ev1", "Normal", "pod1", "Pod") - eventHandler := Init(kubeClient) - events, _ := eventHandler.GetEvents(context.Background(), RadixEnvironmentNamespace(ra, envName)) + kubeClient, radixClient := setupTest() + createRadixApp(t, kubeClient, radixClient, appName, envName) + createKubernetesEvent(t, kubeClient, envNamespace, ev1, k8sEventTypeNormal, podServer1, k8sKindPod, uid1) + eventHandler := Init(kubeClient, radixClient) + events, _ := eventHandler.GetEnvironmentEvents(context.Background(), appName, envName) assert.Len(t, events, 1) assert.Nil(t, events[0].InvolvedObjectState) }) } -func createKubernetesEvent(t *testing.T, client *kubefake.Clientset, namespace, - name, eventType, involvedObjectName, involvedObjectKind string) { - _, err := client.CoreV1().Events(namespace).CreateWithEventNamespace(&v1.Event{ +func Test_EventHandler_GetEnvironmentEvents(t *testing.T) { + envNamespace := operatorutils.GetEnvironmentNamespace(appName1, envName1) + + scenarios := []scenario{ + { + name: "Pod events", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, eventType: k8sEventTypeNormal, objectName: podServer1, objectKind: k8sKindPod, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, eventType: k8sEventTypeNormal, objectName: podServer2, objectKind: k8sKindPod, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, eventType: k8sEventTypeNormal, objectName: podServer3, objectKind: k8sKindPod, objectUid: uid3}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: podServer1, InvolvedObjectKind: k8sKindPod, InvolvedObjectNamespace: envNamespace}, + {InvolvedObjectName: podServer2, InvolvedObjectKind: k8sKindPod, InvolvedObjectNamespace: envNamespace}, + }, + }, + { + name: "Deploy events", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, eventType: k8sEventTypeNormal, objectName: deploy1, objectKind: k8sKindDeployment, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, eventType: k8sEventTypeNormal, objectName: deploy2, objectKind: k8sKindDeployment, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, eventType: k8sEventTypeNormal, objectName: deploy3, objectKind: k8sKindDeployment, objectUid: uid3}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: deploy1, InvolvedObjectKind: k8sKindDeployment, InvolvedObjectNamespace: envNamespace}, + {InvolvedObjectName: deploy2, InvolvedObjectKind: k8sKindDeployment, InvolvedObjectNamespace: envNamespace}, + }, + }, + { + name: "ReplicaSet events", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, eventType: k8sEventTypeNormal, objectName: replicaSetServer1, objectKind: k8sKindReplicaSet, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, eventType: k8sEventTypeNormal, objectName: replicaSetServer2, objectKind: k8sKindReplicaSet, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, eventType: k8sEventTypeNormal, objectName: replicaSetServer3, objectKind: k8sKindReplicaSet, objectUid: uid3}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: replicaSetServer1, InvolvedObjectKind: k8sKindReplicaSet, InvolvedObjectNamespace: envNamespace}, + {InvolvedObjectName: replicaSetServer2, InvolvedObjectKind: k8sKindReplicaSet, InvolvedObjectNamespace: envNamespace}, + }, + }, + { + name: "Ingress events", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, eventType: k8sEventTypeNormal, objectName: ingressName1, objectKind: k8sKindIngress, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, eventType: k8sEventTypeNormal, objectName: ingressName2, objectKind: k8sKindIngress, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, eventType: k8sEventTypeNormal, objectName: ingressName3, objectKind: k8sKindIngress, objectUid: uid3}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: ingressName1, InvolvedObjectKind: k8sKindIngress, InvolvedObjectNamespace: envNamespace}, + {InvolvedObjectName: ingressName2, InvolvedObjectKind: k8sKindIngress, InvolvedObjectNamespace: envNamespace}, + }, + }, + { + name: "Ingress events with rules", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, eventType: k8sEventTypeNormal, objectName: ingressName1, objectKind: k8sKindIngress, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, eventType: k8sEventTypeNormal, objectName: ingressName2, objectKind: k8sKindIngress, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, eventType: k8sEventTypeNormal, objectName: ingressName3, objectKind: k8sKindIngress, objectUid: uid3}, + }, + existingIngressRuleProps: []ingressRuleProps{ + {name: ingressName1, appName: appName1, envName: envName1, host: ingressHost1, service: deploy1, port: port8080}, + {name: ingressName2, appName: appName1, envName: envName1, host: ingressHost2, service: deploy2, port: port8090}, + {name: ingressName3, appName: "app2", envName: envName1, host: ingressHost3, service: deploy3, port: port9090}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: ingressName1, InvolvedObjectKind: k8sKindIngress, InvolvedObjectNamespace: envNamespace}, + {InvolvedObjectName: ingressName2, InvolvedObjectKind: k8sKindIngress, InvolvedObjectNamespace: envNamespace}, + }, + }, + } + for _, ts := range scenarios { + t.Run(ts.name, func(t *testing.T) { + eventHandler, _ := setupTestEnvForHandler(t, ts) + actualEvents, err := eventHandler.GetEnvironmentEvents(context.Background(), appName1, envName1) + assert.Nil(t, err) + assertEvents(t, ts.expectedEvents, actualEvents) + }) + } +} + +func Test_EventHandler_GetComponentEvents(t *testing.T) { + envNamespace := operatorutils.GetEnvironmentNamespace(appName1, envName1) + + scenarios := []scenario{ + { + name: "Pod events", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, componentName: component1, eventType: k8sEventTypeNormal, objectName: podServer1, objectKind: k8sKindPod, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, componentName: component2, eventType: k8sEventTypeNormal, objectName: podServer2, objectKind: k8sKindPod, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, componentName: component1, eventType: k8sEventTypeNormal, objectName: podServer3, objectKind: k8sKindPod, objectUid: uid3}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: podServer1, InvolvedObjectKind: k8sKindPod, InvolvedObjectNamespace: envNamespace}, + }, + }, + { + name: "Deploy events", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, componentName: component1, eventType: k8sEventTypeNormal, objectName: deploy1, objectKind: k8sKindDeployment, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, componentName: component2, eventType: k8sEventTypeNormal, objectName: deploy2, objectKind: k8sKindDeployment, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, componentName: component1, eventType: k8sEventTypeNormal, objectName: deploy3, objectKind: k8sKindDeployment, objectUid: uid3}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: deploy1, InvolvedObjectKind: k8sKindDeployment, InvolvedObjectNamespace: envNamespace}, + }, + }, + { + name: "ReplicaSet events", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, componentName: component1, eventType: k8sEventTypeNormal, objectName: replicaSetServer1, objectKind: k8sKindReplicaSet, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, componentName: component2, eventType: k8sEventTypeNormal, objectName: replicaSetServer2, objectKind: k8sKindReplicaSet, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, componentName: component1, eventType: k8sEventTypeNormal, objectName: replicaSetServer3, objectKind: k8sKindReplicaSet, objectUid: uid3}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: replicaSetServer1, InvolvedObjectKind: k8sKindReplicaSet, InvolvedObjectNamespace: envNamespace}, + }, + }, + { + name: "Ingress events", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, componentName: component1, eventType: k8sEventTypeNormal, objectName: ingressName1, objectKind: k8sKindIngress, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, componentName: component2, eventType: k8sEventTypeNormal, objectName: ingressName2, objectKind: k8sKindIngress, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, componentName: component1, eventType: k8sEventTypeNormal, objectName: ingressName3, objectKind: k8sKindIngress, objectUid: uid3}, + }, + existingIngressRuleProps: []ingressRuleProps{ + {name: ingressName1, appName: appName1, envName: envName1, host: ingressHost1, service: deploy1, port: port8080}, + {name: ingressName2, appName: appName1, envName: envName1, host: ingressHost2, service: deploy2, port: port8090}, + {name: ingressName3, appName: "app2", envName: envName1, host: ingressHost3, service: deploy3, port: port9090}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: ingressName1, InvolvedObjectKind: k8sKindIngress, InvolvedObjectNamespace: envNamespace}, + }, + }, + { + name: "Ingress events with rules", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, componentName: component1, eventType: k8sEventTypeNormal, objectName: ingressName1, objectKind: k8sKindIngress, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, componentName: component2, eventType: k8sEventTypeNormal, objectName: ingressName2, objectKind: k8sKindIngress, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, componentName: component1, eventType: k8sEventTypeNormal, objectName: ingressName3, objectKind: k8sKindIngress, objectUid: uid3}, + }, + existingIngressRuleProps: []ingressRuleProps{ + {name: ingressName1, appName: appName1, envName: envName1, host: ingressHost1, service: deploy1, port: port8080}, + {name: ingressName2, appName: appName1, envName: envName1, host: ingressHost2, service: deploy2, port: port8090}, + {name: ingressName3, appName: "app2", envName: envName1, host: ingressHost3, service: deploy3, port: port9090}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: ingressName1, InvolvedObjectKind: k8sKindIngress, InvolvedObjectNamespace: envNamespace}, + }, + }, + } + for _, ts := range scenarios { + t.Run(ts.name, func(t *testing.T) { + eventHandler, radixClient := setupTestEnvForHandler(t, ts) + createActiveRadixDeployments(t, ts, radixClient) + + actualEvents, err := eventHandler.GetComponentEvents(context.Background(), appName1, envName1, component1) + assert.Nil(t, err) + assertEvents(t, ts.expectedEvents, actualEvents) + }) + } +} + +func Test_EventHandler_GetPodEvents(t *testing.T) { + envNamespace := operatorutils.GetEnvironmentNamespace(appName1, envName1) + + scenarios := []scenario{ + { + name: "Pod events", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, componentName: component1, podName: podServer1, eventType: k8sEventTypeNormal, objectName: podServer1, objectKind: k8sKindPod, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, componentName: component2, podName: podServer2, eventType: k8sEventTypeNormal, objectName: podServer2, objectKind: k8sKindPod, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, componentName: component1, podName: podServer3, eventType: k8sEventTypeNormal, objectName: podServer3, objectKind: k8sKindPod, objectUid: uid3}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: podServer1, InvolvedObjectKind: k8sKindPod, InvolvedObjectNamespace: envNamespace}, + }, + }, + { + name: "Deploy events", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, componentName: component1, podName: podServer1, eventType: k8sEventTypeNormal, objectName: deploy1, objectKind: k8sKindDeployment, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, componentName: component2, podName: podServer2, eventType: k8sEventTypeNormal, objectName: deploy2, objectKind: k8sKindDeployment, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, componentName: component1, podName: podServer3, eventType: k8sEventTypeNormal, objectName: deploy3, objectKind: k8sKindDeployment, objectUid: uid3}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: deploy1, InvolvedObjectKind: k8sKindDeployment, InvolvedObjectNamespace: envNamespace}, + }, + }, + { + name: "ReplicaSet events", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, componentName: component1, podName: podServer1, eventType: k8sEventTypeNormal, objectName: replicaSetServer1, objectKind: k8sKindReplicaSet, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, componentName: component2, podName: podServer2, eventType: k8sEventTypeNormal, objectName: replicaSetServer2, objectKind: k8sKindReplicaSet, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, componentName: component1, podName: podServer3, eventType: k8sEventTypeNormal, objectName: replicaSetServer3, objectKind: k8sKindReplicaSet, objectUid: uid3}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: replicaSetServer1, InvolvedObjectKind: k8sKindReplicaSet, InvolvedObjectNamespace: envNamespace}, + }, + }, + { + name: "Ingress events", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, componentName: component1, podName: podServer1, eventType: k8sEventTypeNormal, objectName: ingressName1, objectKind: k8sKindIngress, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, componentName: component2, podName: podServer2, eventType: k8sEventTypeNormal, objectName: ingressName2, objectKind: k8sKindIngress, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, componentName: component1, podName: podServer3, eventType: k8sEventTypeNormal, objectName: ingressName3, objectKind: k8sKindIngress, objectUid: uid3}, + }, + existingIngressRuleProps: []ingressRuleProps{ + {name: ingressName1, appName: appName1, envName: envName1, host: ingressHost1, service: deploy1, port: port8080}, + {name: ingressName2, appName: appName1, envName: envName1, host: ingressHost2, service: deploy2, port: port8090}, + {name: ingressName3, appName: "app2", envName: envName1, host: ingressHost3, service: deploy3, port: port9090}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: ingressName1, InvolvedObjectKind: k8sKindIngress, InvolvedObjectNamespace: envNamespace}, + }, + }, + { + name: "Ingress events with rules", + existingEventProps: []eventProps{ + {name: ev1, appName: appName1, envName: envName1, componentName: component1, podName: podServer1, eventType: k8sEventTypeNormal, objectName: ingressName1, objectKind: k8sKindIngress, objectUid: uid1}, + {name: ev2, appName: appName1, envName: envName1, componentName: component2, podName: podServer2, eventType: k8sEventTypeNormal, objectName: ingressName2, objectKind: k8sKindIngress, objectUid: uid2}, + {name: ev3, appName: appName2, envName: envName1, componentName: component1, podName: podServer3, eventType: k8sEventTypeNormal, objectName: ingressName3, objectKind: k8sKindIngress, objectUid: uid3}, + }, + existingIngressRuleProps: []ingressRuleProps{ + {name: ingressName1, appName: appName1, envName: envName1, host: ingressHost1, service: deploy1, port: port8080}, + {name: ingressName2, appName: appName1, envName: envName1, host: ingressHost2, service: deploy2, port: port8090}, + {name: ingressName3, appName: "app2", envName: envName1, host: ingressHost3, service: deploy3, port: port9090}, + }, + expectedEvents: []eventModels.Event{ + {InvolvedObjectName: ingressName1, InvolvedObjectKind: k8sKindIngress, InvolvedObjectNamespace: envNamespace}, + }, + }, + } + for _, ts := range scenarios { + t.Run(ts.name, func(t *testing.T) { + eventHandler, radixClient := setupTestEnvForHandler(t, ts) + createActiveRadixDeployments(t, ts, radixClient) + + actualEvents, err := eventHandler.GetPodEvents(context.Background(), appName1, envName1, component1, podServer1) + assert.Nil(t, err) + assertEvents(t, ts.expectedEvents, actualEvents) + }) + } +} + +func createActiveRadixDeployments(t *testing.T, ts scenario, radixClient *radixfake.Clientset) { + appEnvComponentMap := getAppEnvComponentMap(ts) + for appName, envComponentNameMap := range appEnvComponentMap { + for envName, componentNameMap := range envComponentNameMap { + builder := operatorutils. + NewDeploymentBuilder(). + WithAppName(appName). + WithEnvironment(envName) + for componentName := range componentNameMap { + builder = builder.WithComponent(operatorutils. + NewDeployComponentBuilder(). + WithName(componentName)) + } + rd := builder.WithActiveFrom(time.Now()).BuildRD() + _, err := radixClient.RadixV1().RadixDeployments(operatorutils.GetEnvironmentNamespace(appName, envName)). + Create(context.Background(), rd, metav1.CreateOptions{}) + require.NoError(t, err) + } + } +} + +func createRadixApplications(t *testing.T, ts scenario, kubeClient *kubefake.Clientset, radixClient *radixfake.Clientset) { + appEnvComponentMap := getAppEnvComponentMap(ts) + for appName, envComponentNameMap := range appEnvComponentMap { + var envNames []string + for envName := range envComponentNameMap { + envNames = append(envNames, envName) + } + createRadixApp(t, kubeClient, radixClient, appName, envNames...) + } +} + +func getAppEnvComponentMap(ts scenario) map[string]map[string]map[string]struct{} { + appEnvComponentMap := slice.Reduce(ts.existingEventProps, make(map[string]map[string]map[string]struct{}), func(acc map[string]map[string]map[string]struct{}, evProps eventProps) map[string]map[string]map[string]struct{} { + if _, ok := acc[evProps.appName]; !ok { + acc[evProps.appName] = make(map[string]map[string]struct{}) + } + if _, ok := acc[evProps.appName][evProps.envName]; !ok { + acc[evProps.appName][evProps.envName] = make(map[string]struct{}) + } + acc[evProps.appName][evProps.envName][evProps.componentName] = struct{}{} + return acc + }) + return appEnvComponentMap +} + +func getAppEnvPodsMap(ts scenario) map[string]map[string]map[string]string { + appEnvComponentMap := slice.Reduce(ts.existingEventProps, make(map[string]map[string]map[string]string), func(acc map[string]map[string]map[string]string, evProps eventProps) map[string]map[string]map[string]string { + if len(evProps.podName) == 0 { + return nil + } + if _, ok := acc[evProps.appName]; !ok { + acc[evProps.appName] = make(map[string]map[string]string) + } + if _, ok := acc[evProps.appName][evProps.envName]; !ok { + acc[evProps.appName][evProps.envName] = make(map[string]string) + } + acc[evProps.appName][evProps.envName][evProps.podName] = evProps.objectUid + return acc + }) + return appEnvComponentMap +} + +func assertEvents(t *testing.T, expectedEvents []eventModels.Event, actualEvents []*eventModels.Event) { + if assert.Len(t, actualEvents, len(expectedEvents)) { + for i := 0; i < len(expectedEvents); i++ { + assert.Equal(t, expectedEvents[i].InvolvedObjectName, actualEvents[i].InvolvedObjectName) + assert.Equal(t, expectedEvents[i].InvolvedObjectKind, actualEvents[i].InvolvedObjectKind) + assert.Equal(t, expectedEvents[i].InvolvedObjectNamespace, actualEvents[i].InvolvedObjectNamespace) + } + } +} + +func setupTestEnvForHandler(t *testing.T, ts scenario) (EventHandler, *radixfake.Clientset) { + kubeClient, radixClient := setupTest() + createRadixApplications(t, ts, kubeClient, radixClient) + for _, evProps := range ts.existingEventProps { + createKubernetesEvent(t, kubeClient, operatorutils.GetEnvironmentNamespace(evProps.appName, evProps.envName), evProps.name, evProps.eventType, evProps.objectName, evProps.objectKind, evProps.objectUid) + } + appEnvPodsMap := getAppEnvPodsMap(ts) + for appName, envPodNameMap := range appEnvPodsMap { + for envName, podNameMap := range envPodNameMap { + for podName, uid := range podNameMap { + _, err := createKubernetesPod(kubeClient, podName, appName, envName, true, true, 0, uid) + require.NoError(t, err) + } + } + } + + for _, ingressRuleProp := range ts.existingIngressRuleProps { + createIngress(t, kubeClient, ingressRuleProp) + } + eventHandler := Init(kubeClient, radixClient) + return eventHandler, radixClient +} + +func createIngress(t *testing.T, kubeClient *kubefake.Clientset, props ingressRuleProps) { + _, err := kubeClient.NetworkingV1().Ingresses(operatorutils.GetEnvironmentNamespace(props.appName, props.envName)).Create(context.Background(), &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{Name: props.name, + Labels: radixlabels.ForApplicationName(props.appName)}, + Spec: networkingv1.IngressSpec{ + Rules: []networkingv1.IngressRule{ + {Host: props.host, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/", + PathType: pointers.Ptr(networkingv1.PathTypeImplementationSpecific), + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: props.service, + Port: networkingv1.ServiceBackendPort{Number: props.port}, + }, + }, + }, + }, + }, + }}, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) +} + +func createKubernetesEvent(t *testing.T, client *kubefake.Clientset, namespace, name, eventType, involvedObjectName, involvedObjectKind, uid string) { + _, err := client.CoreV1().Events(namespace).CreateWithEventNamespace(&corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, - InvolvedObject: v1.ObjectReference{ + InvolvedObject: corev1.ObjectReference{ Kind: involvedObjectKind, Name: involvedObjectName, Namespace: namespace, + UID: types.UID(uid), }, Type: eventType, }) require.NoError(t, err) } -func createKubernetesPod(client *kubefake.Clientset, name, namespace string, started, ready bool, restartCount int32) (*v1.Pod, error) { +func createKubernetesPod(client *kubefake.Clientset, name, appName, envName string, started, ready bool, restartCount int32, uid string) (*corev1.Pod, error) { + namespace := operatorutils.GetEnvironmentNamespace(appName, envName) return client.CoreV1().Pods(namespace).Create(context.Background(), - &v1.Pod{ + &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Name: name, + Name: name, + UID: types.UID(uid), + Labels: radixlabels.ForApplicationName(appName), }, - Status: v1.PodStatus{ - ContainerStatuses: []v1.ContainerStatus{ + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ {Started: &started, Ready: ready, RestartCount: restartCount}, }, }, }, metav1.CreateOptions{}) } + +func createRadixApp(t *testing.T, kubeClient *kubefake.Clientset, radixClient *radixfake.Clientset, appName string, envName ...string) { + builder := operatorutils.NewRadixApplicationBuilder().WithAppName(appName) + for _, envName := range envName { + builder = builder.WithEnvironment(envName, "") + _, err := radixClient.RadixV1().RadixEnvironments().Create(context.Background(), operatorutils.NewEnvironmentBuilder().WithAppName(appName).WithEnvironmentName(envName).BuildRE(), metav1.CreateOptions{}) + require.NoError(t, err) + _, err = kubeClient.CoreV1().Namespaces().Create(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: operatorutils.GetEnvironmentNamespace(appName, envName)}}, metav1.CreateOptions{}) + require.NoError(t, err) + } + _, err := radixClient.RadixV1().RadixApplications(operatorutils.GetAppNamespace(appName)).Create(context.Background(), builder.BuildRA(), metav1.CreateOptions{}) + require.NoError(t, err) +} diff --git a/api/events/mock/event_handler_mock.go b/api/events/mock/event_handler_mock.go index f3a2222f..5f635a27 100644 --- a/api/events/mock/event_handler_mock.go +++ b/api/events/mock/event_handler_mock.go @@ -8,7 +8,6 @@ import ( context "context" reflect "reflect" - events "github.com/equinor/radix-api/api/events" models "github.com/equinor/radix-api/api/events/models" gomock "github.com/golang/mock/gomock" ) @@ -36,17 +35,47 @@ func (m *MockEventHandler) EXPECT() *MockEventHandlerMockRecorder { return m.recorder } -// GetEvents mocks base method. -func (m *MockEventHandler) GetEvents(ctx context.Context, namespaceFunc events.NamespaceFunc) ([]*models.Event, error) { +// GetComponentEvents mocks base method. +func (m *MockEventHandler) GetComponentEvents(ctx context.Context, appName, envName, componentName string) ([]*models.Event, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEvents", ctx, namespaceFunc) + ret := m.ctrl.Call(m, "GetComponentEvents", ctx, appName, envName, componentName) ret0, _ := ret[0].([]*models.Event) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetEvents indicates an expected call of GetEvents. -func (mr *MockEventHandlerMockRecorder) GetEvents(ctx, namespaceFunc interface{}) *gomock.Call { +// GetComponentEvents indicates an expected call of GetComponentEvents. +func (mr *MockEventHandlerMockRecorder) GetComponentEvents(ctx, appName, envName, componentName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEvents", reflect.TypeOf((*MockEventHandler)(nil).GetEvents), ctx, namespaceFunc) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetComponentEvents", reflect.TypeOf((*MockEventHandler)(nil).GetComponentEvents), ctx, appName, envName, componentName) +} + +// GetEnvironmentEvents mocks base method. +func (m *MockEventHandler) GetEnvironmentEvents(ctx context.Context, appName, envName string) ([]*models.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEnvironmentEvents", ctx, appName, envName) + ret0, _ := ret[0].([]*models.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEnvironmentEvents indicates an expected call of GetEnvironmentEvents. +func (mr *MockEventHandlerMockRecorder) GetEnvironmentEvents(ctx, appName, envName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvironmentEvents", reflect.TypeOf((*MockEventHandler)(nil).GetEnvironmentEvents), ctx, appName, envName) +} + +// GetPodEvents mocks base method. +func (m *MockEventHandler) GetPodEvents(ctx context.Context, appName, envName, componentName, podName string) ([]*models.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPodEvents", ctx, appName, envName, componentName, podName) + ret0, _ := ret[0].([]*models.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPodEvents indicates an expected call of GetPodEvents. +func (mr *MockEventHandlerMockRecorder) GetPodEvents(ctx, appName, envName, componentName, podName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPodEvents", reflect.TypeOf((*MockEventHandler)(nil).GetPodEvents), ctx, appName, envName, componentName, podName) } diff --git a/api/events/models/event.go b/api/events/models/event.go index cf5a17cf..8e8d4576 100644 --- a/api/events/models/event.go +++ b/api/events/models/event.go @@ -23,11 +23,26 @@ type PodState struct { RestartCount int32 `json:"restartCount"` } +// IngressRule specs +// swagger:model IngressRule +type IngressRule struct { + // The service name of the ingress + Service string `json:"service"` + // The host name of the ingress + Host string `json:"host"` + // The port of the ingress + Port int32 `json:"port"` + // The path of the ingress + Path string `json:"path"` +} + // ObjectState holds information about the state of objects involved in an event // swagger:model ObjectState type ObjectState struct { // Details about the pod state for a pod related event Pod *PodState `json:"pod"` + // Details about the ingress rules for an ingress related event + IngressRules []IngressRule `json:"ingressRules"` } // Event holds information about Kubernetes events diff --git a/api/events/models/event_builder.go b/api/events/models/event_builder.go index 28cb43b0..204a99e9 100644 --- a/api/events/models/event_builder.go +++ b/api/events/models/event_builder.go @@ -1,6 +1,7 @@ package models import ( + "strings" "time" v1 "k8s.io/api/core/v1" @@ -100,6 +101,6 @@ func (eb *eventBuilder) Build() *Event { InvolvedObjectState: eb.involvedObjectState, Type: eb.eventType, Reason: eb.reason, - Message: eb.message, + Message: strings.ReplaceAll(eb.message, `\"`, "'"), } } diff --git a/api/events/models/ingress_builder.go b/api/events/models/ingress_builder.go new file mode 100644 index 00000000..35918c85 --- /dev/null +++ b/api/events/models/ingress_builder.go @@ -0,0 +1,59 @@ +package models + +import ( + networkingv1 "k8s.io/api/networking/v1" +) + +// IngressStateBuilder Build Ingress +type IngressStateBuilder interface { + // WithIngress sets the Ingress + WithIngress(*networkingv1.Ingress) IngressStateBuilder + // WithComponent sets the component name + WithComponent(componentName string) IngressStateBuilder + Build() []IngressRule +} + +type ingressStateBuilder struct { + ingress *networkingv1.Ingress + componentName string +} + +// NewIngressBuilder Constructor for ingressBuilder +func NewIngressBuilder() IngressStateBuilder { + return &ingressStateBuilder{} +} + +func (b *ingressStateBuilder) WithIngress(v *networkingv1.Ingress) IngressStateBuilder { + b.ingress = v + return b +} + +func (b *ingressStateBuilder) WithComponent(componentName string) IngressStateBuilder { + b.componentName = componentName + return b +} + +func (b *ingressStateBuilder) Build() []IngressRule { + if b.ingress == nil { + return nil + } + + var ingressRules []IngressRule + for _, rule := range b.ingress.Spec.Rules { + if rule.HTTP != nil { + for _, path := range rule.HTTP.Paths { + ingressRule := IngressRule{Host: rule.Host, Path: path.Path} + + if path.Backend.Service != nil { + if len(b.componentName) == 0 { + ingressRule.Service = path.Backend.Service.Name // provide service name only component name is not set + } + ingressRule.Port = path.Backend.Service.Port.Number + } + ingressRules = append(ingressRules , ingressRule) + } + } + } + + return ingressRules +} diff --git a/api/events/models/object_state_builder.go b/api/events/models/object_state_builder.go index f19ac42b..c1fee399 100644 --- a/api/events/models/object_state_builder.go +++ b/api/events/models/object_state_builder.go @@ -2,12 +2,17 @@ package models // ObjectStateBuilder Build ObjectState DTOs type ObjectStateBuilder interface { + // WithPodState sets the PodState WithPodState(*PodState) ObjectStateBuilder + // WithIngress sets the IngressRules + WithIngress(rules []IngressRule) ObjectStateBuilder + // Build the ObjectState Build() *ObjectState } type objectStateBuilder struct { - podState *PodState + podState *PodState + ingressRules []IngressRule } // NewObjectStateBuilder Constructor for objectStateBuilder @@ -20,8 +25,14 @@ func (b *objectStateBuilder) WithPodState(v *PodState) ObjectStateBuilder { return b } +func (b *objectStateBuilder) WithIngress(rules []IngressRule) ObjectStateBuilder { + b.ingressRules = rules + return b +} + func (b *objectStateBuilder) Build() *ObjectState { return &ObjectState{ - Pod: b.podState, + Pod: b.podState, + IngressRules: b.ingressRules, } } diff --git a/api/models/environment_configuration_status.go b/api/models/environment_configuration_status.go index 7464f245..1299fad5 100644 --- a/api/models/environment_configuration_status.go +++ b/api/models/environment_configuration_status.go @@ -9,7 +9,7 @@ func getEnvironmentConfigurationStatus(re *radixv1.RadixEnvironment) environment switch { case re == nil: return environmentModels.Pending - case re.Status.Orphaned: + case re.Status.Orphaned || re.Status.OrphanedTimestamp != nil: return environmentModels.Orphan case re.Status.Reconciled.IsZero(): return environmentModels.Pending diff --git a/api/utils/predicate/radix.go b/api/utils/predicate/radix.go index 251dc45c..b678ef1c 100644 --- a/api/utils/predicate/radix.go +++ b/api/utils/predicate/radix.go @@ -11,7 +11,7 @@ func IsNotOrphanEnvironment(re radixv1.RadixEnvironment) bool { } func IsOrphanEnvironment(re radixv1.RadixEnvironment) bool { - return re.Status.Orphaned + return re.Status.Orphaned || re.Status.OrphanedTimestamp != nil } func IsBatchJobStatusForBatchJob(job radixv1.RadixBatchJob) func(jobStatus radixv1.RadixBatchJobStatus) bool { diff --git a/api/utils/predicate/radix_test.go b/api/utils/predicate/radix_test.go index de9866a8..55b3f6bd 100644 --- a/api/utils/predicate/radix_test.go +++ b/api/utils/predicate/radix_test.go @@ -3,9 +3,10 @@ package predicate import ( "testing" + "github.com/equinor/radix-common/utils/pointers" radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" "github.com/stretchr/testify/assert" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func Test_IsActiveRadixDeployment(t *testing.T) { @@ -16,14 +17,14 @@ func Test_IsActiveRadixDeployment(t *testing.T) { func Test_IsNotOrphanEnvironment(t *testing.T) { assert.True(t, IsNotOrphanEnvironment(radixv1.RadixEnvironment{})) - assert.True(t, IsNotOrphanEnvironment(radixv1.RadixEnvironment{Status: radixv1.RadixEnvironmentStatus{Orphaned: false}})) - assert.False(t, IsNotOrphanEnvironment(radixv1.RadixEnvironment{Status: radixv1.RadixEnvironmentStatus{Orphaned: true}})) + assert.True(t, IsNotOrphanEnvironment(radixv1.RadixEnvironment{Status: radixv1.RadixEnvironmentStatus{Orphaned: false, OrphanedTimestamp: nil}})) + assert.False(t, IsNotOrphanEnvironment(radixv1.RadixEnvironment{Status: radixv1.RadixEnvironmentStatus{Orphaned: true, OrphanedTimestamp: pointers.Ptr(metav1.Now())}})) } func Test_IsOrphanEnvironment(t *testing.T) { - assert.True(t, IsOrphanEnvironment(radixv1.RadixEnvironment{Status: radixv1.RadixEnvironmentStatus{Orphaned: true}})) + assert.True(t, IsOrphanEnvironment(radixv1.RadixEnvironment{Status: radixv1.RadixEnvironmentStatus{Orphaned: true, OrphanedTimestamp: pointers.Ptr(metav1.Now())}})) assert.False(t, IsOrphanEnvironment(radixv1.RadixEnvironment{})) - assert.False(t, IsOrphanEnvironment(radixv1.RadixEnvironment{Status: radixv1.RadixEnvironmentStatus{Orphaned: false}})) + assert.False(t, IsOrphanEnvironment(radixv1.RadixEnvironment{Status: radixv1.RadixEnvironmentStatus{Orphaned: false, OrphanedTimestamp: nil}})) } func Test_IsBatchJobStatusForBatchJob(t *testing.T) { @@ -40,7 +41,7 @@ func Test_IsBatchJobWithName(t *testing.T) { func Test_IsRadixDeploymentForRadixBatch(t *testing.T) { batch := &radixv1.RadixBatch{ - ObjectMeta: v1.ObjectMeta{Namespace: "batchns"}, + ObjectMeta: metav1.ObjectMeta{Namespace: "batchns"}, Spec: radixv1.RadixBatchSpec{ RadixDeploymentJobRef: radixv1.RadixDeploymentJobComponentSelector{ LocalObjectReference: radixv1.LocalObjectReference{Name: "deployname"}, @@ -48,21 +49,21 @@ func Test_IsRadixDeploymentForRadixBatch(t *testing.T) { }, } sut := IsRadixDeploymentForRadixBatch(batch) - assert.True(t, sut(radixv1.RadixDeployment{ObjectMeta: v1.ObjectMeta{ + assert.True(t, sut(radixv1.RadixDeployment{ObjectMeta: metav1.ObjectMeta{ Name: "deployname", Namespace: "batchns", }})) - assert.False(t, sut(radixv1.RadixDeployment{ObjectMeta: v1.ObjectMeta{ + assert.False(t, sut(radixv1.RadixDeployment{ObjectMeta: metav1.ObjectMeta{ Name: "otherdeployname", Namespace: "batchns", }})) - assert.False(t, sut(radixv1.RadixDeployment{ObjectMeta: v1.ObjectMeta{ + assert.False(t, sut(radixv1.RadixDeployment{ObjectMeta: metav1.ObjectMeta{ Name: "deployname", Namespace: "otherbatchns", }})) sut = IsRadixDeploymentForRadixBatch(nil) - assert.False(t, sut(radixv1.RadixDeployment{ObjectMeta: v1.ObjectMeta{ + assert.False(t, sut(radixv1.RadixDeployment{ObjectMeta: metav1.ObjectMeta{ Name: "anydeployname", Namespace: "anybatchns", }})) diff --git a/go.mod b/go.mod index dc8ecf28..ca7f705a 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/cert-manager/cert-manager v1.15.0 github.com/equinor/radix-common v1.9.5 github.com/equinor/radix-job-scheduler v1.11.0 - github.com/equinor/radix-operator v1.62.0 + github.com/equinor/radix-operator v1.64.0 github.com/evanphx/json-patch/v5 v5.9.0 github.com/felixge/httpsnoop v1.0.4 github.com/golang-jwt/jwt/v5 v5.2.1 @@ -18,7 +18,6 @@ require ( github.com/gorilla/mux v1.8.1 github.com/kedacore/keda/v2 v2.15.1 github.com/kelseyhightower/envconfig v1.4.0 - github.com/marstr/guid v1.1.0 github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 github.com/prometheus-operator/prometheus-operator/pkg/client v0.76.0 github.com/prometheus/client_golang v1.20.3 diff --git a/go.sum b/go.sum index 9ed82c22..1c01ef61 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,8 @@ github.com/equinor/radix-common v1.9.5 h1:p1xldkYUoavwIMguoxxOyVkOXLPA6K8qMsgzez github.com/equinor/radix-common v1.9.5/go.mod h1:+g0Wj0D40zz29DjNkYKVmCVeYy4OsFWKI7Qi9rA6kpY= github.com/equinor/radix-job-scheduler v1.11.0 h1:8wCmXOVl/1cto8q2WJQEE06Cw68/QmfoifYVR49vzkY= github.com/equinor/radix-job-scheduler v1.11.0/go.mod h1:yPXn3kDcMY0Z3kBkosjuefsdY1x2g0NlBeybMmHz5hc= -github.com/equinor/radix-operator v1.62.0 h1:lurDVymrDhlyopd46KMV28eUltrVUPCk3bnBRFuyCsU= -github.com/equinor/radix-operator v1.62.0/go.mod h1:uRW9SgVZ94hkpq87npVv2YVviRuXNJ1zgCleya1uvr8= +github.com/equinor/radix-operator v1.64.0 h1:KbP0ZAX8zZSNzgejc7oOo087RkdrlTpBg4myk7zs48o= +github.com/equinor/radix-operator v1.64.0/go.mod h1:uRW9SgVZ94hkpq87npVv2YVviRuXNJ1zgCleya1uvr8= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= @@ -266,8 +266,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/marstr/guid v1.1.0 h1:/M4H/1G4avsieL6BbUwCOBzulmoeKVP5ux/3mQNnbyI= -github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= diff --git a/swaggerui/html/swagger.json b/swaggerui/html/swagger.json index 8ceb289f..be17b4fb 100644 --- a/swaggerui/html/swagger.json +++ b/swaggerui/html/swagger.json @@ -2642,6 +2642,135 @@ } } }, + "/applications/{appName}/environments/{envName}/events/components/{componentName}": { + "get": { + "tags": [ + "environment" + ], + "summary": "Lists events for an application environment", + "operationId": "getComponentEvents", + "parameters": [ + { + "type": "string", + "description": "name of Radix application", + "name": "appName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of environment", + "name": "envName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Name of component", + "name": "componentName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Works only with custom setup of cluster. Allow impersonation of test users (Required if Impersonate-Group is set)", + "name": "Impersonate-User", + "in": "header" + }, + { + "type": "string", + "description": "Works only with custom setup of cluster. Allow impersonation of a comma-seperated list of test groups (Required if Impersonate-User is set)", + "name": "Impersonate-Group", + "in": "header" + } + ], + "responses": { + "200": { + "description": "Successful get environment events", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Event" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/applications/{appName}/environments/{envName}/events/components/{componentName}/replicas/{podName}": { + "get": { + "tags": [ + "environment" + ], + "summary": "Lists events for an application environment", + "operationId": "getReplicaEvents", + "parameters": [ + { + "type": "string", + "description": "name of Radix application", + "name": "appName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of environment", + "name": "envName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Name of component", + "name": "componentName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Name of pod", + "name": "podName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Works only with custom setup of cluster. Allow impersonation of test users (Required if Impersonate-Group is set)", + "name": "Impersonate-User", + "in": "header" + }, + { + "type": "string", + "description": "Works only with custom setup of cluster. Allow impersonation of a comma-seperated list of test groups (Required if Impersonate-User is set)", + "name": "Impersonate-Group", + "in": "header" + } + ], + "responses": { + "200": { + "description": "Successful get environment events", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Event" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Not found" + } + } + } + }, "/applications/{appName}/environments/{envName}/jobcomponents/{jobComponentName}/batches": { "get": { "tags": [ @@ -6760,6 +6889,34 @@ }, "x-go-package": "github.com/equinor/radix-api/api/deployments/models" }, + "IngressRule": { + "description": "IngressRule specs", + "type": "object", + "properties": { + "host": { + "description": "The host name of the ingress", + "type": "string", + "x-go-name": "Host" + }, + "path": { + "description": "The path of the ingress", + "type": "string", + "x-go-name": "Path" + }, + "port": { + "description": "The port of the ingress", + "type": "integer", + "format": "int32", + "x-go-name": "Port" + }, + "service": { + "description": "The service name of the ingress", + "type": "string", + "x-go-name": "Service" + } + }, + "x-go-package": "github.com/equinor/radix-api/api/events/models" + }, "Job": { "description": "Job holds general information about job", "type": "object", @@ -7073,6 +7230,14 @@ "description": "ObjectState holds information about the state of objects involved in an event", "type": "object", "properties": { + "ingressRules": { + "description": "Details about the ingress rules for an ingress related event", + "type": "array", + "items": { + "$ref": "#/definitions/IngressRule" + }, + "x-go-name": "IngressRules" + }, "pod": { "$ref": "#/definitions/PodState" }