From e2048d0b5462855bccb3d5e5baf7e5673798348e Mon Sep 17 00:00:00 2001 From: Sam Betts <116557335+sambetts-cisco@users.noreply.github.com> Date: Mon, 6 Mar 2023 16:15:55 +0000 Subject: [PATCH] Add support for ODATA $filter, $select and $count on /scanConfigs (#84) * Redo database interface to use model as input and output This commit revisits the database interface such that each function takes in and returns a VMClarity API model object. How the DB driver decides to store these objects and process them efficently is the resposiblity of the DB driver. We start with one DB driver, gorm, which converts the model objects to GORM structs to save them. --- api/client/client.gen.go | 106 ++- api/models/models.gen.go | 33 +- api/openapi.yaml | 37 +- api/server/server.gen.go | 190 ++-- backend/cmd/backend/main.go | 4 +- backend/pkg/backend/backend.go | 14 +- backend/pkg/database/database.go | 111 +-- backend/pkg/database/gorm/database.go | 98 +++ .../convert.go => database/gorm/dbtorest.go} | 132 +-- backend/pkg/database/gorm/dbtorest_test.go | 171 ++++ backend/pkg/database/gorm/odata.go | 236 +++++ .../convert.go => database/gorm/resttodb.go} | 109 +-- .../gorm/resttodb_test.go} | 159 +--- backend/pkg/database/gorm/scan.go | 190 ++++ backend/pkg/database/gorm/scan_config.go | 256 ++++++ backend/pkg/database/gorm/scan_result.go | 166 ++++ backend/pkg/database/gorm/target.go | 176 ++++ backend/pkg/database/odatasql/query.go | 408 +++++++++ backend/pkg/database/odatasql/query_test.go | 831 ++++++++++++++++++ backend/pkg/database/odatasql/schema.go | 48 + backend/pkg/database/odatasql/selectTree.go | 114 +++ backend/pkg/database/scan.go | 164 ---- backend/pkg/database/scan_config.go | 153 ---- backend/pkg/database/scan_result.go | 158 ---- backend/pkg/database/target.go | 165 ---- backend/pkg/database/types/database.go | 91 ++ .../pkg/rest/convert/dbtorest/convert_test.go | 352 -------- backend/pkg/rest/convert/resttodb/params.go | 60 -- backend/pkg/rest/scan_config_controller.go | 121 +-- backend/pkg/rest/scan_controller.go | 80 +- backend/pkg/rest/scan_result_controller.go | 72 +- backend/pkg/rest/server.go | 6 +- backend/pkg/rest/target_controller.go | 62 +- go.mod | 11 +- go.sum | 32 +- 35 files changed, 3306 insertions(+), 1810 deletions(-) create mode 100644 backend/pkg/database/gorm/database.go rename backend/pkg/{rest/convert/dbtorest/convert.go => database/gorm/dbtorest.go} (51%) create mode 100644 backend/pkg/database/gorm/dbtorest_test.go create mode 100644 backend/pkg/database/gorm/odata.go rename backend/pkg/{rest/convert/resttodb/convert.go => database/gorm/resttodb.go} (51%) rename backend/pkg/{rest/convert/resttodb/convert_test.go => database/gorm/resttodb_test.go} (50%) create mode 100644 backend/pkg/database/gorm/scan.go create mode 100644 backend/pkg/database/gorm/scan_config.go create mode 100644 backend/pkg/database/gorm/scan_result.go create mode 100644 backend/pkg/database/gorm/target.go create mode 100644 backend/pkg/database/odatasql/query.go create mode 100644 backend/pkg/database/odatasql/query_test.go create mode 100644 backend/pkg/database/odatasql/schema.go create mode 100644 backend/pkg/database/odatasql/selectTree.go delete mode 100644 backend/pkg/database/scan.go delete mode 100644 backend/pkg/database/scan_config.go delete mode 100644 backend/pkg/database/scan_result.go delete mode 100644 backend/pkg/database/target.go create mode 100644 backend/pkg/database/types/database.go delete mode 100644 backend/pkg/rest/convert/dbtorest/convert_test.go delete mode 100644 backend/pkg/rest/convert/resttodb/params.go diff --git a/api/client/client.gen.go b/api/client/client.gen.go index e09ec674d..6905c2946 100644 --- a/api/client/client.gen.go +++ b/api/client/client.gen.go @@ -102,7 +102,7 @@ type ClientInterface interface { DeleteScanConfigsScanConfigID(ctx context.Context, scanConfigID ScanConfigID, reqEditors ...RequestEditorFn) (*http.Response, error) // GetScanConfigsScanConfigID request - GetScanConfigsScanConfigID(ctx context.Context, scanConfigID ScanConfigID, reqEditors ...RequestEditorFn) (*http.Response, error) + GetScanConfigsScanConfigID(ctx context.Context, scanConfigID ScanConfigID, params *GetScanConfigsScanConfigIDParams, reqEditors ...RequestEditorFn) (*http.Response, error) // PatchScanConfigsScanConfigID request with any body PatchScanConfigsScanConfigIDWithBody(ctx context.Context, scanConfigID ScanConfigID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -227,8 +227,8 @@ func (c *Client) DeleteScanConfigsScanConfigID(ctx context.Context, scanConfigID return c.Client.Do(req) } -func (c *Client) GetScanConfigsScanConfigID(ctx context.Context, scanConfigID ScanConfigID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetScanConfigsScanConfigIDRequest(c.Server, scanConfigID) +func (c *Client) GetScanConfigsScanConfigID(ctx context.Context, scanConfigID ScanConfigID, params *GetScanConfigsScanConfigIDParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetScanConfigsScanConfigIDRequest(c.Server, scanConfigID, params) if err != nil { return nil, err } @@ -612,9 +612,9 @@ func NewGetScanConfigsRequest(server string, params *GetScanConfigsParams) (*htt } - if params.Page != nil { + if params.Select != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "page", runtime.ParamLocationQuery, *params.Page); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$select", runtime.ParamLocationQuery, *params.Select); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -628,9 +628,57 @@ func NewGetScanConfigsRequest(server string, params *GetScanConfigsParams) (*htt } - if params.PageSize != nil { + if params.Count != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "pageSize", runtime.ParamLocationQuery, *params.PageSize); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$count", runtime.ParamLocationQuery, *params.Count); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Top != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$top", runtime.ParamLocationQuery, *params.Top); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Skip != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$skip", runtime.ParamLocationQuery, *params.Skip); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Expand != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$expand", runtime.ParamLocationQuery, *params.Expand); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -729,7 +777,7 @@ func NewDeleteScanConfigsScanConfigIDRequest(server string, scanConfigID ScanCon } // NewGetScanConfigsScanConfigIDRequest generates requests for GetScanConfigsScanConfigID -func NewGetScanConfigsScanConfigIDRequest(server string, scanConfigID ScanConfigID) (*http.Request, error) { +func NewGetScanConfigsScanConfigIDRequest(server string, scanConfigID ScanConfigID, params *GetScanConfigsScanConfigIDParams) (*http.Request, error) { var err error var pathParam0 string @@ -754,6 +802,42 @@ func NewGetScanConfigsScanConfigIDRequest(server string, scanConfigID ScanConfig return nil, err } + queryValues := queryURL.Query() + + if params.Select != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$select", runtime.ParamLocationQuery, *params.Select); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Expand != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$expand", runtime.ParamLocationQuery, *params.Expand); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err @@ -1709,7 +1793,7 @@ type ClientWithResponsesInterface interface { DeleteScanConfigsScanConfigIDWithResponse(ctx context.Context, scanConfigID ScanConfigID, reqEditors ...RequestEditorFn) (*DeleteScanConfigsScanConfigIDResponse, error) // GetScanConfigsScanConfigID request - GetScanConfigsScanConfigIDWithResponse(ctx context.Context, scanConfigID ScanConfigID, reqEditors ...RequestEditorFn) (*GetScanConfigsScanConfigIDResponse, error) + GetScanConfigsScanConfigIDWithResponse(ctx context.Context, scanConfigID ScanConfigID, params *GetScanConfigsScanConfigIDParams, reqEditors ...RequestEditorFn) (*GetScanConfigsScanConfigIDResponse, error) // PatchScanConfigsScanConfigID request with any body PatchScanConfigsScanConfigIDWithBodyWithResponse(ctx context.Context, scanConfigID ScanConfigID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchScanConfigsScanConfigIDResponse, error) @@ -2346,8 +2430,8 @@ func (c *ClientWithResponses) DeleteScanConfigsScanConfigIDWithResponse(ctx cont } // GetScanConfigsScanConfigIDWithResponse request returning *GetScanConfigsScanConfigIDResponse -func (c *ClientWithResponses) GetScanConfigsScanConfigIDWithResponse(ctx context.Context, scanConfigID ScanConfigID, reqEditors ...RequestEditorFn) (*GetScanConfigsScanConfigIDResponse, error) { - rsp, err := c.GetScanConfigsScanConfigID(ctx, scanConfigID, reqEditors...) +func (c *ClientWithResponses) GetScanConfigsScanConfigIDWithResponse(ctx context.Context, scanConfigID ScanConfigID, params *GetScanConfigsScanConfigIDParams, reqEditors ...RequestEditorFn) (*GetScanConfigsScanConfigIDResponse, error) { + rsp, err := c.GetScanConfigsScanConfigID(ctx, scanConfigID, params, reqEditors...) if err != nil { return nil, err } diff --git a/api/models/models.gen.go b/api/models/models.gen.go index c6eae4027..c1c352137 100644 --- a/api/models/models.gen.go +++ b/api/models/models.gen.go @@ -362,11 +362,11 @@ type ScanConfigRelationship struct { // ScanConfigs defines model for ScanConfigs. type ScanConfigs struct { + // Count Total scan config count according to the given filters + Count *int `json:"count,omitempty"` + // Items List of scan configs according to the given filters and page. List length must be lower or equal to pageSize. Items *[]ScanConfig `json:"items,omitempty"` - - // Total Total scan config count according to the given filters - Total *int `json:"total,omitempty"` } // ScanExists defines model for ScanExists. @@ -619,12 +619,24 @@ type WeeklyScheduleScanConfig struct { TimeOfDay *TimeOfDay `json:"timeOfDay,omitempty"` } +// OdataCount defines model for odataCount. +type OdataCount = bool + +// OdataExpand defines model for odataExpand. +type OdataExpand = string + // OdataFilter defines model for odataFilter. type OdataFilter = string // OdataSelect defines model for odataSelect. type OdataSelect = string +// OdataSkip defines model for odataSkip. +type OdataSkip = int + +// OdataTop defines model for odataTop. +type OdataTop = int + // Page defines model for page. type Page = int @@ -652,12 +664,17 @@ type UnknownError = ApiResponse // GetScanConfigsParams defines parameters for GetScanConfigs. type GetScanConfigsParams struct { Filter *OdataFilter `form:"$filter,omitempty" json:"$filter,omitempty"` + Select *OdataSelect `form:"$select,omitempty" json:"$select,omitempty"` + Count *OdataCount `form:"$count,omitempty" json:"$count,omitempty"` + Top *OdataTop `form:"$top,omitempty" json:"$top,omitempty"` + Skip *OdataSkip `form:"$skip,omitempty" json:"$skip,omitempty"` + Expand *OdataExpand `form:"$expand,omitempty" json:"$expand,omitempty"` +} - // Page Page number of the query - Page *Page `form:"page,omitempty" json:"page,omitempty"` - - // PageSize Maximum items to return - PageSize *PageSize `form:"pageSize,omitempty" json:"pageSize,omitempty"` +// GetScanConfigsScanConfigIDParams defines parameters for GetScanConfigsScanConfigID. +type GetScanConfigsScanConfigIDParams struct { + Select *OdataSelect `form:"$select,omitempty" json:"$select,omitempty"` + Expand *OdataExpand `form:"$expand,omitempty" json:"$expand,omitempty"` } // GetScanResultsParams defines parameters for GetScanResults. diff --git a/api/openapi.yaml b/api/openapi.yaml index 8396db0e5..27e3da437 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -362,8 +362,11 @@ paths: summary: Get all scan configs. parameters: - $ref: '#/components/parameters/odataFilter' - - $ref: '#/components/parameters/page' - - $ref: '#/components/parameters/pageSize' + - $ref: '#/components/parameters/odataSelect' + - $ref: '#/components/parameters/odataCount' + - $ref: '#/components/parameters/odataTop' + - $ref: '#/components/parameters/odataSkip' + - $ref: '#/components/parameters/odataExpand' responses: 200: description: Success @@ -404,6 +407,8 @@ paths: summary: Get the details for a scan config. parameters: - $ref: '#/components/parameters/scanConfigID' + - $ref: '#/components/parameters/odataSelect' + - $ref: '#/components/parameters/odataExpand' responses: 200: description: Success @@ -687,7 +692,7 @@ components: ScanConfigs: type: object properties: - total: + count: type: integer description: Total scan config count according to the given filters readOnly: true @@ -698,8 +703,6 @@ components: items: $ref: '#/components/schemas/ScanConfig' readOnly: true - required: - - total ScanConfigData: type: object @@ -1393,6 +1396,30 @@ components: schema: type: string + odataCount: + name: "$count" + in: query + schema: + type: boolean + + odataTop: + name: "$top" + in: query + schema: + type: integer + + odataSkip: + name: "$skip" + in: query + schema: + type: integer + + odataExpand: + name: "$expand" + in: query + schema: + type: string + scanID: name: scanID in: path diff --git a/api/server/server.gen.go b/api/server/server.gen.go index 684632740..2a19f2cd2 100644 --- a/api/server/server.gen.go +++ b/api/server/server.gen.go @@ -32,7 +32,7 @@ type ServerInterface interface { DeleteScanConfigsScanConfigID(ctx echo.Context, scanConfigID ScanConfigID) error // Get the details for a scan config. // (GET /scanConfigs/{scanConfigID}) - GetScanConfigsScanConfigID(ctx echo.Context, scanConfigID ScanConfigID) error + GetScanConfigsScanConfigID(ctx echo.Context, scanConfigID ScanConfigID, params GetScanConfigsScanConfigIDParams) error // Patch a scan config. // (PATCH /scanConfigs/{scanConfigID}) PatchScanConfigsScanConfigID(ctx echo.Context, scanConfigID ScanConfigID) error @@ -107,18 +107,39 @@ func (w *ServerInterfaceWrapper) GetScanConfigs(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $filter: %s", err)) } - // ------------- Optional query parameter "page" ------------- + // ------------- Optional query parameter "$select" ------------- - err = runtime.BindQueryParameter("form", true, false, "page", ctx.QueryParams(), ¶ms.Page) + err = runtime.BindQueryParameter("form", true, false, "$select", ctx.QueryParams(), ¶ms.Select) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter page: %s", err)) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $select: %s", err)) } - // ------------- Optional query parameter "pageSize" ------------- + // ------------- Optional query parameter "$count" ------------- - err = runtime.BindQueryParameter("form", true, false, "pageSize", ctx.QueryParams(), ¶ms.PageSize) + err = runtime.BindQueryParameter("form", true, false, "$count", ctx.QueryParams(), ¶ms.Count) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter pageSize: %s", err)) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $count: %s", err)) + } + + // ------------- Optional query parameter "$top" ------------- + + err = runtime.BindQueryParameter("form", true, false, "$top", ctx.QueryParams(), ¶ms.Top) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $top: %s", err)) + } + + // ------------- Optional query parameter "$skip" ------------- + + err = runtime.BindQueryParameter("form", true, false, "$skip", ctx.QueryParams(), ¶ms.Skip) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $skip: %s", err)) + } + + // ------------- Optional query parameter "$expand" ------------- + + err = runtime.BindQueryParameter("form", true, false, "$expand", ctx.QueryParams(), ¶ms.Expand) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $expand: %s", err)) } // Invoke the callback with all the unmarshalled arguments @@ -162,8 +183,24 @@ func (w *ServerInterfaceWrapper) GetScanConfigsScanConfigID(ctx echo.Context) er return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter scanConfigID: %s", err)) } + // Parameter object where we will unmarshal all parameters from the context + var params GetScanConfigsScanConfigIDParams + // ------------- Optional query parameter "$select" ------------- + + err = runtime.BindQueryParameter("form", true, false, "$select", ctx.QueryParams(), ¶ms.Select) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $select: %s", err)) + } + + // ------------- Optional query parameter "$expand" ------------- + + err = runtime.BindQueryParameter("form", true, false, "$expand", ctx.QueryParams(), ¶ms.Expand) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $expand: %s", err)) + } + // Invoke the callback with all the unmarshalled arguments - err = w.Handler.GetScanConfigsScanConfigID(ctx, scanConfigID) + err = w.Handler.GetScanConfigsScanConfigID(ctx, scanConfigID, params) return err } @@ -554,74 +591,75 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "eAHPUE7g8+fn/w8AAP//xe3zmOV0AAA=", + "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", + "3zvolDyHZ/AEFQQ+fXn6/wAAAP//gJMYvdp2AAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/cmd/backend/main.go b/backend/cmd/backend/main.go index 5cd44499d..3eaa530ad 100644 --- a/backend/cmd/backend/main.go +++ b/backend/cmd/backend/main.go @@ -26,7 +26,7 @@ import ( "github.com/openclarity/vmclarity/backend/pkg/backend" "github.com/openclarity/vmclarity/backend/pkg/config" - "github.com/openclarity/vmclarity/backend/pkg/database" + databaseTypes "github.com/openclarity/vmclarity/backend/pkg/database/types" "github.com/openclarity/vmclarity/backend/pkg/version" ) @@ -43,7 +43,7 @@ func versionCommand(_ *cli.Context) { func main() { viper.SetDefault(config.HealthCheckAddress, ":8081") viper.SetDefault(config.BackendRestPort, "8888") - viper.SetDefault(config.DatabaseDriver, database.DBDriverTypeLocal) + viper.SetDefault(config.DatabaseDriver, databaseTypes.DBDriverTypeLocal) viper.AutomaticEnv() app := cli.NewApp() app.Usage = "" diff --git a/backend/pkg/backend/backend.go b/backend/pkg/backend/backend.go index 82ae54cd0..9446ccd72 100644 --- a/backend/pkg/backend/backend.go +++ b/backend/pkg/backend/backend.go @@ -27,6 +27,7 @@ import ( _config "github.com/openclarity/vmclarity/backend/pkg/config" "github.com/openclarity/vmclarity/backend/pkg/database" + databaseTypes "github.com/openclarity/vmclarity/backend/pkg/database/types" "github.com/openclarity/vmclarity/backend/pkg/rest" runtime_scan_config "github.com/openclarity/vmclarity/runtime_scan/pkg/config" "github.com/openclarity/vmclarity/runtime_scan/pkg/orchestrator" @@ -35,17 +36,17 @@ import ( ) type Backend struct { - dbHandler *database.Handler + dbHandler databaseTypes.Database } -func CreateBackend(dbHandler *database.Handler) *Backend { +func CreateBackend(dbHandler databaseTypes.Database) *Backend { return &Backend{ dbHandler: dbHandler, } } -func createDatabaseConfig(config *_config.Config) *database.DBConfig { - return &database.DBConfig{ +func createDatabaseConfig(config *_config.Config) databaseTypes.DBConfig { + return databaseTypes.DBConfig{ DriverType: config.DatabaseDriver, EnableInfoLogs: config.EnableDBInfoLogs, DBPassword: config.DBPassword, @@ -78,7 +79,10 @@ func Run() { log.Info("VMClarity backend is running") dbConfig := createDatabaseConfig(config) - dbHandler := database.Init(dbConfig) + dbHandler, err := database.InitaliseDatabase(dbConfig) + if err != nil { + log.Fatalf("Failed to initialise database: %v", err) + } _ = CreateBackend(dbHandler) diff --git a/backend/pkg/database/database.go b/backend/pkg/database/database.go index fafdd7904..b143ba6c3 100644 --- a/backend/pkg/database/database.go +++ b/backend/pkg/database/database.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. // All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,99 +16,36 @@ package database import ( - "time" + "fmt" + "sync" - uuid "github.com/satori/go.uuid" - log "github.com/sirupsen/logrus" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" + "github.com/openclarity/vmclarity/backend/pkg/database/gorm" + "github.com/openclarity/vmclarity/backend/pkg/database/types" ) -const ( - DBDriverTypeLocal = "LOCAL" -) - -type Database interface { - ScanResultsTable() ScanResultsTable - ScanConfigsTable() ScanConfigsTable - ScansTable() ScansTable - TargetsTable() TargetsTable -} - -type Handler struct { - DB *gorm.DB -} - -type DBConfig struct { - EnableInfoLogs bool `json:"enable-info-logs"` - DriverType string `json:"driver-type,omitempty"` - DBPassword string `json:"-"` - DBUser string `json:"db-user,omitempty"` - DBHost string `json:"db-host,omitempty"` - DBPort string `json:"db-port,omitempty"` - DBName string `json:"db-name,omitempty"` - - LocalDBPath string `json:"local-db-path,omitempty"` -} - -// Base contains common columns for all tables. -type Base struct { - ID uuid.UUID `gorm:"type:uuid;primaryKey"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt *time.Time `sql:"index"` -} - -// BeforeCreate will set a UUID rather than numeric ID. -func (base *Base) BeforeCreate(db *gorm.DB) error { - base.ID = uuid.NewV4() - return nil -} - -func Init(config *DBConfig) *Handler { - databaseHandler := Handler{} +type DBDriver func(config types.DBConfig) (types.Database, error) - databaseHandler.DB = initDataBase(config) - - return &databaseHandler -} - -func initDataBase(config *DBConfig) *gorm.DB { - dbDriver := config.DriverType - dbLogger := logger.Default - if config.EnableInfoLogs { - dbLogger = dbLogger.LogMode(logger.Info) - } - - db := initDB(config, dbDriver, dbLogger) - - // this will ensure table is created - if err := db.AutoMigrate(Target{}, ScanResult{}, ScanConfig{}, Scan{}); err != nil { - log.Fatalf("Failed to run auto migration: %v", err) - } +var ( + DBDrivers map[string]DBDriver + RegisterDriversOnce sync.Once +) - return db +func RegisterDrivers() { + RegisterDriversOnce.Do(func() { + // If DBDrivers is initialised before this function is called don't + // reset it, this is useful for testing. + if DBDrivers == nil { + DBDrivers = map[string]DBDriver{} + } + DBDrivers[types.DBDriverTypeLocal] = gorm.NewDatabase + }) } -func initDB(config *DBConfig, dbDriver string, dbLogger logger.Interface) *gorm.DB { - var db *gorm.DB - switch dbDriver { - case DBDriverTypeLocal: - db = initSqlite(config, dbLogger) - default: - log.Fatalf("DB driver is not supported: %v", dbDriver) - } - return db -} +func InitaliseDatabase(config types.DBConfig) (types.Database, error) { + RegisterDrivers() -func initSqlite(config *DBConfig, dbLogger logger.Interface) *gorm.DB { - db, err := gorm.Open(sqlite.Open(config.LocalDBPath), &gorm.Config{ - Logger: dbLogger, - }) - if err != nil { - log.Fatalf("Failed to open db: %v", err) + if driver, ok := DBDrivers[config.DriverType]; ok { + return driver(config) } - - return db + return nil, fmt.Errorf("unknown DB driver %s", config.DriverType) } diff --git a/backend/pkg/database/gorm/database.go b/backend/pkg/database/gorm/database.go new file mode 100644 index 000000000..f9596c664 --- /dev/null +++ b/backend/pkg/database/gorm/database.go @@ -0,0 +1,98 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "fmt" + "time" + + uuid "github.com/satori/go.uuid" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "github.com/openclarity/vmclarity/backend/pkg/database/types" +) + +func NewDatabase(config types.DBConfig) (types.Database, error) { + db, err := initDataBase(config) + if err != nil { + return nil, fmt.Errorf("unable to create new GORM database: %w", err) + } + return &Handler{DB: db}, nil +} + +type Handler struct { + DB *gorm.DB +} + +// Base contains common columns for all tables. +type Base struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time `sql:"index"` +} + +// BeforeCreate will set a UUID rather than numeric ID. +func (base *Base) BeforeCreate(db *gorm.DB) error { + base.ID = uuid.NewV4() + return nil +} + +func initDataBase(config types.DBConfig) (*gorm.DB, error) { + dbDriver := config.DriverType + dbLogger := logger.Default + if config.EnableInfoLogs { + dbLogger = dbLogger.LogMode(logger.Info) + } + + db, err := initDB(config, dbDriver, dbLogger) + if err != nil { + return nil, err + } + + // this will ensure table is created + if err := db.AutoMigrate( + Target{}, + ScanResult{}, + ScanConfig{}, + Scan{}, + ); err != nil { + return nil, fmt.Errorf("failed to run auto migration: %w", err) + } + + return db, nil +} + +func initDB(config types.DBConfig, dbDriver string, dbLogger logger.Interface) (*gorm.DB, error) { + switch dbDriver { + case types.DBDriverTypeLocal: + return initSqlite(config, dbLogger) + default: + return nil, fmt.Errorf("driver type %s is not supported by GORM driver", dbDriver) + } +} + +func initSqlite(config types.DBConfig, dbLogger logger.Interface) (*gorm.DB, error) { + db, err := gorm.Open(sqlite.Open(config.LocalDBPath), &gorm.Config{ + Logger: dbLogger, + }) + if err != nil { + return nil, fmt.Errorf("failed to open db: %w", err) + } + return db, nil +} diff --git a/backend/pkg/rest/convert/dbtorest/convert.go b/backend/pkg/database/gorm/dbtorest.go similarity index 51% rename from backend/pkg/rest/convert/dbtorest/convert.go rename to backend/pkg/database/gorm/dbtorest.go index 29dd7abbb..3b8e77e46 100644 --- a/backend/pkg/rest/convert/dbtorest/convert.go +++ b/backend/pkg/database/gorm/dbtorest.go @@ -13,73 +13,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dbtorest +package gorm import ( "encoding/json" "fmt" "github.com/openclarity/vmclarity/api/models" - "github.com/openclarity/vmclarity/backend/pkg/database" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) -func ConvertScanConfig(config *database.ScanConfig) (*models.ScanConfig, error) { - var ret models.ScanConfig - - if config.ScanFamiliesConfig != nil { - ret.ScanFamiliesConfig = &models.ScanFamiliesConfig{} - if err := json.Unmarshal(config.ScanFamiliesConfig, ret.ScanFamiliesConfig); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) - } - } - - if config.Scope != nil { - ret.Scope = &models.ScanScopeType{} - if err := ret.Scope.UnmarshalJSON(config.Scope); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) - } - } - - if config.Scheduled != nil { - ret.Scheduled = &models.RuntimeScheduleScanConfigType{} - if err := ret.Scheduled.UnmarshalJSON(config.Scheduled); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) - } - } - - ret.Name = config.Name - - ret.Id = utils.StringPtr(config.ID.String()) - - return &ret, nil -} - -func ConvertScanConfigs(configs []*database.ScanConfig, total int64) (*models.ScanConfigs, error) { - ret := models.ScanConfigs{ - Items: &[]models.ScanConfig{}, - } - - for _, config := range configs { - sc, err := ConvertScanConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to convet scan config: %w", err) - } - *ret.Items = append(*ret.Items, *sc) - } - - ret.Total = utils.IntPtr(int(total)) - - return &ret, nil -} - -func ConvertTarget(target *database.Target) (*models.Target, error) { +func ConvertToRestTarget(target Target) (models.Target, error) { ret := models.Target{ TargetInfo: &models.TargetType{}, } switch target.Type { - case "VMInfo": + case targetTypeVM: var cloudProvider *models.CloudProvider if target.InstanceProvider != nil { cp := models.CloudProvider(*target.InstanceProvider) @@ -90,130 +40,128 @@ func ConvertTarget(target *database.Target) (*models.Target, error) { InstanceProvider: cloudProvider, Location: *target.Location, }); err != nil { - return nil, fmt.Errorf("FromVMInfo failed: %w", err) + return ret, fmt.Errorf("FromVMInfo failed: %w", err) } - case "Dir": - return nil, fmt.Errorf("unsupported target type Dir") - case "Pod": - return nil, fmt.Errorf("unsupported target type Pod") + case targetTypeDir, targetTypePod: + fallthrough default: - return nil, fmt.Errorf("unknown target type: %v", target.Type) + return ret, fmt.Errorf("unknown target type: %v", target.Type) } ret.Id = utils.StringPtr(target.ID.String()) - return &ret, nil + return ret, nil } -func ConvertTargets(targets []*database.Target, total int64) (*models.Targets, error) { +func ConvertToRestTargets(targets []Target) (models.Targets, error) { ret := models.Targets{ Items: &[]models.Target{}, } for _, target := range targets { - tr, err := ConvertTarget(target) + tr, err := ConvertToRestTarget(target) if err != nil { - return nil, fmt.Errorf("failed to convert target: %w", err) + return ret, fmt.Errorf("failed to convert target: %w", err) } - *ret.Items = append(*ret.Items, *tr) + *ret.Items = append(*ret.Items, tr) } - ret.Total = utils.IntPtr(int(total)) + ret.Total = utils.IntPtr(len(targets)) - return &ret, nil + return ret, nil } // nolint:cyclop -func ConvertScanResult(scanResult *database.ScanResult) (*models.TargetScanResult, error) { +func ConvertToRestScanResult(scanResult ScanResult) (models.TargetScanResult, error) { var ret models.TargetScanResult if scanResult.Secrets != nil { ret.Secrets = &models.SecretScan{} if err := json.Unmarshal(scanResult.Secrets, ret.Secrets); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Vulnerabilities != nil { ret.Vulnerabilities = &models.VulnerabilityScan{} if err := json.Unmarshal(scanResult.Vulnerabilities, ret.Vulnerabilities); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Exploits != nil { ret.Exploits = &models.ExploitScan{} if err := json.Unmarshal(scanResult.Exploits, ret.Exploits); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Malware != nil { ret.Malware = &models.MalwareScan{} if err := json.Unmarshal(scanResult.Malware, ret.Malware); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Misconfigurations != nil { ret.Misconfigurations = &models.MisconfigurationScan{} if err := json.Unmarshal(scanResult.Misconfigurations, ret.Misconfigurations); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Rootkits != nil { ret.Rootkits = &models.RootkitScan{} if err := json.Unmarshal(scanResult.Rootkits, ret.Rootkits); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Sboms != nil { ret.Sboms = &models.SbomScan{} if err := json.Unmarshal(scanResult.Sboms, ret.Sboms); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Status != nil { ret.Status = &models.TargetScanStatus{} if err := json.Unmarshal(scanResult.Status, ret.Status); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } ret.Id = utils.StringPtr(scanResult.ID.String()) ret.ScanId = scanResult.ScanID ret.TargetId = scanResult.TargetID - return &ret, nil + return ret, nil } -func ConvertScanResults(scanResults []*database.ScanResult, total int64) (*models.TargetScanResults, error) { +func ConvertToRestScanResults(scanResults []ScanResult) (models.TargetScanResults, error) { ret := models.TargetScanResults{ Items: &[]models.TargetScanResult{}, } for _, scanResult := range scanResults { - sr, err := ConvertScanResult(scanResult) + sr, err := ConvertToRestScanResult(scanResult) if err != nil { - return nil, fmt.Errorf("failed to convert scan result: %w", err) + return ret, fmt.Errorf("failed to convert scan result: %w", err) } - *ret.Items = append(*ret.Items, *sr) + *ret.Items = append(*ret.Items, sr) } - ret.Total = utils.IntPtr(int(total)) + ret.Total = utils.IntPtr(len(scanResults)) - return &ret, nil + return ret, nil } -func ConvertScan(scan *database.Scan) (*models.Scan, error) { +func ConvertToRestScan(scan Scan) (models.Scan, error) { var ret models.Scan if scan.ScanConfigSnapshot != nil { ret.ScanConfigSnapshot = &models.ScanConfigData{} if err := json.Unmarshal(scan.ScanConfigSnapshot, ret.ScanConfigSnapshot); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scan.TargetIDs != nil { ret.TargetIDs = &[]string{} if err := json.Unmarshal(scan.TargetIDs, ret.TargetIDs); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } @@ -222,23 +170,23 @@ func ConvertScan(scan *database.Scan) (*models.Scan, error) { ret.EndTime = scan.ScanEndTime ret.ScanConfig = &models.ScanConfigRelationship{Id: *scan.ScanConfigID} - return &ret, nil + return ret, nil } -func ConvertScans(scans []*database.Scan, total int64) (*models.Scans, error) { +func ConvertToRestScans(scans []Scan) (models.Scans, error) { ret := models.Scans{ Items: &[]models.Scan{}, } for _, scan := range scans { - sc, err := ConvertScan(scan) + sc, err := ConvertToRestScan(scan) if err != nil { - return nil, fmt.Errorf("failed to convert scan: %w", err) + return models.Scans{}, fmt.Errorf("failed to convert scan: %w", err) } - *ret.Items = append(*ret.Items, *sc) + *ret.Items = append(*ret.Items, sc) } - ret.Total = utils.IntPtr(int(total)) + ret.Total = utils.IntPtr(len(scans)) - return &ret, nil + return ret, nil } diff --git a/backend/pkg/database/gorm/dbtorest_test.go b/backend/pkg/database/gorm/dbtorest_test.go new file mode 100644 index 000000000..e46be4c4f --- /dev/null +++ b/backend/pkg/database/gorm/dbtorest_test.go @@ -0,0 +1,171 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "encoding/json" + "reflect" + "testing" + + uuid "github.com/satori/go.uuid" + "gotest.tools/v3/assert" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" +) + +func TestConvertToRestScanResult(t *testing.T) { + state := models.DONE + status := models.TargetScanStatus{ + Vulnerabilities: &models.TargetScanState{ + Errors: nil, + State: &state, + }, + Exploits: &models.TargetScanState{ + Errors: &[]string{"err"}, + State: &state, + }, + } + + vulsScan := models.VulnerabilityScan{ + Vulnerabilities: &[]models.Vulnerability{ + { + VulnerabilityInfo: &models.VulnerabilityInfo{ + VulnerabilityName: utils.StringPtr("name"), + }, + }, + }, + } + + vulScanB, err := json.Marshal(&vulsScan) + assert.NilError(t, err) + + statusB, err := json.Marshal(&status) + assert.NilError(t, err) + + uid := uuid.NewV4() + + type args struct { + scanResult ScanResult + } + tests := []struct { + name string + args args + want models.TargetScanResult + wantErr bool + }{ + { + name: "sanity", + args: args{ + scanResult: ScanResult{ + Base: Base{ + ID: uid, + }, + ScanID: "1", + TargetID: "2", + Status: statusB, + Vulnerabilities: vulScanB, + }, + }, + want: models.TargetScanResult{ + Id: utils.StringPtr(uid.String()), + ScanId: "1", + Status: &status, + TargetId: "2", + Vulnerabilities: &vulsScan, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ConvertToRestScanResult(tt.args.scanResult) + if (err != nil) != tt.wantErr { + t.Errorf("ConvertToRestScanResult() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ConvertToRestScanResult() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestConvertToRestScan(t *testing.T) { + scanSnap := models.ScanConfigData{ + ScanFamiliesConfig: &models.ScanFamiliesConfig{ + Vulnerabilities: &models.VulnerabilitiesConfig{ + Enabled: nil, + }, + }, + } + + scanSnapB, err := json.Marshal(&scanSnap) + assert.NilError(t, err) + + targetIDs := []string{"s"} + targetIDsB, err := json.Marshal(&targetIDs) + assert.NilError(t, err) + + id := uuid.NewV4() + + type args struct { + scan Scan + } + tests := []struct { + name string + args args + want models.Scan + wantErr bool + }{ + { + name: "sanity", + args: args{ + scan: Scan{ + Base: Base{ + ID: id, + }, + ScanStartTime: nil, + ScanEndTime: nil, + ScanConfigID: utils.StringPtr("1"), + ScanConfigSnapshot: scanSnapB, + TargetIDs: targetIDsB, + }, + }, + want: models.Scan{ + Id: utils.StringPtr(id.String()), + ScanConfig: &models.ScanConfigRelationship{ + Id: "1", + }, + ScanConfigSnapshot: &scanSnap, + TargetIDs: &targetIDs, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ConvertToRestScan(tt.args.scan) + if (err != nil) != tt.wantErr { + t.Errorf("ConvertToRestScan() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ConvertToRestScan() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/backend/pkg/database/gorm/odata.go b/backend/pkg/database/gorm/odata.go new file mode 100644 index 000000000..53ba75902 --- /dev/null +++ b/backend/pkg/database/gorm/odata.go @@ -0,0 +1,236 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "fmt" + + "gorm.io/datatypes" + "gorm.io/gorm" + + "github.com/openclarity/vmclarity/backend/pkg/database/odatasql" + "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" +) + +type ODataObject struct { + gorm.Model + Data datatypes.JSON +} + +var schemaMetas = map[string]odatasql.SchemaMeta{ + "ScanConfig": { + Table: "scan_configs", + Fields: odatasql.Schema{ + "id": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "name": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "scanFamiliesConfig": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"ScanFamiliesConfig"}, + }, + "scheduled": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"SingleScheduleScanConfig"}, + DescriminatorProperty: "objectType", + }, + "scope": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"AwsScanScope"}, + DescriminatorProperty: "objectType", + }, + }, + }, + "ScanFamiliesConfig": { + Fields: odatasql.Schema{ + "exploits": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"ExploitsConfig"}, + }, + "malware": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"MalwareConfig"}, + }, + "misconfigurations": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"MisconfigurationsConfig"}, + }, + "rootkits": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"RootkitsConfig"}, + }, + "sbom": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"SBOMConfig"}, + }, + "secrets": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"SecretsConfig"}, + }, + "vulnerabilities": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"VulnerabilitiesConfig"}, + }, + }, + }, + "ExploitsConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "MalwareConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "MisconfigurationsConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "RootkitsConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "SBOMConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "SecretsConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "VulnerabilitiesConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "SingleScheduleScanConfig": { + Fields: odatasql.Schema{ + "objectType": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "operationTime": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "AwsScanScope": { + Fields: odatasql.Schema{ + "objectType": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "all": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "shouldScanStoppedInstances": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "instanceTagExclusion": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"Tag"}, + }, + }, + "instanceTagSelector": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"Tag"}, + }, + }, + "regions": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"AwsRegion"}, + }, + }, + }, + }, + "Tag": { + Fields: odatasql.Schema{ + "key": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "value": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "AwsRegion": { + Fields: odatasql.Schema{ + "id": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "name": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "vpcs": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"AwsVPC"}, + }, + }, + }, + }, + "AwsVPC": { + Fields: odatasql.Schema{ + "id": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "name": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "securityGroups": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"AwsSecurityGroup"}, + }, + }, + }, + }, + "AwsSecurityGroup": { + Fields: odatasql.Schema{ + "id": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "name": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, +} + +func ODataQuery(db *gorm.DB, schema string, filterString, selectString, expandString *string, top, skip *int, collection bool, result interface{}) error { + // If we're not getting a collection, make sure the result is limited + // to 1 item. + if !collection { + top = utils.IntPtr(1) + skip = nil + } + + // Build the raw SQL query using the odatasql library, this will also + // parse and validate the ODATA query params. + query, err := odatasql.BuildSQLQuery(schemaMetas, schema, filterString, selectString, expandString, top, skip) + if err != nil { + return fmt.Errorf("failed to build query for DB: %w", err) + } + + // Use the query to populate "result" using the gorm finalisers so that + // the gorm error handling processes things like no results found. + if collection { + if err := db.Raw(query).Find(result).Error; err != nil { + return fmt.Errorf("failed to query DB: %w", err) + } + } else { + if err := db.Raw(query).First(result).Error; err != nil { + return fmt.Errorf("failed to query DB: %w", err) + } + } + return nil +} + +func ODataCount(db *gorm.DB, schema string, filterString *string) (int, error) { + query, err := odatasql.BuildCountQuery(schemaMetas, schema, filterString) + if err != nil { + return 0, fmt.Errorf("failed to build query to count objects: %w", err) + } + + var count int + if err := db.Raw(query).Scan(&count).Error; err != nil { + return 0, fmt.Errorf("failed to query DB: %w", err) + } + return count, nil +} diff --git a/backend/pkg/rest/convert/resttodb/convert.go b/backend/pkg/database/gorm/resttodb.go similarity index 51% rename from backend/pkg/rest/convert/resttodb/convert.go rename to backend/pkg/database/gorm/resttodb.go index bfec373e9..bbc71cbc6 100644 --- a/backend/pkg/rest/convert/resttodb/convert.go +++ b/backend/pkg/database/gorm/resttodb.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package resttodb +package gorm import ( "encoding/json" @@ -22,75 +22,34 @@ import ( uuid "github.com/satori/go.uuid" "github.com/openclarity/vmclarity/api/models" - "github.com/openclarity/vmclarity/backend/pkg/database" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) -func ConvertScanConfig(config *models.ScanConfig, id string) (*database.ScanConfig, error) { - var ret database.ScanConfig - var err error - var scanConfigUUID uuid.UUID - - if id != "" { - scanConfigUUID, err = uuid.FromString(id) - if err != nil { - return nil, fmt.Errorf("failed to convert scanConfigID %v to uuid: %v", id, err) - } - } - - if config.ScanFamiliesConfig != nil { - ret.ScanFamiliesConfig, err = json.Marshal(config.ScanFamiliesConfig) - if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) - } - } - - if config.Scope != nil { - ret.Scope, err = config.Scope.MarshalJSON() - if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) - } - } - - if config.Scheduled != nil { - ret.Scheduled, err = config.Scheduled.MarshalJSON() - if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) - } - } - - ret.Name = config.Name - - ret.Base = database.Base{ID: scanConfigUUID} - - return &ret, nil -} - -func ConvertTarget(target *models.Target, id string) (*database.Target, error) { +func ConvertToDBTarget(target models.Target) (Target, error) { var targetUUID uuid.UUID var err error - if id != "" { - targetUUID, err = uuid.FromString(id) + if target.Id != nil { + targetUUID, err = uuid.FromString(*target.Id) if err != nil { - return nil, fmt.Errorf("failed to convert targetID %v to uuid: %v", id, err) + return Target{}, fmt.Errorf("failed to convert targetID %v to uuid: %v", *target.Id, err) } } disc, err := target.TargetInfo.Discriminator() if err != nil { - return nil, fmt.Errorf("failed to get discriminator: %w", err) + return Target{}, fmt.Errorf("failed to get discriminator: %w", err) } switch disc { case "VMInfo": vminfo, err := target.TargetInfo.AsVMInfo() if err != nil { - return nil, fmt.Errorf("failed to convert target to vm info: %w", err) + return Target{}, fmt.Errorf("failed to convert target to vm info: %w", err) } var provider *string if vminfo.InstanceProvider != nil { provider = utils.StringPtr(string(*vminfo.InstanceProvider)) } - return &database.Target{ - Base: database.Base{ + return Target{ + Base: Base{ ID: targetUUID, }, Type: vminfo.ObjectType, @@ -99,20 +58,20 @@ func ConvertTarget(target *models.Target, id string) (*database.Target, error) { InstanceProvider: provider, }, nil default: - return nil, fmt.Errorf("unknown target type: %v", disc) + return Target{}, fmt.Errorf("unknown target type: %v", disc) } } // nolint:cyclop -func ConvertScanResult(result *models.TargetScanResult, id string) (*database.ScanResult, error) { - var ret database.ScanResult +func ConvertToDBScanResult(result models.TargetScanResult) (ScanResult, error) { + var ret ScanResult var err error var scanResultUUID uuid.UUID - if id != "" { - scanResultUUID, err = uuid.FromString(id) + if result.Id != nil { + scanResultUUID, err = uuid.FromString(*result.Id) if err != nil { - return nil, fmt.Errorf("failed to convert scanResultID %v to uuid: %v", id, err) + return ret, fmt.Errorf("failed to convert scanResultID %v to uuid: %v", *result.Id, err) } } ret.ScanID = result.ScanId @@ -121,67 +80,67 @@ func ConvertScanResult(result *models.TargetScanResult, id string) (*database.Sc if result.Exploits != nil { ret.Exploits, err = json.Marshal(result.Exploits) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Malware != nil { ret.Malware, err = json.Marshal(result.Malware) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Misconfigurations != nil { ret.Misconfigurations, err = json.Marshal(result.Misconfigurations) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Rootkits != nil { ret.Rootkits, err = json.Marshal(result.Rootkits) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Sboms != nil { ret.Sboms, err = json.Marshal(result.Sboms) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Secrets != nil { ret.Secrets, err = json.Marshal(result.Secrets) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Status != nil { ret.Status, err = json.Marshal(result.Status) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Vulnerabilities != nil { ret.Vulnerabilities, err = json.Marshal(result.Vulnerabilities) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } - ret.Base = database.Base{ID: scanResultUUID} + ret.Base = Base{ID: scanResultUUID} - return &ret, nil + return ret, nil } -func ConvertScan(scan *models.Scan, id string) (*database.Scan, error) { - var ret database.Scan +func ConvertToDBScan(scan models.Scan) (Scan, error) { + var ret Scan var err error var scanUUID uuid.UUID - if id != "" { - scanUUID, err = uuid.FromString(id) + if scan.Id != nil { + scanUUID, err = uuid.FromString(*scan.Id) if err != nil { - return nil, fmt.Errorf("failed to convert scanID %v to uuid: %v", id, err) + return ret, fmt.Errorf("failed to convert scanID %v to uuid: %v", scan.Id, err) } } @@ -196,18 +155,18 @@ func ConvertScan(scan *models.Scan, id string) (*database.Scan, error) { if scan.ScanConfigSnapshot != nil { ret.ScanConfigSnapshot, err = json.Marshal(scan.ScanConfigSnapshot) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if scan.TargetIDs != nil { ret.TargetIDs, err = json.Marshal(scan.TargetIDs) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } - ret.Base = database.Base{ID: scanUUID} + ret.Base = Base{ID: scanUUID} - return &ret, nil + return ret, nil } diff --git a/backend/pkg/rest/convert/resttodb/convert_test.go b/backend/pkg/database/gorm/resttodb_test.go similarity index 50% rename from backend/pkg/rest/convert/resttodb/convert_test.go rename to backend/pkg/database/gorm/resttodb_test.go index 0b1e566d5..7f718426e 100644 --- a/backend/pkg/rest/convert/resttodb/convert_test.go +++ b/backend/pkg/database/gorm/resttodb_test.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package resttodb +package gorm import ( "encoding/json" @@ -24,99 +24,12 @@ import ( "gotest.tools/v3/assert" "github.com/openclarity/vmclarity/api/models" - "github.com/openclarity/vmclarity/backend/pkg/database" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) const id = "f12d1ca7-1048-4e31-899c-7a25b357bed1" -func TestConvertScanConfig(t *testing.T) { - scanFamiliesConfig := models.ScanFamiliesConfig{ - Vulnerabilities: &models.VulnerabilitiesConfig{Enabled: utils.BoolPtr(true)}, - } - - scanFamiliesConfigB, err := json.Marshal(&scanFamiliesConfig) - assert.NilError(t, err) - - awsScanScope := models.AwsScanScope{ - All: utils.BoolPtr(true), - InstanceTagExclusion: nil, - InstanceTagSelector: nil, - ObjectType: "AwsScanScope", - Regions: nil, - ShouldScanStoppedInstances: utils.BoolPtr(false), - } - - var scanScopeType models.ScanScopeType - - err = scanScopeType.FromAwsScanScope(awsScanScope) - assert.NilError(t, err) - - scanScopeTypeB, err := scanScopeType.MarshalJSON() - assert.NilError(t, err) - - byHoursScheduleScanConfig := models.ByHoursScheduleScanConfig{ - HoursInterval: utils.IntPtr(2), - ObjectType: "ByHoursScheduleScanConfig", - } - - var runtimeScheduleScanConfigType models.RuntimeScheduleScanConfigType - err = runtimeScheduleScanConfigType.FromByHoursScheduleScanConfig(byHoursScheduleScanConfig) - assert.NilError(t, err) - - runtimeScheduleScanConfigTypeB, err := runtimeScheduleScanConfigType.MarshalJSON() - assert.NilError(t, err) - - UUID, err := uuid.FromString(id) - assert.NilError(t, err) - - type args struct { - config *models.ScanConfig - id string - } - tests := []struct { - name string - args args - want *database.ScanConfig - wantErr bool - }{ - { - name: "sanity", - args: args{ - id: id, - config: &models.ScanConfig{ - Id: utils.StringPtr("1"), - Name: utils.StringPtr("scanConfigName"), - ScanFamiliesConfig: &scanFamiliesConfig, - Scheduled: &runtimeScheduleScanConfigType, - Scope: &scanScopeType, - }, - }, - want: &database.ScanConfig{ - Base: database.Base{ID: UUID}, - Name: utils.StringPtr("scanConfigName"), - ScanFamiliesConfig: scanFamiliesConfigB, - Scheduled: runtimeScheduleScanConfigTypeB, - Scope: scanScopeTypeB, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScanConfig(tt.args.config, tt.args.id) - if (err != nil) != tt.wantErr { - t.Errorf("ConvertScanConfig() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScanConfig() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestConvertScanResult(t *testing.T) { +func TestConvertToDBScanResult(t *testing.T) { vulnerabilities := []models.Vulnerability{ { VulnerabilityInfo: &models.VulnerabilityInfo{ @@ -134,29 +47,29 @@ func TestConvertScanResult(t *testing.T) { UUID, err := uuid.FromString(id) assert.NilError(t, err) + idPtr := id + type args struct { - result *models.TargetScanResult - id string + result models.TargetScanResult } tests := []struct { name string args args - want *database.ScanResult + want ScanResult wantErr bool }{ { name: "sanity", args: args{ - id: id, - result: &models.TargetScanResult{ - Id: nil, + result: models.TargetScanResult{ + Id: &idPtr, ScanId: "3", TargetId: "2", Vulnerabilities: &vulScan, }, }, - want: &database.ScanResult{ - Base: database.Base{ID: UUID}, + want: ScanResult{ + Base: Base{ID: UUID}, ScanID: "3", TargetID: "2", Vulnerabilities: vulScanB, @@ -166,19 +79,19 @@ func TestConvertScanResult(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScanResult(tt.args.result, tt.args.id) + got, err := ConvertToDBScanResult(tt.args.result) if (err != nil) != tt.wantErr { - t.Errorf("ConvertScanResult() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ConvertToDBScanResult() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScanResult() got = %v, want %v", got, tt.want) + t.Errorf("ConvertToDBScanResult() got = %v, want %v", got, tt.want) } }) } } -func TestConvertTarget(t *testing.T) { +func TestConvertToDBTarget(t *testing.T) { cloudProvider := models.CloudProvider("aws") vmInfo := models.VMInfo{ InstanceID: "instanceID", @@ -194,26 +107,27 @@ func TestConvertTarget(t *testing.T) { UUID, err := uuid.FromString(id) assert.NilError(t, err) + idPtr := id + type args struct { - target *models.Target - id string + target models.Target } tests := []struct { name string args args - want *database.Target + want Target wantErr bool }{ { name: "sanity", args: args{ - target: &models.Target{ + target: models.Target{ + Id: &idPtr, TargetInfo: &targetType, }, - id: id, }, - want: &database.Target{ - Base: database.Base{ID: UUID}, + want: Target{ + Base: Base{ID: UUID}, Type: "VMInfo", Location: utils.StringPtr("location"), InstanceID: utils.StringPtr("instanceID"), @@ -226,19 +140,19 @@ func TestConvertTarget(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ConvertTarget(tt.args.target, tt.args.id) + got, err := ConvertToDBTarget(tt.args.target) if (err != nil) != tt.wantErr { - t.Errorf("ConvertTarget() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ConvertToDBTarget() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertTarget() got = %v, want %v", got, tt.want) + t.Errorf("ConvertToDBTarget() got = %v, want %v", got, tt.want) } }) } } -func TestConvertScan(t *testing.T) { +func TestConvertToDBScan(t *testing.T) { scanFamiliesConfig := models.ScanConfigData{ ScanFamiliesConfig: &models.ScanFamiliesConfig{ Exploits: &models.ExploitsConfig{ @@ -257,21 +171,22 @@ func TestConvertScan(t *testing.T) { UUID, err := uuid.FromString(id) assert.NilError(t, err) + idPtr := id + type args struct { - scan *models.Scan - id string + scan models.Scan } tests := []struct { name string args args - want *database.Scan + want Scan wantErr bool }{ { name: "sanity", args: args{ - id: id, - scan: &models.Scan{ + scan: models.Scan{ + Id: &idPtr, ScanConfig: &models.ScanConfigRelationship{ Id: "1", }, @@ -279,8 +194,8 @@ func TestConvertScan(t *testing.T) { TargetIDs: &targetIDs, }, }, - want: &database.Scan{ - Base: database.Base{ID: UUID}, + want: Scan{ + Base: Base{ID: UUID}, ScanConfigID: utils.StringPtr("1"), ScanConfigSnapshot: scanFamiliesConfigB, TargetIDs: targetIDsB, @@ -290,13 +205,13 @@ func TestConvertScan(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScan(tt.args.scan, tt.args.id) + got, err := ConvertToDBScan(tt.args.scan) if (err != nil) != tt.wantErr { - t.Errorf("ConvertScan() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ConvertToDBScan() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScan() got = %v, want %v", got, tt.want) + t.Errorf("ConvertToDBScan() got = %v, want %v", got, tt.want) } }) } diff --git a/backend/pkg/database/gorm/scan.go b/backend/pkg/database/gorm/scan.go new file mode 100644 index 000000000..322386677 --- /dev/null +++ b/backend/pkg/database/gorm/scan.go @@ -0,0 +1,190 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "context" + "errors" + "fmt" + "time" + + "gorm.io/gorm" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/backend/pkg/common" + "github.com/openclarity/vmclarity/backend/pkg/database/types" +) + +const ( + scansTableName = "scans" +) + +type Scan struct { + Base + + ScanStartTime *time.Time `json:"scan_start_time,omitempty" gorm:"column:scan_start_time"` + ScanEndTime *time.Time `json:"scan_end_time,omitempty" gorm:"column:scan_end_time"` + + // ScanConfigID The ID of the config that this scan was initiated from (optionanl) + ScanConfigID *string `json:"scan_config_id,omitempty" gorm:"column:scan_config_id"` + // ScanFamiliesConfig The configuration of the scanner families within a scan config + ScanConfigSnapshot []byte `json:"scan_families_config,omitempty" gorm:"column:scan_families_config"` + + // TargetIDs List of target IDs that are targeted for scanning as part of this scan + TargetIDs []byte `json:"target_ids,omitempty" gorm:"column:target_ids"` +} + +type GetScansParams struct { + // Filter Odata filter + Filter *string + // Page Page number of the query + Page *int + // PageSize Maximum items to return + PageSize *int +} + +type ScansTableHandler struct { + scansTable *gorm.DB +} + +func (db *Handler) ScansTable() types.ScansTable { + return &ScansTableHandler{ + scansTable: db.DB.Table(scansTableName), + } +} + +func (s *ScansTableHandler) GetScans(params models.GetScansParams) (models.Scans, error) { + tx := s.scansTable + + var dbScans []Scan + if err := tx.Find(&dbScans).Error; err != nil { + return models.Scans{}, fmt.Errorf("failed to find scans: %w", err) + } + + scans, err := ConvertToRestScans(dbScans) + if err != nil { + return models.Scans{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return scans, nil +} + +func (s *ScansTableHandler) GetScan(scanID models.ScanID) (models.Scan, error) { + var dbScan Scan + if err := s.scansTable.First(&dbScan, "id = ?", scanID).Error; err != nil { + return models.Scan{}, fmt.Errorf("failed to get scan by id %q: %w", scanID, err) + } + + scan, err := ConvertToRestScan(dbScan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return scan, nil +} + +func (s *ScansTableHandler) CreateScan(scan models.Scan) (models.Scan, error) { + // check if there is already a running scan for that scan config id. + existingSR, exist, err := s.checkExist(scan.ScanConfig.Id) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to check existing scan: %w", err) + } + if exist { + converted, err := ConvertToRestScan(existingSR) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, &common.ConflictError{ + Reason: fmt.Sprintf("There is a running scan with scanConfigID=%s", *existingSR.ScanConfigID), + } + } + + dbScan, err := ConvertToDBScan(scan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to create scan in db: %w", err) + } + + if err := s.scansTable.Create(&dbScan).Error; err != nil { + return models.Scan{}, fmt.Errorf("failed to create scan in db: %w", err) + } + + converted, err := ConvertToRestScan(dbScan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScansTableHandler) SaveScan(scan models.Scan) (models.Scan, error) { + dbScan, err := ConvertToDBScan(scan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to create scan in db: %w", err) + } + + if err := s.scansTable.Save(&dbScan).Error; err != nil { + return models.Scan{}, fmt.Errorf("failed to save scan in db: %w", err) + } + + converted, err := ConvertToRestScan(dbScan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScansTableHandler) UpdateScan(scan models.Scan) (models.Scan, error) { + dbScan, err := ConvertToDBScan(scan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to create scan in db: %w", err) + } + + if err := s.scansTable.Model(dbScan).Updates(&dbScan).Error; err != nil { + return models.Scan{}, fmt.Errorf("failed to update scan in db: %w", err) + } + + converted, err := ConvertToRestScan(dbScan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScansTableHandler) DeleteScan(scanID models.ScanID) error { + if err := s.scansTable.Delete(&Scan{}, scanID).Error; err != nil { + return fmt.Errorf("failed to delete scan: %w", err) + } + return nil +} + +func (s *ScansTableHandler) checkExist(scanConfigID string) (Scan, bool, error) { + var scans []Scan + + tx := s.scansTable.WithContext(context.Background()) + + if err := tx.Where("scan_config_id = ?", scanConfigID).Find(&scans).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return Scan{}, false, nil + } + return Scan{}, false, fmt.Errorf("failed to query: %w", err) + } + + // check if there is a running scan (end time not set) + for i, scan := range scans { + if scan.ScanEndTime == nil { + return scans[i], true, nil + } + } + + return Scan{}, false, nil +} diff --git a/backend/pkg/database/gorm/scan_config.go b/backend/pkg/database/gorm/scan_config.go new file mode 100644 index 000000000..a8bcdfc8c --- /dev/null +++ b/backend/pkg/database/gorm/scan_config.go @@ -0,0 +1,256 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "encoding/json" + "errors" + "fmt" + + jsonpatch "github.com/evanphx/json-patch/v5" + "github.com/google/uuid" + "gorm.io/gorm" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/backend/pkg/common" + "github.com/openclarity/vmclarity/backend/pkg/database/types" +) + +type ScanConfig struct { + ODataObject +} + +type ScanConfigsTableHandler struct { + DB *gorm.DB +} + +func (db *Handler) ScanConfigsTable() types.ScanConfigsTable { + return &ScanConfigsTableHandler{ + DB: db.DB, + } +} + +func (s *ScanConfigsTableHandler) GetScanConfigs(params models.GetScanConfigsParams) (models.ScanConfigs, error) { + var scanConfigs []ScanConfig + err := ODataQuery(s.DB, "ScanConfig", params.Filter, params.Select, params.Expand, params.Top, params.Skip, true, &scanConfigs) + if err != nil { + return models.ScanConfigs{}, err + } + + items := []models.ScanConfig{} + for _, scanConfig := range scanConfigs { + var sc models.ScanConfig + err := json.Unmarshal(scanConfig.Data, &sc) + if err != nil { + return models.ScanConfigs{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + items = append(items, sc) + } + + output := models.ScanConfigs{Items: &items} + + if params.Count != nil && *params.Count { + count, err := ODataCount(s.DB, "ScanConfig", params.Filter) + if err != nil { + return models.ScanConfigs{}, fmt.Errorf("failed to count records: %w", err) + } + output.Count = &count + } + + return output, nil +} + +func (s *ScanConfigsTableHandler) GetScanConfig(scanConfigID models.ScanConfigID, params models.GetScanConfigsScanConfigIDParams) (models.ScanConfig, error) { + var dbScanConfig ScanConfig + filter := fmt.Sprintf("id eq '%s'", scanConfigID) + err := ODataQuery(s.DB, "ScanConfig", &filter, params.Select, params.Expand, nil, nil, false, &dbScanConfig) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return models.ScanConfig{}, types.ErrNotFound + } + return models.ScanConfig{}, err + } + + var sc models.ScanConfig + err = json.Unmarshal(dbScanConfig.Data, &sc) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + + return sc, nil +} + +func (s *ScanConfigsTableHandler) CreateScanConfig(scanConfig models.ScanConfig) (models.ScanConfig, error) { + // Check the user provided the name field + if scanConfig.Name == nil || *scanConfig.Name == "" { + return models.ScanConfig{}, fmt.Errorf("name is a required field") + } + + // Check the user didn't provide an Id + if scanConfig.Id != nil { + return models.ScanConfig{}, fmt.Errorf("can not specify Id field when creating a new ScanConfig") + } + + // Generate a new UUID + newID := uuid.New().String() + scanConfig.Id = &newID + + // TODO(sambetts) Lock the table here to prevent race conditions + // checking the uniqueness. + // + // We might also be able to do this without locking the table by doing + // a single query which includes the uniqueness check like: + // + // INSERT INTO scan_configs(data) SELECT * FROM (SELECT "") AS tmp WHERE NOT EXISTS (SELECT * FROM scan_configs WHERE JSON_EXTRACT(`Data`, '$.Name') = '') LIMIT 1; + // + // This should return 0 affected fields if there is a conflicting + // record in the DB, and should be treated safely by the DB without + // locking the table. + + // Check the existing DB entries to ensure that the name field is unique + var scanConfigs []ScanConfig + filter := fmt.Sprintf("name eq '%s'", *scanConfig.Name) + err := ODataQuery(s.DB, "ScanConfig", &filter, nil, nil, nil, nil, true, &scanConfigs) + if err != nil { + return models.ScanConfig{}, err + } + + if len(scanConfigs) > 0 { + var sc models.ScanConfig + err := json.Unmarshal(scanConfigs[0].Data, &sc) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return sc, &common.ConflictError{ + Reason: fmt.Sprintf("Scan config exists with name=%s", *sc.Name), + } + } + + marshaled, err := json.Marshal(scanConfig) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + newScanConfig := ScanConfig{} + newScanConfig.Data = marshaled + + if err := s.DB.Create(&newScanConfig).Error; err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to create scan config in db: %w", err) + } + + // TODO(sambetts) Maybe this isn't required now because the DB isn't + // creating any of the data (like the ID) so we can just return the + // scanConfig pre-marshal above. + var sc models.ScanConfig + err = json.Unmarshal(newScanConfig.Data, &sc) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + + return sc, nil +} + +func getExistingScanConfigbyID(db *gorm.DB, scanConfig models.ScanConfig) (ScanConfig, error) { + var dbScanConfig ScanConfig + filter := fmt.Sprintf("id eq '%s'", *scanConfig.Id) + err := ODataQuery(db, "ScanConfig", &filter, nil, nil, nil, nil, false, &dbScanConfig) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ScanConfig{}, types.ErrNotFound + } + return ScanConfig{}, err + } + return dbScanConfig, nil +} + +func (s *ScanConfigsTableHandler) SaveScanConfig(scanConfig models.ScanConfig) (models.ScanConfig, error) { + dbScanConfig, err := getExistingScanConfigbyID(s.DB, scanConfig) + if err != nil { + return models.ScanConfig{}, err + } + + marshaled, err := json.Marshal(scanConfig) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + dbScanConfig.Data = marshaled + + if err := s.DB.Save(&dbScanConfig).Error; err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to save scan config in db: %w", err) + } + + // TODO(sambetts) Maybe this isn't required now because the DB isn't + // creating any of the data (like the ID) so we can just return the + // scanConfig pre-marshal above. + var sc models.ScanConfig + err = json.Unmarshal(dbScanConfig.Data, &sc) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return sc, nil +} + +func (s *ScanConfigsTableHandler) UpdateScanConfig(scanConfig models.ScanConfig) (models.ScanConfig, error) { + dbScanConfig, err := getExistingScanConfigbyID(s.DB, scanConfig) + if err != nil { + return models.ScanConfig{}, err + } + + marshaled, err := json.Marshal(scanConfig) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + // Calculate the diffs between the current doc and the user doc + patch, err := jsonpatch.CreateMergePatch(dbScanConfig.Data, marshaled) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to calculate patch changes: %w", err) + } + + // Apply the diff to the doc stored in the DB + updated, err := jsonpatch.MergePatch(dbScanConfig.Data, patch) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to apply patch: %w", err) + } + + dbScanConfig.Data = updated + + if err := s.DB.Save(&dbScanConfig).Error; err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to save scan config in db: %w", err) + } + + // TODO(sambetts) Maybe this isn't required now because the DB isn't + // creating any of the data (like the ID) so we can just return the + // scanConfig pre-marshal above. + var sc models.ScanConfig + err = json.Unmarshal(dbScanConfig.Data, &sc) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return sc, nil +} + +func (s *ScanConfigsTableHandler) DeleteScanConfig(scanConfigID models.ScanConfigID) error { + jsonQuotedID := fmt.Sprintf("\"%s\"", scanConfigID) + if err := s.DB.Where("`Data` -> '$.id' = ?", jsonQuotedID).Delete(&ScanConfig{}).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return types.ErrNotFound + } + return fmt.Errorf("failed to delete scan config: %w", err) + } + return nil +} diff --git a/backend/pkg/database/gorm/scan_result.go b/backend/pkg/database/gorm/scan_result.go new file mode 100644 index 000000000..0fe8fdae8 --- /dev/null +++ b/backend/pkg/database/gorm/scan_result.go @@ -0,0 +1,166 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "context" + "errors" + "fmt" + + "gorm.io/gorm" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/backend/pkg/common" + "github.com/openclarity/vmclarity/backend/pkg/database/types" +) + +const ( + scanResultsTableName = "scan_results" +) + +type ScanResult struct { + Base + + ScanID string `json:"scan_id,omitempty" gorm:"column:scan_id"` + TargetID string `json:"target_id,omitempty" gorm:"column:target_id"` + + Exploits []byte `json:"exploits,omitempty" gorm:"column:exploits"` + Malware []byte `json:"malware,omitempty" gorm:"column:malware"` + Misconfigurations []byte `json:"misconfigurations,omitempty" gorm:"column:misconfigurations"` + Rootkits []byte `json:"rootkits,omitempty" gorm:"column:rootkits"` + Sboms []byte `json:"sboms,omitempty" gorm:"column:sboms"` + Secrets []byte `json:"secrets,omitempty" gorm:"column:secrets"` + Status []byte `json:"status,omitempty" gorm:"column:status"` + Vulnerabilities []byte `json:"vulnerabilities,omitempty" gorm:"column:vulnerabilities"` +} + +type ScanResultsTableHandler struct { + scanResultsTable *gorm.DB +} + +func (db *Handler) ScanResultsTable() types.ScanResultsTable { + return &ScanResultsTableHandler{ + scanResultsTable: db.DB.Table(scanResultsTableName), + } +} + +func (s *ScanResultsTableHandler) GetScanResults(params models.GetScanResultsParams) (models.TargetScanResults, error) { + var scanResults []ScanResult + tx := s.scanResultsTable + if err := tx.Find(&scanResults).Error; err != nil { + return models.TargetScanResults{}, fmt.Errorf("failed to find scan results: %w", err) + } + + converted, err := ConvertToRestScanResults(scanResults) + if err != nil { + return models.TargetScanResults{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScanResultsTableHandler) GetScanResult(scanResultID models.ScanResultID, params models.GetScanResultsScanResultIDParams) (models.TargetScanResult, error) { + var scanResult ScanResult + if err := s.scanResultsTable.Where("id = ?", scanResultID).First(&scanResult).Error; err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to get scan result by id %q: %w", scanResultID, err) + } + + converted, err := ConvertToRestScanResult(scanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScanResultsTableHandler) CreateScanResult(scanResult models.TargetScanResult) (models.TargetScanResult, error) { + // check if there is already a scanResult for that scan id and target id. + existingSR, exist, err := s.checkExist(scanResult.ScanId, scanResult.TargetId) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to check existing scan result: %w", err) + } + if exist { + converted, err := ConvertToRestScanResult(existingSR) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, &common.ConflictError{ + Reason: fmt.Sprintf("Target scan result exists with scanID=%s, targetID=%s", existingSR.ScanID, existingSR.TargetID), + } + } + + dbScanResult, err := ConvertToDBScanResult(scanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + if err := s.scanResultsTable.Create(&dbScanResult).Error; err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to create scan result in db: %w", err) + } + + converted, err := ConvertToRestScanResult(dbScanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScanResultsTableHandler) SaveScanResult(scanResult models.TargetScanResult) (models.TargetScanResult, error) { + dbScanResult, err := ConvertToDBScanResult(scanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + if err := s.scanResultsTable.Save(&dbScanResult).Error; err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to save scan result in db: %w", err) + } + + converted, err := ConvertToRestScanResult(dbScanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScanResultsTableHandler) UpdateScanResult(scanResult models.TargetScanResult) (models.TargetScanResult, error) { + dbScanResult, err := ConvertToDBScanResult(scanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + if err := s.scanResultsTable.Model(dbScanResult).Updates(&dbScanResult).Error; err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to update scan result in db: %w", err) + } + + converted, err := ConvertToRestScanResult(dbScanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScanResultsTableHandler) checkExist(scanID string, targetID string) (ScanResult, bool, error) { + var scanResult ScanResult + + tx := s.scanResultsTable.WithContext(context.Background()) + + if err := tx.Where("scan_id = ? AND target_id = ?", scanID, targetID).First(&scanResult).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ScanResult{}, false, nil + } + return ScanResult{}, false, fmt.Errorf("failed to query: %w", err) + } + + return scanResult, true, nil +} diff --git a/backend/pkg/database/gorm/target.go b/backend/pkg/database/gorm/target.go new file mode 100644 index 000000000..a88772415 --- /dev/null +++ b/backend/pkg/database/gorm/target.go @@ -0,0 +1,176 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "context" + "errors" + "fmt" + + "gorm.io/gorm" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/backend/pkg/common" + "github.com/openclarity/vmclarity/backend/pkg/database/types" +) + +const ( + targetsTableName = "targets" + + targetTypeVM = "VMInfo" + targetTypeDir = "Dir" + targetTypePod = "Pod" +) + +type Target struct { + Base + + Type string `json:"type,omitempty" gorm:"column:type"` + Location *string `json:"location,omitempty" gorm:"column:location"` + + // VMInfo + InstanceID *string `json:"instance_id,omitempty" gorm:"column:instance_id"` + InstanceProvider *string `json:"instance_provider,omitempty" gorm:"column:instance_provider"` + + // PodInfo + PodName *string `json:"pod_name,omitempty" gorm:"column:pod_name"` + + // DirInfo + DirName *string `json:"dir_name,omitempty" gorm:"column:dir_name"` +} + +type TargetsTableHandler struct { + targetsTable *gorm.DB +} + +func (db *Handler) TargetsTable() types.TargetsTable { + return &TargetsTableHandler{ + targetsTable: db.DB.Table(targetsTableName), + } +} + +func (t *TargetsTableHandler) GetTargets(params models.GetTargetsParams) (models.Targets, error) { + var targets []Target + tx := t.targetsTable + if err := tx.Find(&targets).Error; err != nil { + return models.Targets{}, fmt.Errorf("failed to find targets: %w", err) + } + + converted, err := ConvertToRestTargets(targets) + if err != nil { + return models.Targets{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (t *TargetsTableHandler) GetTarget(targetID models.TargetID) (models.Target, error) { + var target Target + if err := t.targetsTable.Where("id = ?", targetID).First(&target).Error; err != nil { + return models.Target{}, fmt.Errorf("failed to get target by id %q: %w", targetID, err) + } + + converted, err := ConvertToRestTarget(target) + if err != nil { + return models.Target{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (t *TargetsTableHandler) CreateTarget(target models.Target) (models.Target, error) { + dbTarget, err := ConvertToDBTarget(target) + if err != nil { + return models.Target{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + existingTarget, exist, err := t.checkExist(dbTarget) + if err != nil { + return models.Target{}, fmt.Errorf("failed to check existing target: %w", err) + } + if exist { + converted, err := ConvertToRestTarget(existingTarget) + if err != nil { + return models.Target{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, &common.ConflictError{ + Reason: fmt.Sprintf("Target exists with the unique constraint combination: %s", getUniqueConstraintsOfTarget(existingTarget)), + } + } + + if err := t.targetsTable.Create(&dbTarget).Error; err != nil { + return models.Target{}, fmt.Errorf("failed to create target in db: %w", err) + } + + converted, err := ConvertToRestTarget(dbTarget) + if err != nil { + return models.Target{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (t *TargetsTableHandler) SaveTarget(target models.Target) (models.Target, error) { + dbTarget, err := ConvertToDBTarget(target) + if err != nil { + return models.Target{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + if err := t.targetsTable.Save(&dbTarget).Error; err != nil { + return models.Target{}, fmt.Errorf("failed to save target in db: %w", err) + } + + converted, err := ConvertToRestTarget(dbTarget) + if err != nil { + return models.Target{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (t *TargetsTableHandler) DeleteTarget(targetID models.TargetID) error { + if err := t.targetsTable.Delete(&Target{}, targetID).Error; err != nil { + return fmt.Errorf("failed to delete target: %w", err) + } + return nil +} + +func (t *TargetsTableHandler) checkExist(target Target) (Target, bool, error) { + var targetFromDB Target + + tx := t.targetsTable.WithContext(context.Background()) + + switch target.Type { + case targetTypeVM: + if err := tx.Where("instance_id = ? AND location = ?", *target.InstanceID, *target.Location).First(&targetFromDB).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return Target{}, false, nil + } + return Target{}, false, fmt.Errorf("failed to query: %w", err) + } + } + + return targetFromDB, true, nil +} + +func getUniqueConstraintsOfTarget(target Target) string { + switch target.Type { + case targetTypeVM: + return fmt.Sprintf("instanceID=%s, region=%s", *target.InstanceID, *target.Location) + case targetTypeDir: + return "unsupported target type Dir" + case targetTypePod: + return "unsupported target type Pod" + default: + return fmt.Sprintf("unknown target type: %v", target.Type) + } +} diff --git a/backend/pkg/database/odatasql/query.go b/backend/pkg/database/odatasql/query.go new file mode 100644 index 000000000..7fbd3c962 --- /dev/null +++ b/backend/pkg/database/odatasql/query.go @@ -0,0 +1,408 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 odatasql + +import ( + "context" + "fmt" + "strings" + "sync" + + "github.com/CiscoM31/godata" +) + +var fixSelectToken sync.Once + +// nolint:cyclop +func BuildCountQuery(schemaMetas map[string]SchemaMeta, schema string, filterString *string) (string, error) { + // Parse top level $filter and create the top level "WHERE" + var where string + if filterString != nil && *filterString != "" { + filterQuery, err := godata.ParseFilterString(context.TODO(), *filterString) + if err != nil { + return "", fmt.Errorf("failed to parse $filter: %w", err) + } + + // Build the WHERE conditions based on the $filter tree + conditions, err := buildWhereFromFilter("Data", filterQuery.Tree) + if err != nil { + return "", fmt.Errorf("failed to build DB query from $filter: %w", err) + } + + where = fmt.Sprintf("WHERE %s", conditions) + } + + table := schemaMetas[schema].Table + if table == "" { + return "", fmt.Errorf("trying to query complex type schema %s with no source table", schema) + } + + return fmt.Sprintf("SELECT COUNT(*) FROM %s %s", table, where), nil +} + +// nolint:cyclop +func BuildSQLQuery(schemaMetas map[string]SchemaMeta, schema string, filterString, selectString, expandString *string, top, skip *int) (string, error) { + // Fix GlobalExpandTokenizer so that it allows for `-` characters in the Literal tokens + fixSelectToken.Do(func() { + godata.GlobalExpandTokenizer.Add("^[a-zA-Z0-9_\\'\\.:\\$ \\*-]+", godata.ExpandTokenLiteral) + }) + + // Parse top level $filter and create the top level "WHERE" + var where string + if filterString != nil && *filterString != "" { + filterQuery, err := godata.ParseFilterString(context.TODO(), *filterString) + if err != nil { + return "", fmt.Errorf("failed to parse $filter: %w", err) + } + + // Build the WHERE conditions based on the $filter tree + conditions, err := buildWhereFromFilter("Data", filterQuery.Tree) + if err != nil { + return "", fmt.Errorf("failed to build DB query from $filter: %w", err) + } + + where = fmt.Sprintf("WHERE %s", conditions) + } + + var selectQuery *godata.GoDataSelectQuery + if selectString != nil && *selectString != "" { + // NOTE(sambetts): + // For now we'll won't parse the data here and instead pass + // just the raw value into the selectTree. The select tree will + // parse the select query using the ExpandParser. If we can + // update the GoData select parser to handle paths properly and + // nest query params then we can switch to parsing select once + // here before passing it to the selectTree. + selectQuery = &godata.GoDataSelectQuery{RawValue: *selectString} + } + + var expandQuery *godata.GoDataExpandQuery + if expandString != nil && *expandString != "" { + var err error + expandQuery, err = godata.ParseExpandString(context.TODO(), *expandString) + if err != nil { + return "", fmt.Errorf("failed to parse $expand ") + } + } + + // Turn the select and expand query params into a tree that can be used + // to build nested select statements for the whole schema. + // + // TODO(sambetts) This should probably also validate that all the + // selected/expanded fields are part of the schema. + selectTree := newSelectTree() + err := selectTree.insert(nil, nil, selectQuery, expandQuery, false) + if err != nil { + return "", fmt.Errorf("failed to parse select and expand: %w", err) + } + + table := schemaMetas[schema].Table + + // Build query selecting fields based on the selectTree + // For now all queries must start with a root "object" so we create a + // complex field meta to represent that object + rootObject := FieldMeta{FieldType: ComplexFieldType, ComplexFieldSchemas: []string{schema}} + selectFields := buildSelectFields(schemaMetas, rootObject, schema, fmt.Sprintf("%s.Data", table), "$", selectTree) + + // Build paging statement + var limitStm string + if top != nil || skip != nil { + limitVal := -1 // Negative means no limit, if no "$top" is specified this is what we want + if top != nil { + limitVal = *top + } + limitStm = fmt.Sprintf("LIMIT %d", limitVal) + + if skip != nil { + limitStm = fmt.Sprintf("%s OFFSET %d", limitStm, *skip) + } + } + + if table == "" { + return "", fmt.Errorf("trying to query complex type schema %s with no source table", schema) + } + + return fmt.Sprintf("SELECT ID, %s AS Data FROM %s %s %s", selectFields, table, where, limitStm), nil +} + +// nolint:cyclop,gocognit,gocyclo +func buildSelectFields(schemaMetas map[string]SchemaMeta, field FieldMeta, identifier, source, path string, st *selectNode) string { + switch field.FieldType { + case PrimitiveFieldType: + // If root of source (path is just $) is primitive just return the source + if path == "$" { + return source + } + return fmt.Sprintf("%s -> '%s'", source, path) + case CollectionFieldType: + newIdentifier := fmt.Sprintf("%sOptions", identifier) + newSource := fmt.Sprintf("%s.value", identifier) + + var where string + var newSelectNode *selectNode + if st != nil { + if st.filter != nil { + conditions, _ := buildWhereFromFilter(newSource, st.filter.Tree) + where = fmt.Sprintf("WHERE %s", conditions) + } + newSelectNode = &selectNode{children: st.children, expand: st.expand} + } + + subQuery := buildSelectFields(schemaMetas, *field.CollectionItemMeta, newIdentifier, newSource, "$", newSelectNode) + return fmt.Sprintf("(SELECT JSON_GROUP_ARRAY(%s) FROM JSON_EACH(%s, '%s') AS %s %s)", subQuery, source, path, identifier, where) + case ComplexFieldType: + // If there are no children in the select tree for this complex + // type, shortcircuit and just return the data from the DB raw, + // as there is no need to build the complex query, and it'll + // ensure that null values are handled correctly. + if st == nil || len(st.children) == 0 { + return fmt.Sprintf("%s -> '%s'", source, path) + } + + objects := []string{} + for _, schemaName := range field.ComplexFieldSchemas { + schema := schemaMetas[schemaName] + + parts := []string{} + if field.DescriminatorProperty != "" { + parts = append(parts, fmt.Sprintf("'%s', '%s'", field.DescriminatorProperty, schemaName)) + } + for key, fm := range schema.Fields { + if field.DescriminatorProperty != "" && key == field.DescriminatorProperty { + continue + } + + var sel *selectNode + if st != nil { + // If there are any select children + // then we need to make sure this is + // either a select child or a expand + // child, otherwise skip this field. + if len(st.selectChildren) > 0 { + _, isSelect := st.selectChildren[key] + _, isExpand := st.expandChildren[key] + if !isSelect && !isExpand { + continue + } + } + sel = st.children[key] + } + + extract := buildSelectFields(schemaMetas, fm, fmt.Sprintf("%s%s", identifier, key), source, fmt.Sprintf("%s.%s", path, key), sel) + part := fmt.Sprintf("'%s', %s", key, extract) + parts = append(parts, part) + } + objects = append(objects, fmt.Sprintf("JSON_OBJECT(%s)", strings.Join(parts, ","))) + } + + if len(objects) == 1 { + return objects[0] + } + + // TODO(sambetts) Error, if multiple schema there must be a + // descriminator, this would be a developer error. Might be + // avoidable if we create a schema builder thing instead of + // just defining it as a variable. + // if field.DescriminatorProperty == "" { + // } + + return fmt.Sprintf("(SELECT %s.value FROM JSON_EACH(JSON_ARRAY(%s)) AS %s WHERE %s.value -> '$.%s' = %s -> '%s.%s')", identifier, strings.Join(objects, ","), identifier, identifier, field.DescriminatorProperty, source, path, field.DescriminatorProperty) + + case RelationshipFieldType: + if st == nil || !st.expand { + return fmt.Sprintf("%s -> '%s'", source, path) + } + + schemaName := field.RelationshipSchema + schema := schemaMetas[schemaName] + newsource := fmt.Sprintf("%s.Data", schema.Table) + parts := []string{} + for key, fm := range schema.Fields { + var sel *selectNode + if st != nil { + // If there are any select children + // then we need to make sure this is + // either a select child or a expand + // child, otherwise skip this field. + if len(st.selectChildren) > 0 { + _, isSelect := st.selectChildren[key] + _, isExpand := st.expandChildren[key] + if !isSelect && !isExpand { + continue + } + } + sel = st.children[key] + } + + extract := buildSelectFields(schemaMetas, fm, fmt.Sprintf("%s%s", identifier, key), newsource, fmt.Sprintf("$.%s", key), sel) + part := fmt.Sprintf("'%s', %s", key, extract) + parts = append(parts, part) + } + object := fmt.Sprintf("JSON_OBJECT(%s)", strings.Join(parts, ",")) + + return fmt.Sprintf("(SELECT %s FROM %s WHERE %s -> '$.%s' == %s -> '%s.%s')", object, schema.Table, newsource, field.RelationshipProperty, source, path, field.RelationshipProperty) + case RelationshipCollectionFieldType: + if st == nil || !st.expand { + return fmt.Sprintf("%s -> '%s'", source, path) + } + + schemaName := field.RelationshipSchema + schema := schemaMetas[schemaName] + newSource := fmt.Sprintf("%s.Data", schema.Table) + + where := fmt.Sprintf("WHERE %s -> '$.%s' = %s.value -> '$.%s'", newSource, field.RelationshipProperty, identifier, field.RelationshipProperty) + if st != nil { + if st.filter != nil { + conditions, _ := buildWhereFromFilter(newSource, st.filter.Tree) + where = fmt.Sprintf("%s and %s", where, conditions) + } + } + + parts := []string{} + for key, fm := range schema.Fields { + var sel *selectNode + if st != nil { + // If there are any select children + // then we need to make sure this is + // either a select child or a expand + // child, otherwise skip this field. + if len(st.selectChildren) > 0 { + _, isSelect := st.selectChildren[key] + _, isExpand := st.expandChildren[key] + if !isSelect && !isExpand { + continue + } + } + sel = st.children[key] + } + + extract := buildSelectFields(schemaMetas, fm, fmt.Sprintf("%s%s", identifier, key), newSource, fmt.Sprintf("$.%s", key), sel) + part := fmt.Sprintf("'%s', %s", key, extract) + parts = append(parts, part) + } + subQuery := fmt.Sprintf("JSON_OBJECT(%s)", strings.Join(parts, ",")) + + return fmt.Sprintf("(SELECT JSON_GROUP_ARRAY(%s) FROM %s,JSON_EACH(%s, '%s') AS %s %s)", subQuery, schema.Table, source, path, identifier, where) + default: + return "" + } +} + +var sqlOperators = map[string]string{ + "eq": "=", + "ne": "!=", + "gt": ">", + "ge": ">=", + "lt": "<", + "le": "<=", + "or": "or", + "contains": "%%%s%%", + "endswith": "%%%s", + "startswith": "%s%%", +} + +func singleQuote(s string) string { + return fmt.Sprintf("'%s'", s) +} + +func buildJSONPathFromParseNode(node *godata.ParseNode) (string, error) { + switch node.Token.Type { + case godata.ExpressionTokenNav: + right, err := buildJSONPathFromParseNode(node.Children[0]) + if err != nil { + return "", fmt.Errorf("unable to build right side of navigation path: %w", err) + } + + left, err := buildJSONPathFromParseNode(node.Children[1]) + if err != nil { + return "", fmt.Errorf("unable to build left side of navigation path: %w", err) + } + return fmt.Sprintf("%s.%s", right, left), nil + case godata.ExpressionTokenLiteral: + return node.Token.Value, nil + default: + return "", fmt.Errorf("unsupported token type") + } +} + +// nolint:cyclop +func buildWhereFromFilter(source string, node *godata.ParseNode) (string, error) { + operator := node.Token.Value + + var query string + switch operator { + case "eq", "ne", "gt", "ge", "lt", "le": + // Convert ODATA paths with slashes like "Thing/Name" into JSON + // path like "Thing.Name". + queryPath, err := buildJSONPathFromParseNode(node.Children[0]) + if err != nil { + return "", fmt.Errorf("unable to covert oData path to json path: %w", err) + } + + rhs := node.Children[1] + extractFunction := "->" + var value string + switch rhs.Token.Type { + case godata.ExpressionTokenString: + value = singleQuote(strings.ReplaceAll(rhs.Token.Value, "'", "\"")) + case godata.ExpressionTokenBoolean: + value = singleQuote(rhs.Token.Value) + case godata.ExpressionTokenInteger, godata.ExpressionTokenFloat: + value = rhs.Token.Value + extractFunction = "->>" + default: + return "", fmt.Errorf("unsupported token type %s", node.Children[1].Token.Type) + } + + query = fmt.Sprintf("%s %s '%s' %s %s", source, extractFunction, queryPath, sqlOperators[operator], value) + case "and": + left, err := buildWhereFromFilter(source, node.Children[0]) + if err != nil { + return query, err + } + right, err := buildWhereFromFilter(source, node.Children[1]) + if err != nil { + return query, err + } + query = fmt.Sprintf("(%s AND %s)", left, right) + case "or": + left, err := buildWhereFromFilter(source, node.Children[0]) + if err != nil { + return query, err + } + right, err := buildWhereFromFilter(source, node.Children[1]) + if err != nil { + return query, err + } + query = fmt.Sprintf("(%s OR %s)", left, right) + case "contains", "endswith", "startswith": + queryField := node.Children[0].Token.Value + queryPath := fmt.Sprintf("$.%s", queryField) + + right := node.Children[1].Token.Value + var value interface{} + switch node.Children[1].Token.Type { + case godata.ExpressionTokenString: + r := strings.ReplaceAll(right, "'", "") + value = fmt.Sprintf(sqlOperators[operator], r) + default: + return query, fmt.Errorf("unsupported token type") + } + query = fmt.Sprintf("%s -> '%s' LIKE '%s'", source, queryPath, value) + } + + return query, nil +} diff --git a/backend/pkg/database/odatasql/query_test.go b/backend/pkg/database/odatasql/query_test.go new file mode 100644 index 000000000..57b875107 --- /dev/null +++ b/backend/pkg/database/odatasql/query_test.go @@ -0,0 +1,831 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 odatasql + +import ( + "encoding/json" + "fmt" + "os" + "path" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + + "gorm.io/datatypes" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +type SubOption struct { + Name string `json:"Name"` +} + +type Options struct { + // Example field of a primitve type inside of a nested complex type + Supercharger bool `json:"Supercharger"` + + // Example field of a collection of complex type inside of a nested + // complex type + SubOptions []SubOption `json:"SubOptions"` + + // Example field of a collection of primitive type inside of a nested + // complex type + OtherThings []string `json:OtherThings` +} + +type Engine struct { + // Example field of a complex type inside of a complex type + Options *Options `json:"Options"` + + // Example field which is a relationship to a different collection + // within a complex property + Manufacturer *Manufacturer `json:"Manufacturer"` +} + +// CDPlayer StereoType. +type CDPlayer struct { + ObjectType string `json:"ObjectType"` + Brand string `json:"Brand"` + NumberOfDisks int `json:"NumberOfDisks"` +} + +// Radio StereoType. +type Radio struct { + ObjectType string `json:"ObjectType"` + Brand string `json:"Brand"` + Frequency string `json:"Frequency"` +} + +type Address struct { + City string `json:"City,omitempty"` + Country string `json:"Country,omitempty"` +} + +type StereoType struct { + union json.RawMessage +} + +func (t StereoType) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err // nolint:wrapcheck +} + +func (t *StereoType) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err // nolint:wrapcheck +} + +func (t *StereoType) FromCDPlayer(v CDPlayer) error { + v.ObjectType = "CDPlayer" + b, err := json.Marshal(v) + t.union = b + return err // nolint:wrapcheck +} + +func (t *StereoType) FromRadio(v Radio) error { + v.ObjectType = "Radio" + b, err := json.Marshal(v) + t.union = b + return err // nolint:wrapcheck +} + +type Manufacturer struct { + ID string `json:"Id"` + Name string `json:"Name,omitempty"` + Address *Address `json:"Address,omitempty"` +} + +type Car struct { + ID string `json:"Id"` + + // Example field which is a primitive type + ModelName string `json:"ModelName"` + Seats int `json:"Seats"` + + // Example field which is a complex property + Engine *Engine `json:"Engine"` + + // Example field which can have different object types + MainStereo StereoType `json:"MainStereo"` + + // Example field which is a collection of different object types + OtherStereos []StereoType `json:"OtherStereos"` + + // Example field which is a relationship to a different collection + Manufacturer *Manufacturer `json:"Manufacturer"` + + // Example field which is a collection of relationships to a + // different collection + Manufacturers []Manufacturer `json:"Manufacturers"` +} + +// Gorm Table Definitions. +type CarRow struct { + gorm.Model + Data datatypes.JSON +} + +type ManufacturerRow struct { + gorm.Model + Data datatypes.JSON +} + +var carSchemaMetas = map[string]SchemaMeta{ + "Car": { + Table: "car_rows", + Fields: map[string]FieldMeta{ + "Id": {FieldType: PrimitiveFieldType}, + "ModelName": {FieldType: PrimitiveFieldType}, + "Seats": {FieldType: PrimitiveFieldType}, + "Engine": { + FieldType: ComplexFieldType, + ComplexFieldSchemas: []string{"Engine"}, + }, + "MainStereo": { + FieldType: ComplexFieldType, + ComplexFieldSchemas: []string{"CDPlayer", "Radio"}, + DescriminatorProperty: "ObjectType", + }, + "OtherStereos": { + FieldType: CollectionFieldType, + CollectionItemMeta: &FieldMeta{ + FieldType: ComplexFieldType, + ComplexFieldSchemas: []string{"CDPlayer", "Radio"}, + DescriminatorProperty: "ObjectType", + }, + }, + "Manufacturer": { + FieldType: RelationshipFieldType, + RelationshipSchema: "Manufacturer", + RelationshipProperty: "Id", + }, + "Manufacturers": { + FieldType: RelationshipCollectionFieldType, + RelationshipSchema: "Manufacturer", + RelationshipProperty: "Id", + }, + }, + }, + "Manufacturer": { + Table: "manufacturer_rows", + Fields: map[string]FieldMeta{ + "Id": {FieldType: PrimitiveFieldType}, + "Name": {FieldType: PrimitiveFieldType}, + "Address": { + FieldType: ComplexFieldType, + ComplexFieldSchemas: []string{"Address"}, + }, + "Source": {FieldType: PrimitiveFieldType}, + }, + }, + "Engine": { + Fields: map[string]FieldMeta{ + "Options": { + FieldType: ComplexFieldType, + ComplexFieldSchemas: []string{"Options"}, + }, + "Manufacturer": { + FieldType: RelationshipFieldType, + RelationshipSchema: "Manufacturer", + RelationshipProperty: "Id", + }, + }, + }, + "Options": { + Fields: map[string]FieldMeta{ + "Supercharger": {FieldType: PrimitiveFieldType}, + "SubOptions": { + FieldType: CollectionFieldType, + CollectionItemMeta: &FieldMeta{ + FieldType: ComplexFieldType, + ComplexFieldSchemas: []string{"SubOption"}, + }, + }, + "OtherThings": { + FieldType: CollectionFieldType, + CollectionItemMeta: &FieldMeta{FieldType: PrimitiveFieldType}, + }, + }, + }, + "SubOption": { + Fields: map[string]FieldMeta{ + "Name": {FieldType: PrimitiveFieldType}, + }, + }, + "CDPlayer": { + Fields: map[string]FieldMeta{ + "ObjectType": {FieldType: PrimitiveFieldType}, + "Brand": {FieldType: PrimitiveFieldType}, + "NumberOfDisks": {FieldType: PrimitiveFieldType}, + }, + }, + "Radio": { + Fields: map[string]FieldMeta{ + "ObjectType": {FieldType: PrimitiveFieldType}, + "Brand": {FieldType: PrimitiveFieldType}, + "Frequency": {FieldType: PrimitiveFieldType}, + }, + }, + "Address": { + Fields: map[string]FieldMeta{ + "City": {FieldType: PrimitiveFieldType}, + "Country": {FieldType: PrimitiveFieldType}, + }, + }, +} + +func addManufacturer(db *gorm.DB, postfix string) (Manufacturer, error) { + id := uuid.New().String() + manufacturer := Manufacturer{ + ID: id, + Name: fmt.Sprintf("manu%s", postfix), + Address: &Address{ + City: fmt.Sprintf("city%s", postfix), + Country: "middleofnowhere", + }, + } + manuBytes, err := json.Marshal(manufacturer) + if err != nil { + return Manufacturer{}, fmt.Errorf("failed to marshal manufacturer: %w", err) + } + manu := ManufacturerRow{Data: manuBytes} + db.Create(&manu) + return manufacturer, nil +} + +func addCar(db *gorm.DB, model, manuID string, otherManus []string, seats int) (Car, error) { + id := uuid.New().String() + + otherMs := []Manufacturer{} + for _, otherManu := range otherManus { + otherMs = append(otherMs, Manufacturer{ + ID: otherManu, + }) + } + + cdPlayer1 := &StereoType{} + err := cdPlayer1.FromCDPlayer(CDPlayer{ + ObjectType: "CDPlayer", + Brand: "Sony", + NumberOfDisks: 12, + }) + if err != nil { + return Car{}, err + } + + cdPlayer2 := &StereoType{} + err = cdPlayer2.FromCDPlayer(CDPlayer{ + ObjectType: "CDPlayer", + Brand: "Unknown", + NumberOfDisks: 50, + }) + if err != nil { + return Car{}, err + } + + radio := &StereoType{} + err = radio.FromRadio(Radio{ + ObjectType: "Radio", + Brand: "Samsung", + Frequency: "500mhz", + }) + if err != nil { + return Car{}, err + } + + car := Car{ + ID: id, + ModelName: model, + Seats: seats, + Engine: &Engine{ + Options: &Options{ + Supercharger: false, + SubOptions: []SubOption{ + { + Name: "bluePaint", + }, + { + Name: "blueShoes", + }, + { + Name: "greenPaint", + }, + { + Name: "yellowPaint", + }, + }, + OtherThings: []string{ + "thing1", + "thing2", + }, + }, + Manufacturer: &Manufacturer{ + ID: manuID, + }, + }, + MainStereo: *cdPlayer1, + OtherStereos: []StereoType{ + *radio, + *cdPlayer2, + }, + Manufacturer: &Manufacturer{ + ID: manuID, + }, + Manufacturers: otherMs, + } + carBytes, err := json.Marshal(car) + if err != nil { + return Car{}, fmt.Errorf("failed to marshal car: %w", err) + } + carRow := CarRow{Data: carBytes} + db.Create(&carRow) + return car, nil +} + +// nolint:cyclop,maintidx +func Test_BuildSQLQuery(t *testing.T) { + dbLogger := logger.Default + dbLogger = dbLogger.LogMode(logger.Info) + + dir, err := os.MkdirTemp("", "") + if err != nil { + t.Errorf("Failed to create tmp dir for database: %v", err) + } + defer os.RemoveAll(dir) + + dbpath := path.Join(dir, "test.db") + db, err := gorm.Open(sqlite.Open(dbpath), &gorm.Config{ + Logger: dbLogger, + }) + if err != nil { + t.Errorf("failed to open db: %v", err) + } + + if err := db.AutoMigrate( + CarRow{}, + ManufacturerRow{}, + ); err != nil { + t.Errorf("failed to run auto migration: %v", err) + } + + manu1, err := addManufacturer(db, "1") + if err != nil { + t.Errorf("failed to add manufacturer to db %v", err) + } + + manu2, err := addManufacturer(db, "2") + if err != nil { + t.Errorf("failed to add manufacturer to db %v", err) + } + + manu3, err := addManufacturer(db, "3") + if err != nil { + t.Errorf("failed to add manufacturer to db %v", err) + } + + car1, err := addCar(db, "model1", manu1.ID, []string{}, 5) + if err != nil { + t.Errorf("failed to add car to db %v", err) + } + + car2, err := addCar(db, "model2", manu1.ID, []string{manu2.ID, manu3.ID}, 5) + if err != nil { + t.Errorf("failed to add car to db %v", err) + } + + car3, err := addCar(db, "model3", manu2.ID, []string{}, 2) + if err != nil { + t.Errorf("failed to add car to db %v", err) + } + + car4, err := addCar(db, "model4", manu3.ID, []string{}, 2) + if err != nil { + t.Errorf("failed to add car to db %v", err) + } + + tests := []struct { + name string + filterString *string + selectString *string + expandString *string + top *int + skip *int + want []Car + buildQueryError bool + }{ + { + name: "All cars, no filter, select, expand or pagination", + filterString: nil, + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1, car2, car3, car4}, + }, + { + name: "eq filter by primitive type ModelName", + filterString: PointerTo("ModelName eq 'model1'"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1}, + }, + { + name: "gt by primitive type Seats", + filterString: PointerTo("Seats gt 2"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1, car2}, + }, + { + name: "gt by primitive type on Seats with float", + filterString: PointerTo("Seats gt 2.0"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1, car2}, + }, + { + name: "gt by primitive type Seats with no results", + filterString: PointerTo("Seats gt 10"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{}, + }, + { + name: "gt by primitive type with float on Seats with no results", + filterString: PointerTo("Seats gt 10.0"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{}, + }, + { + name: "combined 'and' filter", + filterString: PointerTo("Seats gt 2 and ModelName eq 'model2'"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car2}, + }, + { + name: "combined 'and' filter with no results", + filterString: PointerTo("Seats gt 2 and ModelName eq 'doesnotexist'"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{}, + }, + { + name: "combined 'or' filter", + filterString: PointerTo("ModelName eq 'model3' or Seats eq 5"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1, car2, car3}, + }, + { + name: "'contains' filter", + filterString: PointerTo("contains(ModelName, '1')"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1}, + }, + { + name: "filter on nested field", + filterString: PointerTo(fmt.Sprintf("Engine/Manufacturer/Id eq '%s'", manu1.ID)), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1, car2}, + }, + { + name: "mismatched brackets", + filterString: PointerTo("contains(ModelName, '1'"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + buildQueryError: true, + }, + { + name: "non-boolean filter", + filterString: PointerTo("ModelName eq"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + buildQueryError: true, + }, + { + name: "simple select one field", + filterString: nil, + selectString: PointerTo("ModelName"), + expandString: nil, + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car1.ModelName, + }, + { + ModelName: car2.ModelName, + }, + { + ModelName: car3.ModelName, + }, + { + ModelName: car4.ModelName, + }, + }, + }, + { + name: "simple select + select nested field", + filterString: nil, + selectString: PointerTo("Seats,Engine/Manufacturer"), + expandString: nil, + top: nil, + skip: nil, + want: []Car{ + { + Seats: car1.Seats, + Engine: &Engine{ + Manufacturer: car1.Manufacturer, + }, + }, + { + Seats: car2.Seats, + Engine: &Engine{ + Manufacturer: car2.Manufacturer, + }, + }, + { + Seats: car3.Seats, + Engine: &Engine{ + Manufacturer: car3.Manufacturer, + }, + }, + { + Seats: car4.Seats, + Engine: &Engine{ + Manufacturer: car4.Manufacturer, + }, + }, + }, + }, + { + name: "select with nested filter", + filterString: nil, + selectString: PointerTo("ModelName,Engine/Options/SubOptions($filter=contains(Name, 'blue'))"), + expandString: nil, + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car1.ModelName, + Engine: &Engine{ + Options: &Options{ + SubOptions: []SubOption{ + { + Name: "bluePaint", + }, + { + Name: "blueShoes", + }, + }, + }, + }, + }, + { + ModelName: car2.ModelName, + Engine: &Engine{ + Options: &Options{ + SubOptions: []SubOption{ + { + Name: "bluePaint", + }, + { + Name: "blueShoes", + }, + }, + }, + }, + }, + { + ModelName: car3.ModelName, + Engine: &Engine{ + Options: &Options{ + SubOptions: []SubOption{ + { + Name: "bluePaint", + }, + { + Name: "blueShoes", + }, + }, + }, + }, + }, + { + ModelName: car4.ModelName, + Engine: &Engine{ + Options: &Options{ + SubOptions: []SubOption{ + { + Name: "bluePaint", + }, + { + Name: "blueShoes", + }, + }, + }, + }, + }, + }, + }, + { + name: "expand on relationship", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car1.ID)), + selectString: PointerTo("ModelName,Manufacturer"), + expandString: PointerTo("Manufacturer"), + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car1.ModelName, + Manufacturer: &manu1, + }, + }, + }, + { + name: "expand on relationship, relationship not in select", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car1.ID)), + selectString: PointerTo("ModelName"), + expandString: PointerTo("Manufacturer"), + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car1.ModelName, + Manufacturer: &manu1, + }, + }, + }, + { + name: "expand on relationship with nested select", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car1.ID)), + selectString: PointerTo("ModelName"), + expandString: PointerTo("Manufacturer($select=Name)"), + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car1.ModelName, + Manufacturer: &Manufacturer{ + Name: manu1.Name, + }, + }, + }, + }, + { + name: "expand on relationship collection", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car2.ID)), + selectString: PointerTo("ModelName"), + expandString: PointerTo("Manufacturers"), + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car2.ModelName, + Manufacturers: []Manufacturer{ + manu2, + manu3, + }, + }, + }, + }, + { + name: "expand on relationship collection with filter", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car2.ID)), + selectString: PointerTo("ModelName"), + expandString: PointerTo("Manufacturers($filter=Name eq 'manu2')"), + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car2.ModelName, + Manufacturers: []Manufacturer{ + manu2, + }, + }, + }, + }, + { + name: "expand on relationship within complex property", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car2.ID)), + selectString: PointerTo("ModelName,Engine/Manufacturer"), + expandString: PointerTo("Engine/Manufacturer"), + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car2.ModelName, + Engine: &Engine{ + Manufacturer: &manu1, + }, + }, + }, + }, + { + name: "expand mismatched brackets", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car2.ID)), + selectString: PointerTo("ModelName,Engine/Manufacturer"), + expandString: PointerTo("Engine/Manufacturer($filter=Name eq 'foo'"), + top: nil, + skip: nil, + want: []Car{}, + buildQueryError: true, + }, + { + name: "expand bad nested filter", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car2.ID)), + selectString: PointerTo("ModelName,Engine/Manufacturer"), + expandString: PointerTo("Engine/Manufacturer($filter=Name eq)"), + top: nil, + skip: nil, + want: []Car{}, + buildQueryError: true, + }, + } + + for _, test := range tests { + query, err := BuildSQLQuery(carSchemaMetas, "Car", test.filterString, test.selectString, test.expandString, test.top, test.skip) + if err != nil { + if !test.buildQueryError { + t.Errorf("[%s] failed to build query for DB: %v", test.name, err) + } + continue + } + + carResult, err := findCars(db, query) + if err != nil { + t.Errorf("[%s] failed to fetch cars: %v", test.name, err) + continue + } + + if diff := cmp.Diff(test.want, carResult, cmp.AllowUnexported(StereoType{})); diff != "" { + t.Errorf("[%s] []Car mismatch (-want +got):\n%s", test.name, diff) + } + } +} + +func findCars(db *gorm.DB, query string) ([]Car, error) { + var results []CarRow + if err := db.Raw(query).Find(&results).Error; err != nil { + return []Car{}, fmt.Errorf("failed to query DB: %w", err) + } + + carResult := []Car{} + for _, result := range results { + var car Car + err := json.Unmarshal(result.Data, &car) + if err != nil { + return []Car{}, fmt.Errorf("failed to unmarshal car data %v: %w", result.Data, err) + } + carResult = append(carResult, car) + } + + return carResult, nil +} + +func PointerTo[T any](value T) *T { + return &value +} diff --git a/backend/pkg/database/odatasql/schema.go b/backend/pkg/database/odatasql/schema.go new file mode 100644 index 000000000..38678bae8 --- /dev/null +++ b/backend/pkg/database/odatasql/schema.go @@ -0,0 +1,48 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 odatasql + +type FieldType string + +const ( + PrimitiveFieldType FieldType = "primitive" + CollectionFieldType FieldType = "collection" + ComplexFieldType FieldType = "complex" + RelationshipFieldType FieldType = "relationship" + RelationshipCollectionFieldType FieldType = "relationshipCollection" +) + +type FieldMeta struct { + FieldType FieldType + + // Field for collection field types + CollectionItemMeta *FieldMeta + + // Fields for complex field types + ComplexFieldSchemas []string + DescriminatorProperty string + + // Fields for relationship and relationship collection types + RelationshipSchema string + RelationshipProperty string +} + +type SchemaMeta struct { + Table string + Fields map[string]FieldMeta +} + +type Schema map[string]FieldMeta diff --git a/backend/pkg/database/odatasql/selectTree.go b/backend/pkg/database/odatasql/selectTree.go new file mode 100644 index 000000000..6365d35fd --- /dev/null +++ b/backend/pkg/database/odatasql/selectTree.go @@ -0,0 +1,114 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 odatasql + +import ( + "context" + "fmt" + + "github.com/CiscoM31/godata" +) + +type selectNode struct { + children map[string]*selectNode + expandChildren map[string]struct{} + selectChildren map[string]struct{} + filter *godata.GoDataFilterQuery + expand bool +} + +func newSelectTree() *selectNode { + return &selectNode{ + children: map[string]*selectNode{}, + expandChildren: map[string]struct{}{}, + selectChildren: map[string]struct{}{}, + } +} + +// nolint:gocognit,cyclop +func (st *selectNode) insert(path []*godata.Token, filter *godata.GoDataFilterQuery, sel *godata.GoDataSelectQuery, subExpand *godata.GoDataExpandQuery, expand bool) error { + // If path length == 0 then we've reach the bottom of the path and now + // we need to save the filter/select and process any sub + // selects/expands + if len(path) == 0 { + if st.filter != nil { + return fmt.Errorf("filter for field specified twice") + } + st.filter = filter + + st.expand = expand + + if sel != nil { + if len(st.children) > 0 { + return fmt.Errorf("can not specify selection for field in multiple places") + } + + // Parse $select using ParseExpandString because godata.ParseSelectString + // is a nieve implementation and doesn't handle query options properly + childSelections, err := godata.ParseExpandString(context.TODO(), sel.RawValue) + if err != nil { + return fmt.Errorf("failed to parse select: %w", err) + } + + for _, s := range childSelections.ExpandItems { + if s.Expand != nil { + return fmt.Errorf("expand can not be specified inside of select") + } + err := st.insert(s.Path, s.Filter, s.Select, nil, false) + if err != nil { + return err + } + } + } + + if subExpand != nil { + for _, s := range subExpand.ExpandItems { + err := st.insert(s.Path, s.Filter, s.Select, s.Expand, true) + if err != nil { + return err + } + } + } + + return nil + } + + // Keep adding the items in the path to the tree. First find if this + // entry is already in the tree by checking children, if it is then + // insert the next part of the path into that child, otherwise add a + // new child and insert it there. + childName := path[0].Value + child, ok := st.children[childName] + if !ok { + st.children[childName] = newSelectTree() + child = st.children[childName] + } + + // Keep track of which children where added as part of $select or + // $expand, or both. + if expand { + st.expandChildren[childName] = struct{}{} + } else { + st.selectChildren[childName] = struct{}{} + } + + err := child.insert(path[1:], filter, sel, subExpand, expand) + if err != nil { + return err + } + + return nil +} diff --git a/backend/pkg/database/scan.go b/backend/pkg/database/scan.go deleted file mode 100644 index d59632763..000000000 --- a/backend/pkg/database/scan.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. -// All rights reserved. -// -// 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 database - -import ( - "context" - "errors" - "fmt" - "time" - - uuid "github.com/satori/go.uuid" - "gorm.io/gorm" - - "github.com/openclarity/vmclarity/backend/pkg/common" -) - -const ( - scansTableName = "scans" -) - -type Scan struct { - Base - - ScanStartTime *time.Time `json:"scan_start_time,omitempty" gorm:"column:scan_start_time"` - ScanEndTime *time.Time `json:"scan_end_time,omitempty" gorm:"column:scan_end_time"` - - // ScanConfigID The ID of the config that this scan was initiated from (optionanl) - ScanConfigID *string `json:"scan_config_id,omitempty" gorm:"column:scan_config_id"` - // ScanFamiliesConfig The configuration of the scanner families within a scan config - ScanConfigSnapshot []byte `json:"scan_families_config,omitempty" gorm:"column:scan_families_config"` - - // TargetIDs List of target IDs that are targeted for scanning as part of this scan - TargetIDs []byte `json:"target_ids,omitempty" gorm:"column:target_ids"` -} - -type GetScansParams struct { - // Filter Odata filter - Filter *string - // Page Page number of the query - Page *int - // PageSize Maximum items to return - PageSize *int -} - -type ScansTable interface { - GetScansAndTotal(params GetScansParams) ([]*Scan, int64, error) - GetScan(scanID uuid.UUID) (*Scan, error) - UpdateScan(scan *Scan) (*Scan, error) - SaveScan(scan *Scan) (*Scan, error) - DeleteScan(scanID uuid.UUID) error - CreateScan(scan *Scan) (*Scan, error) -} - -type ScansTableHandler struct { - scansTable *gorm.DB -} - -func (db *Handler) ScansTable() ScansTable { - return &ScansTableHandler{ - scansTable: db.DB.Table(scansTableName), - } -} - -func (s *ScansTableHandler) GetScansAndTotal(params GetScansParams) ([]*Scan, int64, error) { - var count int64 - var scans []*Scan - - tx := s.scansTable - - if err := tx.Count(&count).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count total: %w", err) - } - - if err := tx.Find(&scans).Error; err != nil { - return nil, 0, fmt.Errorf("failed to find scans: %w", err) - } - - return scans, count, nil -} - -func (s *ScansTableHandler) CreateScan(scan *Scan) (*Scan, error) { - // check if there is already a running scan for that scan config id. - existingSR, exist, err := s.checkExist(*scan.ScanConfigID) - if err != nil { - return nil, fmt.Errorf("failed to check existing scan: %w", err) - } - if exist { - return existingSR, &common.ConflictError{ - Reason: fmt.Sprintf("There is a running scan with scanConfigID=%s", *existingSR.ScanConfigID), - } - } - - if err := s.scansTable.Create(scan).Error; err != nil { - return nil, fmt.Errorf("failed to create scan in db: %w", err) - } - return scan, nil -} - -func (s *ScansTableHandler) SaveScan(scan *Scan) (*Scan, error) { - if err := s.scansTable.Save(scan).Error; err != nil { - return nil, fmt.Errorf("failed to save scan in db: %w", err) - } - - return scan, nil -} - -func (s *ScansTableHandler) UpdateScan(scan *Scan) (*Scan, error) { - if err := s.scansTable.Model(scan).Updates(scan).Error; err != nil { - return nil, fmt.Errorf("failed to update scan in db: %w", err) - } - return scan, nil -} - -func (s *ScansTableHandler) GetScan(scanID uuid.UUID) (*Scan, error) { - var scan *Scan - - if err := s.scansTable.Where("id = ?", scanID).First(&scan).Error; err != nil { - return nil, fmt.Errorf("failed to get scan by id %q: %w", scanID, err) - } - - return scan, nil -} - -func (s *ScansTableHandler) DeleteScan(scanID uuid.UUID) error { - if err := s.scansTable.Delete(&Scan{}, scanID).Error; err != nil { - return fmt.Errorf("failed to delete scan: %w", err) - } - return nil -} - -func (s *ScansTableHandler) checkExist(scanConfigID string) (*Scan, bool, error) { - var scans []Scan - - tx := s.scansTable.WithContext(context.Background()) - - if err := tx.Where("scan_config_id = ?", scanConfigID).Find(&scans).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, false, nil - } - return nil, false, fmt.Errorf("failed to query: %w", err) - } - - // check if there is a running scan (end time not set) - for i, scan := range scans { - if scan.ScanEndTime == nil { - return &scans[i], true, nil - } - } - - return nil, false, nil -} diff --git a/backend/pkg/database/scan_config.go b/backend/pkg/database/scan_config.go deleted file mode 100644 index f559823e7..000000000 --- a/backend/pkg/database/scan_config.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. -// All rights reserved. -// -// 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 database - -import ( - "context" - "errors" - "fmt" - - uuid "github.com/satori/go.uuid" - "gorm.io/gorm" - - "github.com/openclarity/vmclarity/backend/pkg/common" -) - -const ( - scanConfigsTableName = "scan_configs" -) - -type ScanConfig struct { - Base - - Name *string `json:"name,omitempty" gorm:"column:name"` - - // ScanFamiliesConfig The configuration of the scanner families within a scan config - ScanFamiliesConfig []byte `json:"scan_families_config,omitempty" gorm:"column:scan_families_config"` - Scheduled []byte `json:"scheduled,omitempty" gorm:"column:scheduled"` - Scope []byte `json:"scope,omitempty" gorm:"column:scope"` -} - -type GetScanConfigsParams struct { - // Filter Odata filter - Filter *string - // Page Page number of the query - Page *int - // PageSize Maximum items to return - PageSize *int -} - -type ScanConfigsTable interface { - GetScanConfigsAndTotal(params GetScanConfigsParams) ([]*ScanConfig, int64, error) - GetScanConfig(scanConfigID uuid.UUID) (*ScanConfig, error) - UpdateScanConfig(scanConfig *ScanConfig) (*ScanConfig, error) - SaveScanConfig(scanConfig *ScanConfig) (*ScanConfig, error) - DeleteScanConfig(scanConfigID uuid.UUID) error - CreateScanConfig(scanConfig *ScanConfig) (*ScanConfig, error) -} - -type ScanConfigsTableHandler struct { - scanConfigsTable *gorm.DB -} - -func (db *Handler) ScanConfigsTable() ScanConfigsTable { - return &ScanConfigsTableHandler{ - scanConfigsTable: db.DB.Table(scanConfigsTableName), - } -} - -func (s *ScanConfigsTableHandler) GetScanConfigsAndTotal(params GetScanConfigsParams) ([]*ScanConfig, int64, error) { - var count int64 - var scanConfigs []*ScanConfig - - tx := s.scanConfigsTable - - if err := tx.Count(&count).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count total: %w", err) - } - - if err := tx.Find(&scanConfigs).Error; err != nil { - return nil, 0, fmt.Errorf("failed to find scan configs: %w", err) - } - - return scanConfigs, count, nil -} - -func (s *ScanConfigsTableHandler) CreateScanConfig(scanConfig *ScanConfig) (*ScanConfig, error) { - // check if there is already a scan config with that name. - existingSR, exist, err := s.checkExist(*scanConfig.Name) - if err != nil { - return nil, fmt.Errorf("failed to check existing scan config: %w", err) - } - if exist { - return existingSR, &common.ConflictError{ - Reason: fmt.Sprintf("Scan config exists with name=%s", *existingSR.Name), - } - } - - if err := s.scanConfigsTable.Create(scanConfig).Error; err != nil { - return nil, fmt.Errorf("failed to create scan config in db: %w", err) - } - return scanConfig, nil -} - -func (s *ScanConfigsTableHandler) SaveScanConfig(scanConfig *ScanConfig) (*ScanConfig, error) { - if err := s.scanConfigsTable.Save(scanConfig).Error; err != nil { - return nil, fmt.Errorf("failed to save scan config in db: %w", err) - } - - return scanConfig, nil -} - -func (s *ScanConfigsTableHandler) UpdateScanConfig(scanConfig *ScanConfig) (*ScanConfig, error) { - if err := s.scanConfigsTable.Model(scanConfig).Updates(scanConfig).Error; err != nil { - return nil, fmt.Errorf("failed to update scan config in db: %w", err) - } - - return scanConfig, nil -} - -func (s *ScanConfigsTableHandler) GetScanConfig(scanConfigID uuid.UUID) (*ScanConfig, error) { - var scanConfig *ScanConfig - - if err := s.scanConfigsTable.Where("id = ?", scanConfigID).First(&scanConfig).Error; err != nil { - return nil, fmt.Errorf("failed to get scan config by id %q: %w", scanConfigID, err) - } - - return scanConfig, nil -} - -func (s *ScanConfigsTableHandler) DeleteScanConfig(scanConfigID uuid.UUID) error { - if err := s.scanConfigsTable.Delete(&Scan{}, scanConfigID).Error; err != nil { - return fmt.Errorf("failed to delete scan config: %w", err) - } - return nil -} - -func (s *ScanConfigsTableHandler) checkExist(name string) (*ScanConfig, bool, error) { - var scanConfig *ScanConfig - - tx := s.scanConfigsTable.WithContext(context.Background()) - - if err := tx.Where("name = ?", name).First(&scanConfig).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, false, nil - } - return nil, false, fmt.Errorf("failed to query: %w", err) - } - - return scanConfig, true, nil -} diff --git a/backend/pkg/database/scan_result.go b/backend/pkg/database/scan_result.go deleted file mode 100644 index e5b5484b6..000000000 --- a/backend/pkg/database/scan_result.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. -// All rights reserved. -// -// 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 database - -import ( - "context" - "errors" - "fmt" - - uuid "github.com/satori/go.uuid" - "gorm.io/gorm" - - "github.com/openclarity/vmclarity/backend/pkg/common" -) - -const ( - scanResultsTableName = "scan_results" -) - -type ScanResult struct { - Base - - ScanID string `json:"scan_id,omitempty" gorm:"column:scan_id"` - TargetID string `json:"target_id,omitempty" gorm:"column:target_id"` - - Exploits []byte `json:"exploits,omitempty" gorm:"column:exploits"` - Malware []byte `json:"malware,omitempty" gorm:"column:malware"` - Misconfigurations []byte `json:"misconfigurations,omitempty" gorm:"column:misconfigurations"` - Rootkits []byte `json:"rootkits,omitempty" gorm:"column:rootkits"` - Sboms []byte `json:"sboms,omitempty" gorm:"column:sboms"` - Secrets []byte `json:"secrets,omitempty" gorm:"column:secrets"` - Status []byte `json:"status,omitempty" gorm:"column:status"` - Vulnerabilities []byte `json:"vulnerabilities,omitempty" gorm:"column:vulnerabilities"` -} - -type GetScanResultsParams struct { - // Filter Odata filter - Filter *string - // Select Odata select - Select *string - // Page Page number of the query - Page *int - // PageSize Maximum items to return - PageSize *int -} - -type GetScanResultsScanResultIDParams struct { - // Select Odata select - Select *string -} - -//nolint:interfacebloat -type ScanResultsTable interface { - CreateScanResult(scanResults *ScanResult) (*ScanResult, error) - GetScanResultsAndTotal(params GetScanResultsParams) ([]*ScanResult, int64, error) - GetScanResult(scanResultID uuid.UUID, params GetScanResultsScanResultIDParams) (*ScanResult, error) - UpdateScanResult(scanResults *ScanResult) (*ScanResult, error) - SaveScanResult(scanResults *ScanResult) (*ScanResult, error) -} - -type ScanResultsTableHandler struct { - scanResultsTable *gorm.DB -} - -func (db *Handler) ScanResultsTable() ScanResultsTable { - return &ScanResultsTableHandler{ - scanResultsTable: db.DB.Table(scanResultsTableName), - } -} - -func (s *ScanResultsTableHandler) GetScanResult(scanResultID uuid.UUID, params GetScanResultsScanResultIDParams) (*ScanResult, error) { - var scanResult *ScanResult - - if err := s.scanResultsTable.Where("id = ?", scanResultID).First(&scanResult).Error; err != nil { - return nil, fmt.Errorf("failed to get scan result by id %q: %w", scanResultID, err) - } - - return scanResult, nil -} - -func (s *ScanResultsTableHandler) CreateScanResult(scanResult *ScanResult) (*ScanResult, error) { - // check if there is already a scanResult for that scan id and target id. - existingSR, exist, err := s.checkExist(scanResult.ScanID, scanResult.TargetID) - if err != nil { - return nil, fmt.Errorf("failed to check existing scan result: %w", err) - } - if exist { - return existingSR, &common.ConflictError{ - Reason: fmt.Sprintf("Target scan result exists with scanID=%s, targetID=%s", existingSR.ScanID, existingSR.TargetID), - } - } - - if err := s.scanResultsTable.Create(scanResult).Error; err != nil { - return nil, fmt.Errorf("failed to create scan result in db: %w", err) - } - return scanResult, nil -} - -func (s *ScanResultsTableHandler) GetScanResultsAndTotal(params GetScanResultsParams) ([]*ScanResult, int64, error) { - var count int64 - var scanResults []*ScanResult - - tx := s.scanResultsTable - - if err := tx.Count(&count).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count total: %w", err) - } - - if err := tx.Find(&scanResults).Error; err != nil { - return nil, 0, fmt.Errorf("failed to find scan results: %w", err) - } - - return scanResults, count, nil -} - -func (s *ScanResultsTableHandler) SaveScanResult(scanResult *ScanResult) (*ScanResult, error) { - if err := s.scanResultsTable.Save(scanResult).Error; err != nil { - return nil, fmt.Errorf("failed to save scan result in db: %w", err) - } - - return scanResult, nil -} - -func (s *ScanResultsTableHandler) UpdateScanResult(scanResult *ScanResult) (*ScanResult, error) { - if err := s.scanResultsTable.Model(scanResult).Updates(scanResult).Error; err != nil { - return nil, fmt.Errorf("failed to update scan result in db: %w", err) - } - - return scanResult, nil -} - -func (s *ScanResultsTableHandler) checkExist(scanID string, targetID string) (*ScanResult, bool, error) { - var scanResult *ScanResult - - tx := s.scanResultsTable.WithContext(context.Background()) - - if err := tx.Where("scan_id = ? AND target_id = ?", scanID, targetID).First(&scanResult).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, false, nil - } - return nil, false, fmt.Errorf("failed to query: %w", err) - } - - return scanResult, true, nil -} diff --git a/backend/pkg/database/target.go b/backend/pkg/database/target.go deleted file mode 100644 index 1eeb68a3a..000000000 --- a/backend/pkg/database/target.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. -// All rights reserved. -// -// 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 database - -import ( - "context" - "errors" - "fmt" - - uuid "github.com/satori/go.uuid" - "gorm.io/gorm" - - "github.com/openclarity/vmclarity/backend/pkg/common" -) - -const ( - targetsTableName = "targets" -) - -type Target struct { - Base - - Type string `json:"type,omitempty" gorm:"column:type"` - Location *string `json:"location,omitempty" gorm:"column:location"` - - // VMInfo - InstanceID *string `json:"instance_id,omitempty" gorm:"column:instance_id"` - InstanceProvider *string `json:"instance_provider,omitempty" gorm:"column:instance_provider"` - - // PodInfo - PodName *string `json:"pod_name,omitempty" gorm:"column:pod_name"` - - // DirInfo - DirName *string `json:"dir_name,omitempty" gorm:"column:dir_name"` -} - -type GetTargetsParams struct { - // Filter Odata filter - Filter *string - // Page Page number of the query - Page *int - // PageSize Maximum items to return - PageSize *int -} - -type TargetsTable interface { - GetTargetsAndTotal(params GetTargetsParams) ([]*Target, int64, error) - GetTarget(targetID uuid.UUID) (*Target, error) - CreateTarget(target *Target) (*Target, error) - SaveTarget(target *Target) (*Target, error) - DeleteTarget(targetID uuid.UUID) error -} - -type TargetsTableHandler struct { - targetsTable *gorm.DB -} - -func (db *Handler) TargetsTable() TargetsTable { - return &TargetsTableHandler{ - targetsTable: db.DB.Table(targetsTableName), - } -} - -func (t *TargetsTableHandler) GetTargetsAndTotal(params GetTargetsParams) ([]*Target, int64, error) { - var count int64 - var targets []*Target - - tx := t.targetsTable - - if err := tx.Count(&count).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count total: %w", err) - } - - if err := tx.Find(&targets).Error; err != nil { - return nil, 0, fmt.Errorf("failed to find targets: %w", err) - } - - return targets, count, nil -} - -func (t *TargetsTableHandler) GetTarget(targetID uuid.UUID) (*Target, error) { - var target *Target - - if err := t.targetsTable.Where("id = ?", targetID).First(&target).Error; err != nil { - return nil, fmt.Errorf("failed to get target by id %q: %w", targetID, err) - } - - return target, nil -} - -func (t *TargetsTableHandler) CreateTarget(target *Target) (*Target, error) { - existingTarget, exist, err := t.checkExist(target) - if err != nil { - return nil, fmt.Errorf("failed to check existing target: %w", err) - } - if exist { - return existingTarget, &common.ConflictError{ - Reason: fmt.Sprintf("Target exists with the unique constraint combination: %s", getUniqueConstraintsOfTarget(existingTarget)), - } - } - - if err := t.targetsTable.Create(target).Error; err != nil { - return nil, fmt.Errorf("failed to create target in db: %w", err) - } - return target, nil -} - -func (t *TargetsTableHandler) SaveTarget(target *Target) (*Target, error) { - if err := t.targetsTable.Save(target).Error; err != nil { - return nil, fmt.Errorf("failed to save target in db: %w", err) - } - - return target, nil -} - -func (t *TargetsTableHandler) DeleteTarget(targetID uuid.UUID) error { - if err := t.targetsTable.Delete(&Scan{}, targetID).Error; err != nil { - return fmt.Errorf("failed to delete target: %w", err) - } - return nil -} - -func (t *TargetsTableHandler) checkExist(target *Target) (*Target, bool, error) { - var targetFromDB Target - - tx := t.targetsTable.WithContext(context.Background()) - - switch target.Type { - case "VMInfo": - if err := tx.Where("instance_id = ? AND location = ?", *target.InstanceID, *target.Location).First(&targetFromDB).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, false, nil - } - return nil, false, fmt.Errorf("failed to query: %w", err) - } - } - - return &targetFromDB, true, nil -} - -func getUniqueConstraintsOfTarget(target *Target) string { - switch target.Type { - case "VMInfo": - return fmt.Sprintf("instanceID=%s, region=%s", *target.InstanceID, *target.Location) - case "Dir": - return "unsupported target type Dir" - case "Pod": - return "unsupported target type Pod" - default: - return fmt.Sprintf("unknown target type: %v", target.Type) - } -} diff --git a/backend/pkg/database/types/database.go b/backend/pkg/database/types/database.go new file mode 100644 index 000000000..7e7b9783c --- /dev/null +++ b/backend/pkg/database/types/database.go @@ -0,0 +1,91 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 types + +import ( + "errors" + + "github.com/openclarity/vmclarity/api/models" +) + +const ( + DBDriverTypeLocal = "LOCAL" +) + +var ErrNotFound = errors.New("not found") + +type DBConfig struct { + EnableInfoLogs bool `json:"enable-info-logs"` + DriverType string `json:"driver-type,omitempty"` + DBPassword string `json:"-"` + DBUser string `json:"db-user,omitempty"` + DBHost string `json:"db-host,omitempty"` + DBPort string `json:"db-port,omitempty"` + DBName string `json:"db-name,omitempty"` + + LocalDBPath string `json:"local-db-path,omitempty"` +} + +type Database interface { + ScanResultsTable() ScanResultsTable + ScanConfigsTable() ScanConfigsTable + ScansTable() ScansTable + TargetsTable() TargetsTable +} + +type ScansTable interface { + GetScans(params models.GetScansParams) (models.Scans, error) + GetScan(scanID models.ScanID) (models.Scan, error) + + CreateScan(scan models.Scan) (models.Scan, error) + UpdateScan(scan models.Scan) (models.Scan, error) + SaveScan(scan models.Scan) (models.Scan, error) + + DeleteScan(scanID models.ScanID) error +} + +type ScanResultsTable interface { + GetScanResults(params models.GetScanResultsParams) (models.TargetScanResults, error) + GetScanResult(scanResultID models.ScanResultID, params models.GetScanResultsScanResultIDParams) (models.TargetScanResult, error) + + CreateScanResult(scanResults models.TargetScanResult) (models.TargetScanResult, error) + UpdateScanResult(scanResults models.TargetScanResult) (models.TargetScanResult, error) + SaveScanResult(scanResults models.TargetScanResult) (models.TargetScanResult, error) + + // DeleteScanResult(scanResultID models.ScanResultID) error +} + +type ScanConfigsTable interface { + GetScanConfigs(params models.GetScanConfigsParams) (models.ScanConfigs, error) + GetScanConfig(scanConfigID models.ScanConfigID, params models.GetScanConfigsScanConfigIDParams) (models.ScanConfig, error) + + CreateScanConfig(scanConfig models.ScanConfig) (models.ScanConfig, error) + UpdateScanConfig(scanConfig models.ScanConfig) (models.ScanConfig, error) + SaveScanConfig(scanConfig models.ScanConfig) (models.ScanConfig, error) + + DeleteScanConfig(scanConfigID models.ScanConfigID) error +} + +type TargetsTable interface { + GetTargets(params models.GetTargetsParams) (models.Targets, error) + GetTarget(targetID models.TargetID) (models.Target, error) + + CreateTarget(target models.Target) (models.Target, error) + // UpdateTarget(target models.Target) (models.Target, error) + SaveTarget(target models.Target) (models.Target, error) + + DeleteTarget(targetID models.TargetID) error +} diff --git a/backend/pkg/rest/convert/dbtorest/convert_test.go b/backend/pkg/rest/convert/dbtorest/convert_test.go deleted file mode 100644 index 3a8df2fbe..000000000 --- a/backend/pkg/rest/convert/dbtorest/convert_test.go +++ /dev/null @@ -1,352 +0,0 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. -// All rights reserved. -// -// 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 dbtorest - -import ( - "encoding/json" - "reflect" - "testing" - - uuid "github.com/satori/go.uuid" - "gotest.tools/v3/assert" - - "github.com/openclarity/vmclarity/api/models" - "github.com/openclarity/vmclarity/backend/pkg/database" - "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" -) - -func TestConvertScanConfig(t *testing.T) { - scanFamiliesConfig := models.ScanFamiliesConfig{ - Vulnerabilities: &models.VulnerabilitiesConfig{Enabled: utils.BoolPtr(true)}, - } - - scanFamiliesConfigB, err := json.Marshal(&scanFamiliesConfig) - assert.NilError(t, err) - - awsScanScope := models.AwsScanScope{ - All: utils.BoolPtr(true), - InstanceTagExclusion: nil, - InstanceTagSelector: nil, - ObjectType: "AwsScanScope", - Regions: nil, - ShouldScanStoppedInstances: utils.BoolPtr(false), - } - - var scanScopeType models.ScanScopeType - - err = scanScopeType.FromAwsScanScope(awsScanScope) - assert.NilError(t, err) - - scanScopeTypeB, err := scanScopeType.MarshalJSON() - assert.NilError(t, err) - - byHoursScheduleScanConfig := models.ByHoursScheduleScanConfig{ - HoursInterval: utils.IntPtr(2), - ObjectType: "ByHoursScheduleScanConfig", - } - - var runtimeScheduleScanConfigType models.RuntimeScheduleScanConfigType - err = runtimeScheduleScanConfigType.FromByHoursScheduleScanConfig(byHoursScheduleScanConfig) - assert.NilError(t, err) - - runtimeScheduleScanConfigTypeB, err := runtimeScheduleScanConfigType.MarshalJSON() - assert.NilError(t, err) - - uid := uuid.NewV4() - - type args struct { - config *database.ScanConfig - } - tests := []struct { - name string - args args - want *models.ScanConfig - wantErr bool - }{ - { - name: "sanity", - args: args{ - config: &database.ScanConfig{ - Base: database.Base{ - ID: uid, - }, - Name: utils.StringPtr("test"), - ScanFamiliesConfig: scanFamiliesConfigB, - Scheduled: runtimeScheduleScanConfigTypeB, - Scope: scanScopeTypeB, - }, - }, - want: &models.ScanConfig{ - Id: utils.StringPtr(uid.String()), - Name: utils.StringPtr("test"), - ScanFamiliesConfig: &scanFamiliesConfig, - Scheduled: &runtimeScheduleScanConfigType, - Scope: &scanScopeType, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScanConfig(tt.args.config) - if (err != nil) != tt.wantErr { - t.Errorf("ConvertScanConfig() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScanConfig() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestConvertScanConfigs(t *testing.T) { - scanFamiliesConfig := models.ScanFamiliesConfig{ - Vulnerabilities: &models.VulnerabilitiesConfig{Enabled: utils.BoolPtr(true)}, - } - - scanFamiliesConfigB, err := json.Marshal(&scanFamiliesConfig) - assert.NilError(t, err) - - awsScanScope := models.AwsScanScope{ - All: utils.BoolPtr(true), - InstanceTagExclusion: nil, - InstanceTagSelector: nil, - ObjectType: "AwsScanScope", - Regions: nil, - ShouldScanStoppedInstances: utils.BoolPtr(false), - } - - var scanScopeType models.ScanScopeType - - err = scanScopeType.FromAwsScanScope(awsScanScope) - assert.NilError(t, err) - - scanScopeTypeB, err := scanScopeType.MarshalJSON() - assert.NilError(t, err) - - byHoursScheduleScanConfig := models.ByHoursScheduleScanConfig{ - HoursInterval: utils.IntPtr(2), - ObjectType: "ByHoursScheduleScanConfig", - } - - var runtimeScheduleScanConfigType models.RuntimeScheduleScanConfigType - err = runtimeScheduleScanConfigType.FromByHoursScheduleScanConfig(byHoursScheduleScanConfig) - assert.NilError(t, err) - - runtimeScheduleScanConfigTypeB, err := runtimeScheduleScanConfigType.MarshalJSON() - assert.NilError(t, err) - - uid := uuid.NewV4() - total := 1 - - type args struct { - configs []*database.ScanConfig - total int64 - } - tests := []struct { - name string - args args - want *models.ScanConfigs - wantErr bool - }{ - { - name: "sanity", - args: args{ - configs: []*database.ScanConfig{ - { - Base: database.Base{ - ID: uid, - }, - Name: utils.StringPtr("test"), - ScanFamiliesConfig: scanFamiliesConfigB, - Scheduled: runtimeScheduleScanConfigTypeB, - Scope: scanScopeTypeB, - }, - }, - total: 1, - }, - want: &models.ScanConfigs{ - Items: &[]models.ScanConfig{ - { - Id: utils.StringPtr(uid.String()), - Name: utils.StringPtr("test"), - ScanFamiliesConfig: &scanFamiliesConfig, - Scheduled: &runtimeScheduleScanConfigType, - Scope: &scanScopeType, - }, - }, - Total: &total, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScanConfigs(tt.args.configs, tt.args.total) - if (err != nil) != tt.wantErr { - t.Errorf("ConvertScanConfigs() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScanConfigs() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestConvertScanResult(t *testing.T) { - state := models.DONE - status := models.TargetScanStatus{ - Vulnerabilities: &models.TargetScanState{ - Errors: nil, - State: &state, - }, - Exploits: &models.TargetScanState{ - Errors: &[]string{"err"}, - State: &state, - }, - } - - vulsScan := models.VulnerabilityScan{ - Vulnerabilities: &[]models.Vulnerability{ - { - VulnerabilityInfo: &models.VulnerabilityInfo{ - VulnerabilityName: utils.StringPtr("name"), - }, - }, - }, - } - - vulScanB, err := json.Marshal(&vulsScan) - assert.NilError(t, err) - - statusB, err := json.Marshal(&status) - assert.NilError(t, err) - - uid := uuid.NewV4() - - type args struct { - scanResult *database.ScanResult - } - tests := []struct { - name string - args args - want *models.TargetScanResult - wantErr bool - }{ - { - name: "sanity", - args: args{ - scanResult: &database.ScanResult{ - Base: database.Base{ - ID: uid, - }, - ScanID: "1", - TargetID: "2", - Status: statusB, - Vulnerabilities: vulScanB, - }, - }, - want: &models.TargetScanResult{ - Id: utils.StringPtr(uid.String()), - ScanId: "1", - Status: &status, - TargetId: "2", - Vulnerabilities: &vulsScan, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScanResult(tt.args.scanResult) - if (err != nil) != tt.wantErr { - t.Errorf("ConvertScanResult() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScanResult() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestConvertScan(t *testing.T) { - scanSnap := models.ScanConfigData{ - ScanFamiliesConfig: &models.ScanFamiliesConfig{ - Vulnerabilities: &models.VulnerabilitiesConfig{ - Enabled: nil, - }, - }, - } - - scanSnapB, err := json.Marshal(&scanSnap) - assert.NilError(t, err) - - targetIDs := []string{"s"} - targetIDsB, err := json.Marshal(&targetIDs) - assert.NilError(t, err) - - id := uuid.NewV4() - - type args struct { - scan *database.Scan - } - tests := []struct { - name string - args args - want *models.Scan - wantErr bool - }{ - { - name: "sanity", - args: args{ - scan: &database.Scan{ - Base: database.Base{ - ID: id, - }, - ScanStartTime: nil, - ScanEndTime: nil, - ScanConfigID: utils.StringPtr("1"), - ScanConfigSnapshot: scanSnapB, - TargetIDs: targetIDsB, - }, - }, - want: &models.Scan{ - Id: utils.StringPtr(id.String()), - ScanConfig: &models.ScanConfigRelationship{ - Id: "1", - }, - ScanConfigSnapshot: &scanSnap, - TargetIDs: &targetIDs, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScan(tt.args.scan) - if (err != nil) != tt.wantErr { - t.Errorf("ConvertScan() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScan() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/backend/pkg/rest/convert/resttodb/params.go b/backend/pkg/rest/convert/resttodb/params.go deleted file mode 100644 index 6e4b7ac76..000000000 --- a/backend/pkg/rest/convert/resttodb/params.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. -// All rights reserved. -// -// 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 resttodb - -import ( - "github.com/openclarity/vmclarity/api/models" - "github.com/openclarity/vmclarity/backend/pkg/database" -) - -func ConvertGetTargetsParams(params models.GetTargetsParams) database.GetTargetsParams { - return database.GetTargetsParams{ - Filter: params.Filter, - Page: params.Page, - PageSize: params.PageSize, - } -} - -func ConvertGetScanResultsParams(params models.GetScanResultsParams) database.GetScanResultsParams { - return database.GetScanResultsParams{ - Filter: params.Filter, - Select: params.Select, - Page: params.Page, - PageSize: params.PageSize, - } -} - -func ConvertGetScanResultsScanResultIDParams(params models.GetScanResultsScanResultIDParams) database.GetScanResultsScanResultIDParams { - return database.GetScanResultsScanResultIDParams{ - Select: params.Select, - } -} - -func ConvertGetScansParams(params models.GetScansParams) database.GetScansParams { - return database.GetScansParams{ - Filter: params.Filter, - Page: params.Page, - PageSize: params.PageSize, - } -} - -func ConvertGetScanConfigsParams(params models.GetScanConfigsParams) database.GetScanConfigsParams { - return database.GetScanConfigsParams{ - Filter: params.Filter, - Page: params.Page, - PageSize: params.PageSize, - } -} diff --git a/backend/pkg/rest/scan_config_controller.go b/backend/pkg/rest/scan_config_controller.go index 2c31a6ea9..3efed708c 100644 --- a/backend/pkg/rest/scan_config_controller.go +++ b/backend/pkg/rest/scan_config_controller.go @@ -21,27 +21,30 @@ import ( "net/http" "github.com/labstack/echo/v4" - uuid "github.com/satori/go.uuid" - "gorm.io/gorm" "github.com/openclarity/vmclarity/api/models" "github.com/openclarity/vmclarity/backend/pkg/common" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/dbtorest" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/resttodb" + databaseTypes "github.com/openclarity/vmclarity/backend/pkg/database/types" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) func (s *ServerImpl) GetScanConfigs(ctx echo.Context, params models.GetScanConfigsParams) error { - dbScanConfigs, total, err := s.dbHandler.ScanConfigsTable().GetScanConfigsAndTotal(resttodb.ConvertGetScanConfigsParams(params)) + scanConfigs, err := s.dbHandler.ScanConfigsTable().GetScanConfigs(params) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan configs from db: %v", err)) } + return sendResponse(ctx, http.StatusOK, scanConfigs) +} - converted, err := dbtorest.ConvertScanConfigs(dbScanConfigs, total) +func (s *ServerImpl) GetScanConfigsScanConfigID(ctx echo.Context, scanConfigID models.ScanConfigID, params models.GetScanConfigsScanConfigIDParams) error { + sc, err := s.dbHandler.ScanConfigsTable().GetScanConfig(scanConfigID, params) if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan configs: %v", err)) + if errors.Is(err, databaseTypes.ErrNotFound) { + return sendError(ctx, http.StatusNotFound, fmt.Sprintf("ScanConfig with ID %v not found", scanConfigID)) + } + return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan config from db. scanConfigID=%v: %v", scanConfigID, err)) } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, sc) } func (s *ServerImpl) PostScanConfigs(ctx echo.Context) error { @@ -51,32 +54,20 @@ func (s *ServerImpl) PostScanConfigs(ctx echo.Context) error { return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScanConfig(&scanConfig, "") - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config: %v", err)) - } - createdScanConfig, err := s.dbHandler.ScanConfigsTable().CreateScanConfig(convertedDB) + createdScanConfig, err := s.dbHandler.ScanConfigsTable().CreateScanConfig(scanConfig) if err != nil { var conflictErr *common.ConflictError if errors.As(err, &conflictErr) { - convertedExist, err := dbtorest.ConvertScanConfig(createdScanConfig) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert existing scan config: %v", err)) - } existResponse := &models.ScanConfigExists{ Message: utils.StringPtr(conflictErr.Reason), - ScanConfig: convertedExist, + ScanConfig: &createdScanConfig, } return sendResponse(ctx, http.StatusConflict, existResponse) } return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to create scan config in db: %v", err)) } - converted, err := dbtorest.ConvertScanConfig(createdScanConfig) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config: %v", err)) - } - return sendResponse(ctx, http.StatusCreated, converted) + return sendResponse(ctx, http.StatusCreated, createdScanConfig) } func (s *ServerImpl) DeleteScanConfigsScanConfigID(ctx echo.Context, scanConfigID models.ScanConfigID) error { @@ -84,14 +75,9 @@ func (s *ServerImpl) DeleteScanConfigsScanConfigID(ctx echo.Context, scanConfigI Message: utils.StringPtr(fmt.Sprintf("scan config %v deleted", scanConfigID)), } - scanConfigUUID, err := uuid.FromString(scanConfigID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scanConfigID %v to uuid: %v", scanConfigID, err)) - } - - if err := s.dbHandler.ScanConfigsTable().DeleteScanConfig(scanConfigUUID); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return sendError(ctx, http.StatusNotFound, err.Error()) + if err := s.dbHandler.ScanConfigsTable().DeleteScanConfig(scanConfigID); err != nil { + if errors.Is(err, databaseTypes.ErrNotFound) { + return sendError(ctx, http.StatusNotFound, fmt.Sprintf("ScanConfig with ID %v not found", scanConfigID)) } return sendError(ctx, http.StatusInternalServerError, err.Error()) } @@ -99,27 +85,6 @@ func (s *ServerImpl) DeleteScanConfigsScanConfigID(ctx echo.Context, scanConfigI return sendResponse(ctx, http.StatusNoContent, &success) } -func (s *ServerImpl) GetScanConfigsScanConfigID(ctx echo.Context, scanConfigID models.ScanConfigID) error { - scanConfigUUID, err := uuid.FromString(scanConfigID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scanConfigID %v to uuid: %v", scanConfigID, err)) - } - - sc, err := s.dbHandler.ScanConfigsTable().GetScanConfig(scanConfigUUID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return sendError(ctx, http.StatusNotFound, err.Error()) - } - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan config from db. scanConfigID=%v: %v", scanConfigID, err)) - } - - converted, err := dbtorest.ConvertScanConfig(sc) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config. scanConfigID=%v: %v", scanConfigID, err)) - } - return sendResponse(ctx, http.StatusOK, converted) -} - func (s *ServerImpl) PatchScanConfigsScanConfigID(ctx echo.Context, scanConfigID models.ScanConfigID) error { var scanConfig models.ScanConfig err := ctx.Bind(&scanConfig) @@ -127,30 +92,18 @@ func (s *ServerImpl) PatchScanConfigsScanConfigID(ctx echo.Context, scanConfigID return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScanConfig(&scanConfig, scanConfigID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config: %v", err)) - } - - // check that a scan config with that id exists. - _, err = s.dbHandler.ScanConfigsTable().GetScanConfig(convertedDB.ID) + // PATCH request might not contain the ID in the body, so set it from + // the URL field so that the DB layer knows which object is being updated. + scanConfig.Id = &scanConfigID + updatedScanConfig, err := s.dbHandler.ScanConfigsTable().UpdateScanConfig(scanConfig) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return sendError(ctx, http.StatusNotFound, fmt.Sprintf("scan config was not found. scanConfigID=%v: %v", scanConfigID, err)) + if errors.Is(err, databaseTypes.ErrNotFound) { + return sendError(ctx, http.StatusNotFound, fmt.Sprintf("ScanConfig with ID %v not found", scanConfigID)) } - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan config from db. scanConfigID=%v: %v", scanConfigID, err)) - } - - updatedScanConfig, err := s.dbHandler.ScanConfigsTable().UpdateScanConfig(convertedDB) - if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to update scan config in db. scanConfigID=%v: %v", scanConfigID, err)) } - converted, err := dbtorest.ConvertScanConfig(updatedScanConfig) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config. scanConfigID=%v: %v", scanConfigID, err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedScanConfig) } func (s *ServerImpl) PutScanConfigsScanConfigID(ctx echo.Context, scanConfigID models.ScanConfigID) error { @@ -160,28 +113,16 @@ func (s *ServerImpl) PutScanConfigsScanConfigID(ctx echo.Context, scanConfigID m return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScanConfig(&scanConfig, scanConfigID) + // PUT request might not contain the ID in the body, so set it from the + // URL field so that the DB layer knows which object is being updated. + scanConfig.Id = &scanConfigID + updatedScanConfig, err := s.dbHandler.ScanConfigsTable().SaveScanConfig(scanConfig) if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config: %v", err)) - } - - // check that a scan config with that id exists. - _, err = s.dbHandler.ScanConfigsTable().GetScanConfig(convertedDB.ID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return sendError(ctx, http.StatusNotFound, fmt.Sprintf("scan config was not found. scanConfigID=%v: %v", scanConfigID, err)) + if errors.Is(err, databaseTypes.ErrNotFound) { + return sendError(ctx, http.StatusNotFound, fmt.Sprintf("ScanConfig with ID %v not found", scanConfigID)) } - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan config from db. scanConfigID=%v: %v", scanConfigID, err)) - } - - updatedScanConfig, err := s.dbHandler.ScanConfigsTable().SaveScanConfig(convertedDB) - if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to update scan config in db. scanConfigID=%v: %v", scanConfigID, err)) } - converted, err := dbtorest.ConvertScanConfig(updatedScanConfig) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config. scanConfigID=%v: %v", scanConfigID, err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedScanConfig) } diff --git a/backend/pkg/rest/scan_controller.go b/backend/pkg/rest/scan_controller.go index 00f68751e..3bb4cd8c9 100644 --- a/backend/pkg/rest/scan_controller.go +++ b/backend/pkg/rest/scan_controller.go @@ -21,27 +21,20 @@ import ( "net/http" "github.com/labstack/echo/v4" - uuid "github.com/satori/go.uuid" "gorm.io/gorm" "github.com/openclarity/vmclarity/api/models" "github.com/openclarity/vmclarity/backend/pkg/common" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/dbtorest" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/resttodb" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) func (s *ServerImpl) GetScans(ctx echo.Context, params models.GetScansParams) error { - dbScans, total, err := s.dbHandler.ScansTable().GetScansAndTotal(resttodb.ConvertGetScansParams(params)) + scans, err := s.dbHandler.ScansTable().GetScans(params) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scans from db: %v", err)) } - converted, err := dbtorest.ConvertScans(dbScans, total) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scans: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, scans) } func (s *ServerImpl) PostScans(ctx echo.Context) error { @@ -51,32 +44,20 @@ func (s *ServerImpl) PostScans(ctx echo.Context) error { return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScan(&scan, "") - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - createdScan, err := s.dbHandler.ScansTable().CreateScan(convertedDB) + createdScan, err := s.dbHandler.ScansTable().CreateScan(scan) if err != nil { var conflictErr *common.ConflictError if errors.As(err, &conflictErr) { - convertedExist, err := dbtorest.ConvertScan(createdScan) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert existing scan: %v", err)) - } existResponse := &models.ScanExists{ Message: utils.StringPtr(conflictErr.Reason), - Scan: convertedExist, + Scan: &createdScan, } return sendResponse(ctx, http.StatusConflict, existResponse) } return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to create scan in db: %v", err)) } - converted, err := dbtorest.ConvertScan(createdScan) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - return sendResponse(ctx, http.StatusCreated, converted) + return sendResponse(ctx, http.StatusCreated, createdScan) } func (s *ServerImpl) DeleteScansScanID(ctx echo.Context, scanID models.ScanID) error { @@ -84,12 +65,7 @@ func (s *ServerImpl) DeleteScansScanID(ctx echo.Context, scanID models.ScanID) e Message: utils.StringPtr(fmt.Sprintf("scan %v deleted", scanID)), } - scanUUID, err := uuid.FromString(scanID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scanID %v to uuid: %v", scanID, err)) - } - - if err := s.dbHandler.ScansTable().DeleteScan(scanUUID); err != nil { + if err := s.dbHandler.ScansTable().DeleteScan(scanID); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, err.Error()) } @@ -100,24 +76,14 @@ func (s *ServerImpl) DeleteScansScanID(ctx echo.Context, scanID models.ScanID) e } func (s *ServerImpl) GetScansScanID(ctx echo.Context, scanID models.ScanID) error { - scanUUID, err := uuid.FromString(scanID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scanID %v to uuid: %v", scanID, err)) - } - - scan, err := s.dbHandler.ScansTable().GetScan(scanUUID) + scan, err := s.dbHandler.ScansTable().GetScan(scanID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, err.Error()) } return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan from db. id=%v: %v", scanID, err)) } - - converted, err := dbtorest.ConvertScan(scan) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, scan) } func (s *ServerImpl) PatchScansScanID(ctx echo.Context, scanID models.ScanID) error { @@ -127,13 +93,8 @@ func (s *ServerImpl) PatchScansScanID(ctx echo.Context, scanID models.ScanID) er return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScan(&scan, scanID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - // check that a scan with that id exists. - _, err = s.dbHandler.ScansTable().GetScan(convertedDB.ID) + _, err = s.dbHandler.ScansTable().GetScan(scanID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, fmt.Sprintf("scan was not found in db. scanID=%v: %v", scanID, err)) @@ -141,16 +102,12 @@ func (s *ServerImpl) PatchScansScanID(ctx echo.Context, scanID models.ScanID) er return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan from db. scanID=%v: %v", scanID, err)) } - updatedScan, err := s.dbHandler.ScansTable().UpdateScan(convertedDB) + updatedScan, err := s.dbHandler.ScansTable().UpdateScan(scan) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to update scan in db. scanID=%v: %v", scanID, err)) } - converted, err := dbtorest.ConvertScan(updatedScan) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedScan) } func (s *ServerImpl) PutScansScanID(ctx echo.Context, scanID models.ScanID) error { @@ -160,13 +117,8 @@ func (s *ServerImpl) PutScansScanID(ctx echo.Context, scanID models.ScanID) erro return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScan(&scan, scanID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - // check that a scan with that id exists. - _, err = s.dbHandler.ScansTable().GetScan(convertedDB.ID) + _, err = s.dbHandler.ScansTable().GetScan(scanID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, fmt.Sprintf("scan was not found in db. scanID=%v: %v", scanID, err)) @@ -174,14 +126,10 @@ func (s *ServerImpl) PutScansScanID(ctx echo.Context, scanID models.ScanID) erro return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan from db. scanID=%v: %v", scanID, err)) } - updatedScan, err := s.dbHandler.ScansTable().SaveScan(convertedDB) + updatedScan, err := s.dbHandler.ScansTable().SaveScan(scan) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to update scan in db. scanID=%v: %v", scanID, err)) } - converted, err := dbtorest.ConvertScan(updatedScan) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedScan) } diff --git a/backend/pkg/rest/scan_result_controller.go b/backend/pkg/rest/scan_result_controller.go index 79822a5d8..f62232cb6 100644 --- a/backend/pkg/rest/scan_result_controller.go +++ b/backend/pkg/rest/scan_result_controller.go @@ -21,28 +21,20 @@ import ( "net/http" "github.com/labstack/echo/v4" - uuid "github.com/satori/go.uuid" "gorm.io/gorm" "github.com/openclarity/vmclarity/api/models" "github.com/openclarity/vmclarity/backend/pkg/common" - "github.com/openclarity/vmclarity/backend/pkg/database" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/dbtorest" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/resttodb" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) func (s *ServerImpl) GetScanResults(ctx echo.Context, params models.GetScanResultsParams) error { - dbScanResults, total, err := s.dbHandler.ScanResultsTable().GetScanResultsAndTotal(resttodb.ConvertGetScanResultsParams(params)) + dbScanResults, err := s.dbHandler.ScanResultsTable().GetScanResults(params) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scans results from db: %v", err)) } - converted, err := dbtorest.ConvertScanResults(dbScanResults, total) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan results: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, dbScanResults) } func (s *ServerImpl) PostScanResults(ctx echo.Context) error { @@ -52,52 +44,31 @@ func (s *ServerImpl) PostScanResults(ctx echo.Context) error { return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScanResult(&scanResult, "") - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } - createdScanResult, err := s.dbHandler.ScanResultsTable().CreateScanResult(convertedDB) + createdScanResult, err := s.dbHandler.ScanResultsTable().CreateScanResult(scanResult) if err != nil { var conflictErr *common.ConflictError if errors.As(err, &conflictErr) { - convertedExist, err := dbtorest.ConvertScanResult(createdScanResult) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert existing scan result: %v", err)) - } existResponse := &models.TargetScanResultExists{ Message: utils.StringPtr(conflictErr.Reason), - TargetScanResult: convertedExist, + TargetScanResult: &createdScanResult, } return sendResponse(ctx, http.StatusConflict, existResponse) } return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to create scan result in db: %v", err)) } - converted, err := dbtorest.ConvertScanResult(createdScanResult) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } - return sendResponse(ctx, http.StatusCreated, converted) + return sendResponse(ctx, http.StatusCreated, createdScanResult) } func (s *ServerImpl) GetScanResultsScanResultID(ctx echo.Context, scanResultID models.ScanResultID, params models.GetScanResultsScanResultIDParams) error { - scanResultUUID, err := uuid.FromString(scanResultID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scanResultID %v to uuid: %v", scanResultID, err)) - } - dbScanResult, err := s.dbHandler.ScanResultsTable().GetScanResult(scanResultUUID, resttodb.ConvertGetScanResultsScanResultIDParams(params)) + dbScanResult, err := s.dbHandler.ScanResultsTable().GetScanResult(scanResultID, params) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, err.Error()) } return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan result from db. scanResultID=%v: %v", scanResultID, err)) } - - converted, err := dbtorest.ConvertScanResult(dbScanResult) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, dbScanResult) } func (s *ServerImpl) PatchScanResultsScanResultID(ctx echo.Context, scanResultID models.ScanResultID) error { @@ -107,12 +78,8 @@ func (s *ServerImpl) PatchScanResultsScanResultID(ctx echo.Context, scanResultID return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScanResult(&scanResult, scanResultID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } // check that a scan result with that id exists. - _, err = s.dbHandler.ScanResultsTable().GetScanResult(convertedDB.ID, database.GetScanResultsScanResultIDParams{}) + _, err = s.dbHandler.ScanResultsTable().GetScanResult(scanResultID, models.GetScanResultsScanResultIDParams{}) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, fmt.Sprintf("scan result was not found. scanResultID=%v: %v", scanResultID, err)) @@ -120,16 +87,12 @@ func (s *ServerImpl) PatchScanResultsScanResultID(ctx echo.Context, scanResultID return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan result. scanResultID=%v: %v", scanResultID, err)) } - updatedScanResult, err := s.dbHandler.ScanResultsTable().UpdateScanResult(convertedDB) + updatedScanResult, err := s.dbHandler.ScanResultsTable().UpdateScanResult(scanResult) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to update scan result in db. scanResultID=%v: %v", scanResultID, err)) } - converted, err := dbtorest.ConvertScanResult(updatedScanResult) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedScanResult) } func (s *ServerImpl) PutScanResultsScanResultID(ctx echo.Context, scanResultID models.ScanResultID) error { @@ -139,13 +102,8 @@ func (s *ServerImpl) PutScanResultsScanResultID(ctx echo.Context, scanResultID m return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScanResult(&scanResult, scanResultID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } - // check that a scan result with that id exists. - _, err = s.dbHandler.ScanResultsTable().GetScanResult(convertedDB.ID, database.GetScanResultsScanResultIDParams{}) + _, err = s.dbHandler.ScanResultsTable().GetScanResult(scanResultID, models.GetScanResultsScanResultIDParams{}) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, fmt.Sprintf("scan result was not found. scanResultID=%v: %v", scanResultID, err)) @@ -153,14 +111,10 @@ func (s *ServerImpl) PutScanResultsScanResultID(ctx echo.Context, scanResultID m return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan result. scanResultID=%v: %v", scanResultID, err)) } - updatedScanResult, err := s.dbHandler.ScanResultsTable().SaveScanResult(convertedDB) + updatedScanResult, err := s.dbHandler.ScanResultsTable().SaveScanResult(scanResult) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to update scan result in db. scanResultID=%v: %v", scanResultID, err)) } - converted, err := dbtorest.ConvertScanResult(updatedScanResult) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedScanResult) } diff --git a/backend/pkg/rest/server.go b/backend/pkg/rest/server.go index 274b123e2..5ffb0cb80 100644 --- a/backend/pkg/rest/server.go +++ b/backend/pkg/rest/server.go @@ -27,7 +27,7 @@ import ( "github.com/openclarity/vmclarity/api/server" "github.com/openclarity/vmclarity/backend/pkg/common" - "github.com/openclarity/vmclarity/backend/pkg/database" + databaseTypes "github.com/openclarity/vmclarity/backend/pkg/database/types" ) const ( @@ -36,7 +36,7 @@ const ( ) type ServerImpl struct { - dbHandler database.Database + dbHandler databaseTypes.Database } type Server struct { @@ -76,7 +76,7 @@ func createEchoServer() (*echo.Echo, error) { return e, nil } -func (s *Server) RegisterHandlers(dbHandler database.Database) { +func (s *Server) RegisterHandlers(dbHandler databaseTypes.Database) { serverImpl := &ServerImpl{ dbHandler: dbHandler, } diff --git a/backend/pkg/rest/target_controller.go b/backend/pkg/rest/target_controller.go index ca8548db6..3bbe7611e 100644 --- a/backend/pkg/rest/target_controller.go +++ b/backend/pkg/rest/target_controller.go @@ -21,27 +21,20 @@ import ( "net/http" "github.com/labstack/echo/v4" - uuid "github.com/satori/go.uuid" "gorm.io/gorm" "github.com/openclarity/vmclarity/api/models" "github.com/openclarity/vmclarity/backend/pkg/common" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/dbtorest" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/resttodb" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) func (s *ServerImpl) GetTargets(ctx echo.Context, params models.GetTargetsParams) error { - dbTargets, total, err := s.dbHandler.TargetsTable().GetTargetsAndTotal(resttodb.ConvertGetTargetsParams(params)) + dbTargets, err := s.dbHandler.TargetsTable().GetTargets(params) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get targets from db: %v", err)) } - converted, err := dbtorest.ConvertTargets(dbTargets, total) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert targets: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, dbTargets) } // nolint:cyclop @@ -52,41 +45,24 @@ func (s *ServerImpl) PostTargets(ctx echo.Context) error { return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertTarget(&target, "") - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert target: %v", err)) - } - createdTarget, err := s.dbHandler.TargetsTable().CreateTarget(convertedDB) + createdTarget, err := s.dbHandler.TargetsTable().CreateTarget(target) if err != nil { var conflictErr *common.ConflictError if errors.As(err, &conflictErr) { - convertedExist, err := dbtorest.ConvertTarget(createdTarget) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert existing target: %v", err)) - } existResponse := &models.TargetExists{ Message: utils.StringPtr(conflictErr.Reason), - Target: convertedExist, + Target: &createdTarget, } return sendResponse(ctx, http.StatusConflict, existResponse) } return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to create target in db: %v", err)) } - converted, err := dbtorest.ConvertTarget(createdTarget) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert target: %v", err)) - } - return sendResponse(ctx, http.StatusCreated, converted) + return sendResponse(ctx, http.StatusCreated, createdTarget) } func (s *ServerImpl) GetTargetsTargetID(ctx echo.Context, targetID models.TargetID) error { - targetUUID, err := uuid.FromString(targetID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert targetID %v to uuid: %v", targetID, err)) - } - - target, err := s.dbHandler.TargetsTable().GetTarget(targetUUID) + target, err := s.dbHandler.TargetsTable().GetTarget(targetID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, err.Error()) @@ -94,11 +70,7 @@ func (s *ServerImpl) GetTargetsTargetID(ctx echo.Context, targetID models.Target return sendError(ctx, http.StatusInternalServerError, fmt.Errorf("failed to get target from db. targetID=%v: %v", targetID, err).Error()) } - converted, err := dbtorest.ConvertTarget(target) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert target: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, target) } func (s *ServerImpl) PutTargetsTargetID(ctx echo.Context, targetID models.TargetID) error { @@ -108,11 +80,7 @@ func (s *ServerImpl) PutTargetsTargetID(ctx echo.Context, targetID models.Target return sendError(ctx, http.StatusBadRequest, fmt.Errorf("failed to bind request: %v", err).Error()) } - convertedDB, err := resttodb.ConvertTarget(&target, targetID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert target: %v", err)) - } - _, err = s.dbHandler.TargetsTable().GetTarget(convertedDB.ID) + _, err = s.dbHandler.TargetsTable().GetTarget(targetID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, fmt.Sprintf("target was not found in db. targetID=%v", targetID)) @@ -120,28 +88,20 @@ func (s *ServerImpl) PutTargetsTargetID(ctx echo.Context, targetID models.Target return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get target from db: %v", err)) } - updatedTarget, err := s.dbHandler.TargetsTable().SaveTarget(convertedDB) + updatedTarget, err := s.dbHandler.TargetsTable().SaveTarget(target) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Errorf("failed to update target in db. targetID=%v: %v", targetID, err).Error()) } - converted, err := dbtorest.ConvertTarget(updatedTarget) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert target: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedTarget) } func (s *ServerImpl) DeleteTargetsTargetID(ctx echo.Context, targetID models.TargetID) error { success := models.Success{ Message: utils.StringPtr(fmt.Sprintf("target %v deleted", targetID)), } - targetUUID, err := uuid.FromString(targetID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert targetID %v to uuid: %v", targetID, err)) - } - if err := s.dbHandler.TargetsTable().DeleteTarget(targetUUID); err != nil { + if err := s.dbHandler.TargetsTable().DeleteTarget(targetID); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, err.Error()) } diff --git a/go.mod b/go.mod index 78413bf65..28f4e88d4 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/openclarity/vmclarity go 1.19 require ( + github.com/CiscoM31/godata v1.0.7 github.com/CycloneDX/cyclonedx-go v0.6.0 github.com/Masterminds/sprig/v3 v3.2.3 github.com/Portshift/go-utils v0.0.0-20220421083203-89265d8a6487 @@ -11,8 +12,10 @@ require ( github.com/aws/aws-sdk-go-v2/service/ec2 v1.63.1 github.com/cenkalti/backoff v2.2.1+incompatible github.com/deepmap/oapi-codegen v1.12.3 + github.com/evanphx/json-patch/v5 v5.6.0 github.com/ghodss/yaml v1.0.0 github.com/google/go-cmp v0.5.9 + github.com/google/uuid v1.3.0 github.com/labstack/echo/v4 v4.9.1 github.com/openclarity/kubeclarity/cli v0.0.0-00010101000000-000000000000 github.com/openclarity/kubeclarity/shared v0.0.0 @@ -26,8 +29,9 @@ require ( github.com/vulsio/go-exploitdb v0.4.2 golang.org/x/exp v0.0.0-20220823124025-807a23277127 gopkg.in/yaml.v3 v3.0.1 + gorm.io/datatypes v1.1.0 gorm.io/driver/sqlite v1.3.6 - gorm.io/gorm v1.23.5 + gorm.io/gorm v1.23.8 gotest.tools/v3 v3.4.0 k8s.io/mount-utils v0.26.2 ) @@ -192,6 +196,7 @@ require ( github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-restruct/restruct v1.2.0-alpha // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-test/deep v1.0.8 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/googleapis v1.4.1 // indirect @@ -215,7 +220,6 @@ require ( github.com/google/licenseclassifier/v2 v2.0.0-pre6 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/trillian v1.5.0 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/google/wire v0.5.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect github.com/googleapis/gax-go/v2 v2.6.0 // indirect @@ -282,7 +286,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/mattn/go-sqlite3 v1.14.12 // indirect + github.com/mattn/go-sqlite3 v1.14.15 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mholt/archiver/v3 v3.5.1 // indirect github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032 // indirect @@ -445,6 +449,7 @@ require ( gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gorm.io/driver/mysql v1.4.4 // indirect helm.sh/helm/v3 v3.10.3 // indirect k8s.io/api v0.25.3 // indirect k8s.io/apiextensions-apiserver v0.25.2 // indirect diff --git a/go.sum b/go.sum index 3efcdde2a..b8f0b8653 100644 --- a/go.sum +++ b/go.sum @@ -156,6 +156,8 @@ github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/CiscoM31/godata v1.0.7 h1:y3FHdICAU9j+IkK6E66ezCghaQSamFbYoj/YEHig0kY= +github.com/CiscoM31/godata v1.0.7/go.mod h1:ZMiT6JuD3Rm83HEtiTx4JEChsd25YCrxchKGag/sdTc= github.com/CycloneDX/cyclonedx-go v0.6.0 h1:SizWGbZzFTC/O/1yh072XQBMxfvsoWqd//oKCIyzFyE= github.com/CycloneDX/cyclonedx-go v0.6.0/go.mod h1:nQCiF4Tvrg5Ieu8qPhYMvzPGMu5I7fANZkrSsJjl5mg= github.com/CycloneDX/cyclonedx-gomod v1.2.0 h1:LgBL5TXwmluFqaum/8tXaPlwZvy1sSbBNKGV179E+Sk= @@ -821,6 +823,8 @@ github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQL github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= @@ -983,8 +987,9 @@ github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsX github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -1054,6 +1059,8 @@ github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= @@ -1373,8 +1380,10 @@ github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= @@ -1383,9 +1392,13 @@ github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5 github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g= +github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= @@ -1394,7 +1407,9 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1: github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= @@ -1403,6 +1418,7 @@ github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4 github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE= +github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= @@ -1410,6 +1426,7 @@ github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXg github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc= +github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= @@ -1648,8 +1665,9 @@ github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGw github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -1659,6 +1677,7 @@ github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88J github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032 h1:TLygBUBxikNJJfLwgm+Qwdgq1FtfV8Uh7bcxRyTzK8s= github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032/go.mod h1:vYT9HE7WCvL64iVeZylKmCsWKfE+JZ8105iuh2Trk8g= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -3314,17 +3333,24 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.1.0 h1:EVp1Z28N4ACpYFK1nHboEIJGIFfjY7vLeieDk8jSHJA= +gorm.io/datatypes v1.1.0/go.mod h1:SH2K9R+2RMjuX1CkCONrPwoe9JzVv2hkQvEu4bXGojE= gorm.io/driver/mysql v1.1.0/go.mod h1:KdrTanmfLPPyAOeYGyG+UpDys7/7eeWT1zCq+oekYnU= +gorm.io/driver/mysql v1.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ= +gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM= gorm.io/driver/postgres v1.1.0/go.mod h1:hXQIwafeRjJvUm+OMxcFWyswJ/vevcpPLlGocwAwuqw= +gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc= gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= gorm.io/driver/sqlite v1.3.6 h1:Fi8xNYCUplOqWiPa3/GuCeowRNBRGTf62DEmhMDHeQQ= gorm.io/driver/sqlite v1.3.6/go.mod h1:Sg1/pvnKtbQ7jLXxfZa+jSHvoX8hoZA8cn4xllOMTgE= +gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= gorm.io/gorm v1.21.10/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= -gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE= +gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=