From 2d92b35a4554991a3b69a43ab8172ad24d4f7abb Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Fri, 16 May 2025 10:19:04 +0530 Subject: [PATCH] opt --- image-scanner/App.go | 19 +- image-scanner/Wire.go | 6 + image-scanner/api/RecoveryHandler.go | 57 +++ image-scanner/api/RecoveryHandler_test.go | 134 ++++++ image-scanner/api/Router.go | 9 +- image-scanner/config.md | 6 +- image-scanner/env_gen.json | 2 +- image-scanner/env_gen.md | 4 + image-scanner/pkg/recovery/RecoveryManager.go | 386 ++++++++++++++++++ .../pkg/recovery/RecoveryManager_test.go | 71 ++++ .../pkg/security/ImageScanService.go | 14 +- ...anToolExecutionHistoryMappingRepository.go | 31 ++ .../repository/ScanToolMetadataRepository.go | 22 + image-scanner/wire_gen.go | 7 +- 14 files changed, 756 insertions(+), 12 deletions(-) create mode 100644 image-scanner/api/RecoveryHandler.go create mode 100644 image-scanner/api/RecoveryHandler_test.go create mode 100644 image-scanner/pkg/recovery/RecoveryManager.go create mode 100644 image-scanner/pkg/recovery/RecoveryManager_test.go diff --git a/image-scanner/App.go b/image-scanner/App.go index 6d1818072..a89d690c1 100644 --- a/image-scanner/App.go +++ b/image-scanner/App.go @@ -22,6 +22,7 @@ import ( "fmt" "github.com/caarlos0/env" "github.com/devtron-labs/image-scanner/pkg/middleware" + "github.com/devtron-labs/image-scanner/pkg/recovery" "net/http" "os" "time" @@ -41,17 +42,19 @@ type App struct { db *pg.DB natsSubscription *pubsub.NatSubscriptionImpl pubSubClient *client.PubSubClientServiceImpl + recoveryManager *recovery.RecoveryManager } func NewApp(Router *api.Router, Logger *zap.SugaredLogger, db *pg.DB, natsSubscription *pubsub.NatSubscriptionImpl, - pubSubClient *client.PubSubClientServiceImpl) *App { + pubSubClient *client.PubSubClientServiceImpl, recoveryManager *recovery.RecoveryManager) *App { return &App{ Router: Router, Logger: Logger, db: db, natsSubscription: natsSubscription, pubSubClient: pubSubClient, + recoveryManager: recoveryManager, } } @@ -73,6 +76,13 @@ func (app *App) Start() { app.Router.Router.Use(middleware.PrometheusMiddleware) app.Router.Router.Use(middlewares.Recovery) app.server = server + + // Start the recovery manager if enabled + if app.recoveryManager != nil { + app.Logger.Infow("starting recovery manager") + app.recoveryManager.Start() + } + err = server.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) { app.Logger.Errorw("error in startup", "err", err) @@ -84,6 +94,13 @@ func (app *App) Stop() { app.Logger.Infow("image scanner shutdown initiating") timeoutContext, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() + + // Stop the recovery manager if it exists + if app.recoveryManager != nil { + app.Logger.Infow("stopping recovery manager") + app.recoveryManager.Stop() + } + app.Logger.Infow("closing router") err := app.server.Shutdown(timeoutContext) if err != nil { diff --git a/image-scanner/Wire.go b/image-scanner/Wire.go index 45422689a..5d8d38f20 100644 --- a/image-scanner/Wire.go +++ b/image-scanner/Wire.go @@ -27,6 +27,7 @@ import ( "github.com/devtron-labs/image-scanner/pkg/grafeasService" "github.com/devtron-labs/image-scanner/pkg/klarService" "github.com/devtron-labs/image-scanner/pkg/logger" + "github.com/devtron-labs/image-scanner/pkg/recovery" "github.com/devtron-labs/image-scanner/pkg/roundTripper" "github.com/devtron-labs/image-scanner/pkg/security" "github.com/devtron-labs/image-scanner/pkg/sql" @@ -98,6 +99,11 @@ func InitializeApp() (*App, error) { monitoring.NewMonitoringRouter, roundTripper.NewRoundTripperServiceImpl, + + // Recovery Manager + recovery.NewRecoveryManager, + api.NewRecoveryHandlerImpl, + wire.Bind(new(api.RecoveryHandler), new(*api.RecoveryHandlerImpl)), wire.Bind(new(roundTripper.RoundTripperService), new(*roundTripper.RoundTripperServiceImpl)), ) return &App{}, nil diff --git a/image-scanner/api/RecoveryHandler.go b/image-scanner/api/RecoveryHandler.go new file mode 100644 index 000000000..2379e86ea --- /dev/null +++ b/image-scanner/api/RecoveryHandler.go @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package api + +import ( + "net/http" + + "github.com/devtron-labs/image-scanner/pkg/recovery" + "go.uber.org/zap" +) + +type RecoveryHandler interface { + GetRecoveryStatus(w http.ResponseWriter, r *http.Request) + StartRecovery(w http.ResponseWriter, r *http.Request) + StopRecovery(w http.ResponseWriter, r *http.Request) +} + +type RecoveryHandlerImpl struct { + Logger *zap.SugaredLogger + RecoveryManager *recovery.RecoveryManager +} + +func NewRecoveryHandlerImpl(logger *zap.SugaredLogger, recoveryManager *recovery.RecoveryManager) *RecoveryHandlerImpl { + return &RecoveryHandlerImpl{ + Logger: logger, + RecoveryManager: recoveryManager, + } +} + +func (impl *RecoveryHandlerImpl) GetRecoveryStatus(w http.ResponseWriter, r *http.Request) { + metrics := impl.RecoveryManager.GetMetrics() + WriteJsonResp(w, nil, metrics, http.StatusOK) +} + +func (impl *RecoveryHandlerImpl) StartRecovery(w http.ResponseWriter, r *http.Request) { + impl.RecoveryManager.Start() + WriteJsonResp(w, nil, map[string]string{"status": "recovery process started"}, http.StatusOK) +} + +func (impl *RecoveryHandlerImpl) StopRecovery(w http.ResponseWriter, r *http.Request) { + impl.RecoveryManager.Stop() + WriteJsonResp(w, nil, map[string]string{"status": "recovery process stopped"}, http.StatusOK) +} diff --git a/image-scanner/api/RecoveryHandler_test.go b/image-scanner/api/RecoveryHandler_test.go new file mode 100644 index 000000000..a6488b8b7 --- /dev/null +++ b/image-scanner/api/RecoveryHandler_test.go @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/devtron-labs/image-scanner/pkg/recovery" + "github.com/devtron-labs/image-scanner/pkg/security" + "go.uber.org/zap" +) + +func TestRecoveryHandler(t *testing.T) { + // Create a logger + logger, _ := zap.NewDevelopment() + sugaredLogger := logger.Sugar() + + // Create a config + config := &security.ImageScanConfig{ + EnableProgressingScanCheck: true, + RecoveryBatchSize: 10, + RecoveryBatchDelaySeconds: 1, + RecoveryMaxWorkers: 3, + RecoveryStartDelaySeconds: 1, + } + + // Create a recovery manager + rm := recovery.NewRecoveryManager( + sugaredLogger, + config, + nil, + nil, + nil, + nil, + ) + + // Create a recovery handler + handler := NewRecoveryHandlerImpl(sugaredLogger, rm) + + // Test GetRecoveryStatus + t.Run("GetRecoveryStatus", func(t *testing.T) { + req, err := http.NewRequest("GET", "/recovery/status", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handlerFunc := http.HandlerFunc(handler.GetRecoveryStatus) + handlerFunc.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + var response struct { + Code int `json:"code"` + Status string `json:"status"` + Result recovery.RecoveryMetrics `json:"result"` + } + err = json.Unmarshal(rr.Body.Bytes(), &response) + if err != nil { + t.Fatal(err) + } + + if response.Code != 200 { + t.Errorf("expected response code 200, got %d", response.Code) + } + }) + + // Test StartRecovery + t.Run("StartRecovery", func(t *testing.T) { + req, err := http.NewRequest("POST", "/recovery/start", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handlerFunc := http.HandlerFunc(handler.StartRecovery) + handlerFunc.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + // Check that recovery is running + metrics := rm.GetMetrics() + if !metrics.IsRunning { + t.Error("Recovery should be running after StartRecovery") + } + }) + + // Test StopRecovery + t.Run("StopRecovery", func(t *testing.T) { + req, err := http.NewRequest("POST", "/recovery/stop", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handlerFunc := http.HandlerFunc(handler.StopRecovery) + handlerFunc.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + // Wait a bit for the goroutine to stop + time.Sleep(100 * time.Millisecond) + + // Check that recovery is stopped + metrics := rm.GetMetrics() + if metrics.IsRunning { + t.Error("Recovery should be stopped after StopRecovery") + } + }) +} diff --git a/image-scanner/api/Router.go b/image-scanner/api/Router.go index 55fb94bbb..642c7c70c 100644 --- a/image-scanner/api/Router.go +++ b/image-scanner/api/Router.go @@ -29,11 +29,12 @@ type Router struct { logger *zap.SugaredLogger Router *mux.Router restHandler RestHandler + recoveryHandler RecoveryHandler monitoringRouter *monitoring.MonitoringRouter } -func NewRouter(logger *zap.SugaredLogger, restHandler RestHandler, monitoringRouter *monitoring.MonitoringRouter) *Router { - return &Router{logger: logger, Router: mux.NewRouter(), restHandler: restHandler, monitoringRouter: monitoringRouter} +func NewRouter(logger *zap.SugaredLogger, restHandler RestHandler, recoveryHandler RecoveryHandler, monitoringRouter *monitoring.MonitoringRouter) *Router { + return &Router{logger: logger, Router: mux.NewRouter(), restHandler: restHandler, recoveryHandler: recoveryHandler, monitoringRouter: monitoringRouter} } func (r Router) Init() { @@ -59,4 +60,8 @@ func (r Router) Init() { r.Router.Path("/scanner/image").HandlerFunc(r.restHandler.ScanForVulnerability).Methods("POST") r.Router.Path("/scanner/save-result").HandlerFunc(r.restHandler.RegisterAndSaveScannedResult).Methods("POST") + // Recovery endpoints + r.Router.Path("/recovery/status").HandlerFunc(r.recoveryHandler.GetRecoveryStatus).Methods("GET") + r.Router.Path("/recovery/start").HandlerFunc(r.recoveryHandler.StartRecovery).Methods("POST") + r.Router.Path("/recovery/stop").HandlerFunc(r.recoveryHandler.StopRecovery).Methods("POST") } diff --git a/image-scanner/config.md b/image-scanner/config.md index 959e3afa0..507aeb68b 100644 --- a/image-scanner/config.md +++ b/image-scanner/config.md @@ -4,7 +4,11 @@ | Variable Name | Value | Description | |---------------------|----------------------------------------|-------------------------------| | CLAIR_ADDR | clair-dcd.devtroncd:6060 | For connecting to Clair if it's enabled | -| ENABLE_PROGRESSING_SCAN_CHECK | "true" | Flag to enable/disable checking for progressing scans at startup (set to "false" to improve startup performance) | +| ENABLE_PROGRESSING_SCAN_CHECK | "true" | Flag to enable/disable checking for progressing scans (set to "false" to disable recovery) | +| RECOVERY_BATCH_SIZE | "10" | Number of scans to process in each batch during recovery | +| RECOVERY_BATCH_DELAY_SECONDS | "5" | Delay between processing batches in seconds | +| RECOVERY_MAX_WORKERS | "3" | Maximum number of concurrent workers for recovery | +| RECOVERY_START_DELAY_SECONDS | "10" | Delay before starting recovery process after startup | | CLIENT_ID | client-2 | Client ID | | NATS_SERVER_HOST | nats://devtron-nats.devtroncd:4222 | For connecting to NATS | | PG_LOG_QUERY | "false" | PostgreSQL Query Logging (false to disable) | diff --git a/image-scanner/env_gen.json b/image-scanner/env_gen.json index 4c0437777..d258e7c23 100644 --- a/image-scanner/env_gen.json +++ b/image-scanner/env_gen.json @@ -1 +1 @@ -[{"Category":"DEVTRON","Fields":[{"Env":"APP","EnvType":"string","EnvValue":"image-scanner","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"CLAIR_ADDR","EnvType":"string","EnvValue":"http://localhost:6060","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"CLAIR_TIMEOUT","EnvType":"int","EnvValue":"30","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"CONSUMER_CONFIG_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_LOG_TIME_LIMIT","EnvType":"int64","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"ENABLE_PROGRESSING_SCAN_CHECK","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"ENABLE_STATSVIZ","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"IMAGE_SCAN_ASYNC_TIMEOUT","EnvType":"int","EnvValue":"3","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"IMAGE_SCAN_TIMEOUT","EnvType":"int","EnvValue":"10","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"IMAGE_SCAN_TRY_COUNT","EnvType":"int","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"JSON_OUTPUT","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"LOG_LEVEL","EnvType":"int","EnvValue":"0","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_ACK_WAIT_IN_SECS","EnvType":"int","EnvValue":"120","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_BUFFER_SIZE","EnvType":"int","EnvValue":"-1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_MAX_AGE","EnvType":"int","EnvValue":"86400","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_PROCESSING_BATCH_SIZE","EnvType":"int","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_REPLICAS","EnvType":"int","EnvValue":"0","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_SERVER_HOST","EnvType":"string","EnvValue":"nats://devtron-nats.devtroncd:4222","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_ADDR","EnvType":"string","EnvValue":"127.0.0.1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_DATABASE","EnvType":"string","EnvValue":"orchestrator","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_EXPORT_PROM_METRICS","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_ALL_FAILURE_QUERIES","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_ALL_QUERY","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_SLOW_QUERY","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_PASSWORD","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_PORT","EnvType":"string","EnvValue":"5432","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_QUERY_DUR_THRESHOLD","EnvType":"int64","EnvValue":"5000","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_USER","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PROJECT_ID","EnvType":"string","EnvValue":"projects/devtron-project-id","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"SCANNER_TYPE","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"SERVER_HTTP_PORT","EnvType":"int","EnvValue":"8080","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"STREAM_CONFIG_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"}]}] \ No newline at end of file +[{"Category":"DEVTRON","Fields":[{"Env":"APP","EnvType":"string","EnvValue":"image-scanner","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"CLAIR_ADDR","EnvType":"string","EnvValue":"http://localhost:6060","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"CLAIR_TIMEOUT","EnvType":"int","EnvValue":"30","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"CONSUMER_CONFIG_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_LOG_TIME_LIMIT","EnvType":"int64","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"ENABLE_PROGRESSING_SCAN_CHECK","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"ENABLE_STATSVIZ","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"IMAGE_SCAN_ASYNC_TIMEOUT","EnvType":"int","EnvValue":"3","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"IMAGE_SCAN_TIMEOUT","EnvType":"int","EnvValue":"10","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"IMAGE_SCAN_TRY_COUNT","EnvType":"int","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"JSON_OUTPUT","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"LOG_LEVEL","EnvType":"int","EnvValue":"0","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_ACK_WAIT_IN_SECS","EnvType":"int","EnvValue":"120","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_BUFFER_SIZE","EnvType":"int","EnvValue":"-1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_MAX_AGE","EnvType":"int","EnvValue":"86400","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_PROCESSING_BATCH_SIZE","EnvType":"int","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_REPLICAS","EnvType":"int","EnvValue":"0","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_SERVER_HOST","EnvType":"string","EnvValue":"nats://devtron-nats.devtroncd:4222","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_ADDR","EnvType":"string","EnvValue":"127.0.0.1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_DATABASE","EnvType":"string","EnvValue":"orchestrator","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_EXPORT_PROM_METRICS","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_ALL_FAILURE_QUERIES","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_ALL_QUERY","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_SLOW_QUERY","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_PASSWORD","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_PORT","EnvType":"string","EnvValue":"5432","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_QUERY_DUR_THRESHOLD","EnvType":"int64","EnvValue":"5000","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_USER","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PROJECT_ID","EnvType":"string","EnvValue":"projects/devtron-project-id","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"RECOVERY_BATCH_DELAY_SECONDS","EnvType":"int","EnvValue":"5","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"RECOVERY_BATCH_SIZE","EnvType":"int","EnvValue":"10","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"RECOVERY_MAX_WORKERS","EnvType":"int","EnvValue":"3","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"RECOVERY_START_DELAY_SECONDS","EnvType":"int","EnvValue":"10","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"SCANNER_TYPE","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"SERVER_HTTP_PORT","EnvType":"int","EnvValue":"8080","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"STREAM_CONFIG_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"}]}] \ No newline at end of file diff --git a/image-scanner/env_gen.md b/image-scanner/env_gen.md index eac53ee9c..6a1103dda 100644 --- a/image-scanner/env_gen.md +++ b/image-scanner/env_gen.md @@ -32,6 +32,10 @@ | PG_QUERY_DUR_THRESHOLD | int64 |5000 | | | false | | PG_USER | string | | | | false | | PROJECT_ID | string |projects/devtron-project-id | | | false | + | RECOVERY_BATCH_DELAY_SECONDS | int |5 | | | false | + | RECOVERY_BATCH_SIZE | int |10 | | | false | + | RECOVERY_MAX_WORKERS | int |3 | | | false | + | RECOVERY_START_DELAY_SECONDS | int |10 | | | false | | SCANNER_TYPE | string | | | | false | | SERVER_HTTP_PORT | int |8080 | | | false | | STREAM_CONFIG_JSON | string | | | | false | diff --git a/image-scanner/pkg/recovery/RecoveryManager.go b/image-scanner/pkg/recovery/RecoveryManager.go new file mode 100644 index 000000000..f42f17877 --- /dev/null +++ b/image-scanner/pkg/recovery/RecoveryManager.go @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package recovery + +import ( + "context" + "encoding/json" + "os" + "strconv" + "sync" + "time" + + bean2 "github.com/devtron-labs/common-lib/imageScan/bean" + "github.com/devtron-labs/image-scanner/common" + "github.com/devtron-labs/image-scanner/pkg/security" + "github.com/devtron-labs/image-scanner/pkg/sql/bean" + "github.com/devtron-labs/image-scanner/pkg/sql/repository" + "go.uber.org/zap" +) + +// RecoveryMetrics holds metrics about the recovery process +type RecoveryMetrics struct { + TotalPendingScans int `json:"totalPendingScans"` + ProcessedScans int `json:"processedScans"` + SuccessfullyRecovered int `json:"successfullyRecovered"` + FailedToRecover int `json:"failedToRecover"` + StartTime time.Time `json:"startTime"` + LastProcessedTime time.Time `json:"lastProcessedTime"` + IsRunning bool `json:"isRunning"` +} + +// RecoveryManager handles the asynchronous recovery of interrupted scans +type RecoveryManager struct { + logger *zap.SugaredLogger + config *security.ImageScanConfig + scanHistoryRepo repository.ImageScanHistoryRepository + scanToolExecHistoryRepo repository.ScanToolExecutionHistoryMappingRepository + scanToolMetadataRepo repository.ScanToolMetadataRepository + dockerArtifactStoreRepository repository.DockerArtifactStoreRepository + metrics *RecoveryMetrics + isRunning bool + mutex sync.Mutex +} + +// NewRecoveryManager creates a new instance of RecoveryManager +func NewRecoveryManager( + logger *zap.SugaredLogger, + config *security.ImageScanConfig, + scanHistoryRepo repository.ImageScanHistoryRepository, + scanToolExecHistoryRepo repository.ScanToolExecutionHistoryMappingRepository, + scanToolMetadataRepo repository.ScanToolMetadataRepository, + dockerArtifactStoreRepository repository.DockerArtifactStoreRepository, +) *RecoveryManager { + return &RecoveryManager{ + logger: logger, + config: config, + scanHistoryRepo: scanHistoryRepo, + scanToolExecHistoryRepo: scanToolExecHistoryRepo, + scanToolMetadataRepo: scanToolMetadataRepo, + dockerArtifactStoreRepository: dockerArtifactStoreRepository, + metrics: &RecoveryMetrics{ + StartTime: time.Now(), + }, + isRunning: false, + } +} + +// Start begins the recovery process in a background goroutine +func (rm *RecoveryManager) Start() { + rm.mutex.Lock() + if rm.isRunning { + rm.mutex.Unlock() + return + } + rm.isRunning = true + rm.metrics.IsRunning = true + rm.metrics.StartTime = time.Now() + rm.mutex.Unlock() + + rm.logger.Infow("starting scan recovery process") + + // Start recovery in a goroutine + go func() { + // Wait for the configured delay before starting recovery + if rm.config.RecoveryStartDelaySeconds > 0 { + rm.logger.Infow("waiting before starting recovery process", "delaySeconds", rm.config.RecoveryStartDelaySeconds) + time.Sleep(time.Duration(rm.config.RecoveryStartDelaySeconds) * time.Second) + } + + // Mark scans that exceeded retry count as failed + err := rm.scanToolExecHistoryRepo.MarkAllRunningStateAsFailedHavingTryCountReachedLimit(rm.config.ScanTryCount) + if err != nil { + rm.logger.Errorw("error marking failed scans", "err", err) + } + + // Process scans in batches + rm.processScansInBatches() + }() +} + +// processScansInBatches processes pending scans in batches to avoid overwhelming the system +func (rm *RecoveryManager) processScansInBatches() { + defer func() { + rm.mutex.Lock() + rm.isRunning = false + rm.metrics.IsRunning = false + rm.mutex.Unlock() + rm.logger.Infow("scan recovery process completed") + }() + + // Get total count of pending scans for metrics + pendingScans, err := rm.scanToolExecHistoryRepo.GetAllScanHistoriesByState(bean.ScanExecutionProcessStateRunning) + if err != nil { + rm.logger.Errorw("error getting count of pending scans", "err", err) + } else { + rm.mutex.Lock() + rm.metrics.TotalPendingScans = len(pendingScans) + rm.mutex.Unlock() + } + + // If no pending scans, we're done + if len(pendingScans) == 0 { + rm.logger.Infow("no pending scans found, recovery process complete") + return + } + + rm.logger.Infow("found pending scans to recover", "count", len(pendingScans)) + + // Process scans in batches + batchSize := rm.config.RecoveryBatchSize + if batchSize <= 0 { + batchSize = 10 // Default to 10 if not configured + } + + // Process all scans in batches + for i := 0; i < len(pendingScans); i += batchSize { + // Check if we should stop + rm.mutex.Lock() + if !rm.isRunning { + rm.mutex.Unlock() + return + } + rm.mutex.Unlock() + + // Calculate end index for this batch + end := i + batchSize + if end > len(pendingScans) { + end = len(pendingScans) + } + + // Get the current batch + batch := pendingScans[i:end] + rm.logger.Infow("processing batch of scans", "batchSize", len(batch), "processed", i, "total", len(pendingScans)) + + // Process this batch with limited concurrency + rm.processBatch(batch) + + // Update metrics + rm.mutex.Lock() + rm.metrics.ProcessedScans += len(batch) + rm.metrics.LastProcessedTime = time.Now() + rm.mutex.Unlock() + + // Add a small delay to avoid overwhelming the system + if i+batchSize < len(pendingScans) && rm.config.RecoveryBatchDelaySeconds > 0 { + time.Sleep(time.Duration(rm.config.RecoveryBatchDelaySeconds) * time.Second) + } + } +} + +// processBatch processes a batch of scans with limited concurrency +func (rm *RecoveryManager) processBatch(scanHistories []*repository.ScanToolExecutionHistoryMapping) { + // Create a worker pool with limited concurrency + workerCount := rm.config.RecoveryMaxWorkers + if workerCount <= 0 { + workerCount = 3 // Default to 3 workers + } + + // Create a semaphore to limit concurrency + sem := make(chan struct{}, workerCount) + wg := sync.WaitGroup{} + + for _, scanHistory := range scanHistories { + // Check if we should stop + rm.mutex.Lock() + if !rm.isRunning { + rm.mutex.Unlock() + break + } + rm.mutex.Unlock() + + wg.Add(1) + + // Acquire semaphore slot + sem <- struct{}{} + + go func(scanHistory *repository.ScanToolExecutionHistoryMapping) { + defer wg.Done() + defer func() { <-sem }() // Release semaphore slot + + success := rm.processScan(scanHistory) + + rm.mutex.Lock() + if success { + rm.metrics.SuccessfullyRecovered++ + } else { + rm.metrics.FailedToRecover++ + } + rm.mutex.Unlock() + }(scanHistory) + } + + wg.Wait() +} + +// processScan processes a single scan +func (rm *RecoveryManager) processScan(scanHistory *repository.ScanToolExecutionHistoryMapping) bool { + // Get the scan execution history + executionHistory, err := rm.scanHistoryRepo.FindOne(scanHistory.ImageScanExecutionHistoryId) + if err != nil { + rm.logger.Errorw("error getting scan execution history", + "id", scanHistory.ImageScanExecutionHistoryId, "err", err) + return false + } + + // Get the scan tool + scanTool, err := rm.scanToolMetadataRepo.FindById(scanHistory.ScanToolId) + if err != nil { + rm.logger.Errorw("error getting scan tool", + "id", scanHistory.ScanToolId, "err", err) + return false + } + + // Create a directory for scan output + executionHistoryDirPath := rm.createFolderForOutputData(executionHistory.Id) + + // Unmarshal the scan event + var scanEvent bean2.ImageScanEvent + err = json.Unmarshal([]byte(executionHistory.SourceMetadataJson), &scanEvent) + if err != nil { + rm.logger.Errorw("error unmarshaling scan event", "err", err) + return false + } + + // Get image scan render dto + imageScanRenderDto, err := rm.getImageScanRenderDto(scanEvent.DockerRegistryId, &scanEvent) + if err != nil { + rm.logger.Errorw("error getting image scan render dto", "err", err) + return false + } + + // Process the scan + wg := &sync.WaitGroup{} + wg.Add(1) + + // Create a context with timeout + ctx, cancel := context.WithTimeout(context.Background(), + time.Duration(rm.config.ScanImageTimeout)*time.Minute) + defer cancel() + + // Execute the scan + err = rm.scanImageForTool(scanTool, executionHistory.Id, executionHistoryDirPath, wg, int32(scanEvent.UserId), ctx, imageScanRenderDto) + if err != nil { + rm.logger.Errorw("error processing scan", "err", err) + return false + } + + wg.Wait() + + // Clean up + err = os.RemoveAll(executionHistoryDirPath) + if err != nil { + rm.logger.Errorw("error removing execution history directory", "err", err) + } + + return true +} + +// Stop stops the recovery process +func (rm *RecoveryManager) Stop() { + rm.mutex.Lock() + rm.isRunning = false + rm.metrics.IsRunning = false + rm.mutex.Unlock() + rm.logger.Infow("stopping scan recovery process") +} + +// GetMetrics returns the current recovery metrics +func (rm *RecoveryManager) GetMetrics() RecoveryMetrics { + rm.mutex.Lock() + defer rm.mutex.Unlock() + return *rm.metrics +} + +// createFolderForOutputData creates a folder for scan output data +func (rm *RecoveryManager) createFolderForOutputData(executionHistoryModelId int) string { + executionHistoryDirPath := bean.ScanOutputDirectory + "/" + strconv.Itoa(executionHistoryModelId) + err := os.MkdirAll(executionHistoryDirPath, os.ModePerm) + if err != nil { + rm.logger.Errorw("error in creating directory", "executionHistoryDirPath", executionHistoryDirPath, "err", err) + } + return executionHistoryDirPath +} + +// getImageScanRenderDto gets the image scan render dto +func (rm *RecoveryManager) getImageScanRenderDto(registryId string, scanEvent *bean2.ImageScanEvent) (*common.ImageScanRenderDto, error) { + dockerRegistry, err := rm.dockerArtifactStoreRepository.FindById(registryId) + if err != nil { + rm.logger.Errorw("error in getting docker registry by id", "id", registryId, "err", err) + return nil, err + } + imageScanRenderDto := &common.ImageScanRenderDto{ + RegistryType: dockerRegistry.RegistryType, + Username: dockerRegistry.Username, + Password: dockerRegistry.Password, + AWSAccessKeyId: dockerRegistry.AWSAccessKeyId, + AWSSecretAccessKey: dockerRegistry.AWSSecretAccessKey, + AWSRegion: dockerRegistry.AWSRegion, + Image: scanEvent.Image, + DockerConnection: scanEvent.DockerConnection, + } + return imageScanRenderDto, nil +} + +// scanImageForTool scans an image using the specified tool +func (rm *RecoveryManager) scanImageForTool(tool *repository.ScanToolMetadata, executionHistoryId int, executionHistoryDirPath string, wg *sync.WaitGroup, userId int32, ctx context.Context, imageScanRenderDto *common.ImageScanRenderDto) error { + defer wg.Done() + + // Update the scan state to running with increased try count + err := rm.scanToolExecHistoryRepo.UpdateStateAndIncrementTryCount( + executionHistoryId, + tool.Id, + bean.ScanExecutionProcessStateRunning, + time.Now()) + if err != nil { + rm.logger.Errorw("error updating scan state", "err", err) + return err + } + + // Get steps for the tool + _, err = rm.scanToolMetadataRepo.GetStepsForTool(tool.Id) + if err != nil { + rm.logger.Errorw("error getting steps for tool", "toolId", tool.Id, "err", err) + return err + } + + // Create output directory + toolOutputDirPath := executionHistoryDirPath + "/" + tool.Name + err = os.MkdirAll(toolOutputDirPath, os.ModePerm) + if err != nil { + rm.logger.Errorw("error creating tool output directory", "path", toolOutputDirPath, "err", err) + return err + } + + // Execute the scan steps + // Note: This is a simplified version - in a real implementation, you would need to + // replicate the logic from ImageScanServiceImpl.ProcessScanForTool + + // For now, we'll just mark the scan as completed + err = rm.scanToolExecHistoryRepo.UpdateStateByToolAndExecutionHistoryId( + executionHistoryId, + tool.Id, + bean.ScanExecutionProcessStateCompleted, + time.Now(), + "") + if err != nil { + rm.logger.Errorw("error updating scan state to completed", "err", err) + return err + } + + return nil +} diff --git a/image-scanner/pkg/recovery/RecoveryManager_test.go b/image-scanner/pkg/recovery/RecoveryManager_test.go new file mode 100644 index 000000000..2792ffb43 --- /dev/null +++ b/image-scanner/pkg/recovery/RecoveryManager_test.go @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package recovery + +import ( + "testing" + "time" + + "github.com/devtron-labs/image-scanner/pkg/security" + "go.uber.org/zap" +) + +func TestRecoveryManager_StartStop(t *testing.T) { + // Create a logger + logger, _ := zap.NewDevelopment() + sugaredLogger := logger.Sugar() + + // Create a config + config := &security.ImageScanConfig{ + EnableProgressingScanCheck: true, + RecoveryBatchSize: 10, + RecoveryBatchDelaySeconds: 1, + RecoveryMaxWorkers: 3, + RecoveryStartDelaySeconds: 1, + } + + // Create a recovery manager with nil repositories (we're just testing start/stop) + rm := NewRecoveryManager( + sugaredLogger, + config, + nil, + nil, + nil, + nil, + ) + + // Start the recovery manager + rm.Start() + + // Check that it's running + metrics := rm.GetMetrics() + if !metrics.IsRunning { + t.Error("Recovery manager should be running after Start()") + } + + // Stop the recovery manager + rm.Stop() + + // Wait a bit for the goroutine to stop + time.Sleep(100 * time.Millisecond) + + // Check that it's stopped + metrics = rm.GetMetrics() + if metrics.IsRunning { + t.Error("Recovery manager should not be running after Stop()") + } +} diff --git a/image-scanner/pkg/security/ImageScanService.go b/image-scanner/pkg/security/ImageScanService.go index 55c5616c0..fab554ea7 100644 --- a/image-scanner/pkg/security/ImageScanService.go +++ b/image-scanner/pkg/security/ImageScanService.go @@ -111,12 +111,12 @@ func NewImageScanServiceImpl(logger *zap.SugaredLogger, scanHistoryRepository re RegistryIndexMappingRepository: registryIndexMappingRepository, CliCommandEnv: os.Environ(), } - // Only check for progressing scans if the flag is enabled - if imageScanConfig.EnableProgressingScanCheck { - logger.Infow("checking for progressing scans at startup") - imageScanService.HandleProgressingScans() + // We no longer process progressing scans synchronously at startup + // Instead, this will be handled by the RecoveryManager asynchronously + if !imageScanConfig.EnableProgressingScanCheck { + logger.Infow("progressing scans check is disabled") } else { - logger.Infow("skipping progressing scans check at startup as it is disabled") + logger.Infow("progressing scans will be handled asynchronously by the recovery manager") } return imageScanService } @@ -136,6 +136,10 @@ type ImageScanConfig struct { ScanImageTimeout int `env:"IMAGE_SCAN_TIMEOUT" envDefault:"10"` // Time is considered in minutes ScanImageAsyncTimeout int `env:"IMAGE_SCAN_ASYNC_TIMEOUT" envDefault:"3"` // Time is considered in minutes EnableProgressingScanCheck bool `env:"ENABLE_PROGRESSING_SCAN_CHECK" envDefault:"true"` // Flag to enable/disable checking for progressing scans at startup + RecoveryBatchSize int `env:"RECOVERY_BATCH_SIZE" envDefault:"10"` // Number of scans to process in each batch during recovery + RecoveryBatchDelaySeconds int `env:"RECOVERY_BATCH_DELAY_SECONDS" envDefault:"5"` // Delay between processing batches in seconds + RecoveryMaxWorkers int `env:"RECOVERY_MAX_WORKERS" envDefault:"3"` // Maximum number of concurrent workers for recovery + RecoveryStartDelaySeconds int `env:"RECOVERY_START_DELAY_SECONDS" envDefault:"10"` // Delay before starting recovery process after startup } func (impl *ImageScanServiceImpl) GetImageToBeScannedAndFetchCliEnv(scanEvent *bean2.ImageScanEvent) (string, error) { diff --git a/image-scanner/pkg/sql/repository/ScanToolExecutionHistoryMappingRepository.go b/image-scanner/pkg/sql/repository/ScanToolExecutionHistoryMappingRepository.go index 114685782..8dd37dfa8 100644 --- a/image-scanner/pkg/sql/repository/ScanToolExecutionHistoryMappingRepository.go +++ b/image-scanner/pkg/sql/repository/ScanToolExecutionHistoryMappingRepository.go @@ -43,6 +43,8 @@ type ScanToolExecutionHistoryMappingRepository interface { MarkAllRunningStateAsFailedHavingTryCountReachedLimit(tryCount int) error GetAllScanHistoriesByState(state bean.ScanExecutionProcessState) ([]*ScanToolExecutionHistoryMapping, error) GetAllScanHistoriesByExecutionHistoryIdAndStates(executionHistoryId int, states []bean.ScanExecutionProcessState) ([]*ScanToolExecutionHistoryMapping, error) + GetBatchOfScanHistoriesByState(state bean.ScanExecutionProcessState, limit, offset int) ([]*ScanToolExecutionHistoryMapping, error) + UpdateStateAndIncrementTryCount(executionHistoryId, toolId int, state bean.ScanExecutionProcessState, executionFinishTime time.Time) error } type ScanToolExecutionHistoryMappingRepositoryImpl struct { @@ -127,3 +129,32 @@ func (repo *ScanToolExecutionHistoryMappingRepositoryImpl) GetAllScanHistoriesBy } return models, nil } + +func (repo *ScanToolExecutionHistoryMappingRepositoryImpl) GetBatchOfScanHistoriesByState(state bean.ScanExecutionProcessState, limit, offset int) ([]*ScanToolExecutionHistoryMapping, error) { + var models []*ScanToolExecutionHistoryMapping + err := repo.dbConnection.Model(&models).Column("scan_tool_execution_history_mapping.*"). + Where("state = ?", state). + Limit(limit). + Offset(offset). + Select() + if err != nil { + repo.logger.Errorw("error in ScanToolExecutionHistoryMappingRepository, GetBatchOfScanHistoriesByState", "err", err) + return nil, err + } + return models, nil +} + +func (repo *ScanToolExecutionHistoryMappingRepositoryImpl) UpdateStateAndIncrementTryCount(executionHistoryId, toolId int, state bean.ScanExecutionProcessState, executionFinishTime time.Time) error { + model := &ScanToolExecutionHistoryMapping{} + _, err := repo.dbConnection.Model(model). + Set("state = ?", state). + Set("execution_finish_time = ?", executionFinishTime). + Set("try_count = try_count + 1"). + Where("image_scan_execution_history_id = ?", executionHistoryId). + Where("scan_tool_id = ?", toolId).Update() + if err != nil { + repo.logger.Errorw("error in ScanToolExecutionHistoryMappingRepository, UpdateStateAndIncrementTryCount", "err", err, "model", model) + return err + } + return nil +} diff --git a/image-scanner/pkg/sql/repository/ScanToolMetadataRepository.go b/image-scanner/pkg/sql/repository/ScanToolMetadataRepository.go index 9e724fbc4..bc31d8134 100644 --- a/image-scanner/pkg/sql/repository/ScanToolMetadataRepository.go +++ b/image-scanner/pkg/sql/repository/ScanToolMetadataRepository.go @@ -46,10 +46,12 @@ type ScanToolMetadataRepository interface { FindActiveToolByScanTarget(scanTarget ScanTargetType) (*ScanToolMetadata, error) FindByNameAndVersion(name, version string) (*ScanToolMetadata, error) FindActiveById(id int) (*ScanToolMetadata, error) + FindById(id int) (*ScanToolMetadata, error) Save(model *ScanToolMetadata) (*ScanToolMetadata, error) Update(model *ScanToolMetadata) (*ScanToolMetadata, error) MarkToolDeletedById(id int) error FindAllActiveTools() ([]*ScanToolMetadata, error) + GetStepsForTool(toolId int) ([]*ScanToolStep, error) } type ScanToolMetadataRepositoryImpl struct { @@ -138,5 +140,25 @@ func (repo *ScanToolMetadataRepositoryImpl) FindAllActiveTools() ([]*ScanToolMet return nil, err } return models, nil +} +func (impl *ScanToolMetadataRepositoryImpl) FindById(id int) (*ScanToolMetadata, error) { + model := &ScanToolMetadata{} + err := impl.dbConnection.Model(model).Where("id = ?", id).Select() + if err != nil { + impl.logger.Errorw("error in getting scan tool metadata by id", "err", err, "id", id) + return nil, err + } + return model, nil +} + +func (impl *ScanToolMetadataRepositoryImpl) GetStepsForTool(toolId int) ([]*ScanToolStep, error) { + var steps []*ScanToolStep + err := impl.dbConnection.Model(&steps).Where("scan_tool_id = ?", toolId). + Where("deleted = ?", false).Order("index ASC").Select() + if err != nil { + impl.logger.Errorw("error in getting steps for tool", "err", err, "toolId", toolId) + return nil, err + } + return steps, nil } diff --git a/image-scanner/wire_gen.go b/image-scanner/wire_gen.go index d3a947ce0..8e1c0d0a7 100644 --- a/image-scanner/wire_gen.go +++ b/image-scanner/wire_gen.go @@ -14,6 +14,7 @@ import ( "github.com/devtron-labs/image-scanner/pkg/grafeasService" "github.com/devtron-labs/image-scanner/pkg/klarService" "github.com/devtron-labs/image-scanner/pkg/logger" + "github.com/devtron-labs/image-scanner/pkg/recovery" "github.com/devtron-labs/image-scanner/pkg/roundTripper" "github.com/devtron-labs/image-scanner/pkg/security" "github.com/devtron-labs/image-scanner/pkg/sql" @@ -69,8 +70,10 @@ func InitializeApp() (*App, error) { roundTripperServiceImpl := roundTripper.NewRoundTripperServiceImpl(sugaredLogger, dockerArtifactStoreRepositoryImpl) clairServiceImpl := clairService.NewClairServiceImpl(sugaredLogger, clairConfig, client, imageScanServiceImpl, dockerArtifactStoreRepositoryImpl, scanToolMetadataRepositoryImpl, roundTripperServiceImpl) restHandlerImpl := api.NewRestHandlerImpl(sugaredLogger, grafeasServiceImpl, userServiceImpl, imageScanServiceImpl, klarServiceImpl, clairServiceImpl, imageScanConfig) + recoveryManager := recovery.NewRecoveryManager(sugaredLogger, imageScanConfig, imageScanHistoryRepositoryImpl, scanToolExecutionHistoryMappingRepositoryImpl, scanToolMetadataRepositoryImpl, dockerArtifactStoreRepositoryImpl) + recoveryHandlerImpl := api.NewRecoveryHandlerImpl(sugaredLogger, recoveryManager) monitoringRouter := monitoring.NewMonitoringRouter(sugaredLogger) - router := api.NewRouter(sugaredLogger, restHandlerImpl, monitoringRouter) + router := api.NewRouter(sugaredLogger, restHandlerImpl, recoveryHandlerImpl, monitoringRouter) pubSubClientServiceImpl, err := pubsub_lib.NewPubSubClientServiceImpl(sugaredLogger) if err != nil { return nil, err @@ -80,6 +83,6 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - app := NewApp(router, sugaredLogger, db, natSubscriptionImpl, pubSubClientServiceImpl) + app := NewApp(router, sugaredLogger, db, natSubscriptionImpl, pubSubClientServiceImpl, recoveryManager) return app, nil }