diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 5661ad12..bfb5b2f3 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -5,29 +5,42 @@ import ( "net/http/httptest" "reflect" "testing" + "time" + "github.com/labstack/echo/v4" ) func TestMapQueryParameters(t *testing.T) { - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - + // setup type tests struct { name string qinputs map[string]string - qoutputs map[string][]string + qoutputs map[string]interface{} errmsg string } - var all_tests = []tests{ + now := time.Now().UTC().Truncate(time.Second) + firstOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC).Truncate(time.Second) + + startTime := time.Date(2023, 3, 23, 0, 0, 0, 0, time.UTC).Truncate(time.Second) + endTime := time.Date(2023, 3, 24, 0, 0, 0, 0, time.UTC).Truncate(time.Second) + + all_tests := []tests{ + { + name: "When start date and end date are not provided", + qinputs: map[string]string{"start_date": "", "end_date": ""}, + qoutputs: map[string]interface{}{ + "recommendation_sets.monitoring_end_time <= ?": now, + "recommendation_sets.monitoring_end_time >= ?": firstOfMonth, + }, + errmsg: `The startTime should be 1st of current month. The endTime should the current time.`, + }, { name: "When start date and end date are provided", - qinputs: map[string]string{"start_date": "2023-03-23", "end_date": "2023-03-24"}, - qoutputs: map[string][]string{ - "DATE(recommendation_sets.monitoring_end_time) <= ?": {"2023-03-24"}, - "DATE(recommendation_sets.monitoring_end_time) >= ?": {"2023-03-23"}, + qinputs: map[string]string{"start_date": startTime.Format("2006-01-02"), "end_date": endTime.Format("2006-01-02")}, + qoutputs: map[string]interface{}{ + "recommendation_sets.monitoring_end_time <= ?": endTime, + "recommendation_sets.monitoring_end_time >= ?": startTime, }, errmsg: `The recommendation_sets.monitoring_end_time should be less than or equal to end date! The recommendation_sets.monitoring_end_time should be greater than or equal to start date!`, @@ -35,17 +48,29 @@ func TestMapQueryParameters(t *testing.T) { } for _, tt := range all_tests { + tt := tt t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create a fresh request and recorder for each parallel test + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + for k, v := range tt.qinputs { c.QueryParams().Add(k, v) } + defer func() { + // Cleanup query params regardless of test result + for k := range c.QueryParams() { + delete(c.QueryParams(), k) + } + }() result, _ := MapQueryParameters(c) if reflect.DeepEqual(result, tt.qoutputs) != true { t.Errorf("%s", tt.errmsg) } - for k := range c.QueryParams() { - delete(c.QueryParams(), k) - } }) } } diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 208d3c5e..6c93b20b 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -17,10 +17,10 @@ func GetRecommendationSetList(c echo.Context) error { OrgID := XRHID.Identity.OrgID user_permissions := get_user_permissions(c) handlerName := "recommendationset-list" - var unitChoices = make(map[string]string) + unitChoices := make(map[string]string) cpuUnitParam := c.QueryParam("cpu-unit") - var cpuUnitOptions = map[string]bool{ + cpuUnitOptions := map[string]bool{ "millicores": true, "cores": true, } @@ -36,7 +36,7 @@ func GetRecommendationSetList(c echo.Context) error { } memoryUnitParam := c.QueryParam("memory-unit") - var memoryUnitOptions = map[string]bool{ + memoryUnitOptions := map[string]bool{ "bytes": true, "MiB": true, "GiB": true, @@ -60,7 +60,7 @@ func GetRecommendationSetList(c echo.Context) error { orderBy = c.QueryParam("order_by") if orderBy != "" { - var orderByOptions = map[string]string{ + orderByOptions := map[string]string{ "cluster": "clusters.cluster_alias", "workload_type": "workloads.workload_type", "workload": "workloads.workload_name", @@ -127,35 +127,25 @@ func GetRecommendationSetList(c echo.Context) error { return c.JSON(http.StatusBadRequest, echo.Map{"status": "error", "message": "invalid value for true-units"}) } } - setk8sUnits := !trueUnits - allRecommendations := []map[string]interface{}{} - - for _, recommendation := range recommendationSets { - recommendationData := make(map[string]interface{}) - - recommendationData["id"] = recommendation.ID - recommendationData["source_id"] = recommendation.Workload.Cluster.SourceId - recommendationData["cluster_uuid"] = recommendation.Workload.Cluster.ClusterUUID - recommendationData["cluster_alias"] = recommendation.Workload.Cluster.ClusterAlias - recommendationData["project"] = recommendation.Workload.Namespace - recommendationData["workload_type"] = recommendation.Workload.WorkloadType - recommendationData["workload"] = recommendation.Workload.WorkloadName - recommendationData["container"] = recommendation.ContainerName - recommendationData["last_reported"] = recommendation.Workload.Cluster.LastReportedAtStr - recommendationData["recommendations"] = UpdateRecommendationJSON(handlerName, recommendation.ID, recommendation.Workload.Cluster.ClusterUUID, unitChoices, setk8sUnits, recommendation.Recommendations) - allRecommendations = append(allRecommendations, recommendationData) - + for i := range recommendationSets { + recommendationSets[i].RecommendationsJSON = UpdateRecommendationJSON( + handlerName, + recommendationSets[i].ID, + recommendationSets[i].ClusterUUID, + unitChoices, + setk8sUnits, + recommendationSets[i].Recommendations, + ) } - interfaceSlice := make([]interface{}, len(allRecommendations)) - for i, v := range allRecommendations { + interfaceSlice := make([]interface{}, len(recommendationSets)) + for i, v := range recommendationSets { interfaceSlice[i] = v } results := CollectionResponse(interfaceSlice, c.Request(), count, limit, offset) return c.JSON(http.StatusOK, results) - } func GetRecommendationSet(c echo.Context) error { @@ -170,7 +160,7 @@ func GetRecommendationSet(c echo.Context) error { return c.JSON(http.StatusBadRequest, echo.Map{"status": "error", "message": "bad recommendation_id"}) } - var unitChoices = make(map[string]string) + unitChoices := make(map[string]string) trueUnitsStr := c.QueryParam("true-units") var trueUnits bool @@ -181,11 +171,10 @@ func GetRecommendationSet(c echo.Context) error { return c.JSON(http.StatusBadRequest, echo.Map{"status": "error", "message": "invalid value for true-units"}) } } - setk8sUnits := !trueUnits cpuUnitParam := c.QueryParam("cpu-unit") - var cpuUnitOptions = map[string]bool{ + cpuUnitOptions := map[string]bool{ "millicores": true, "cores": true, } @@ -201,7 +190,7 @@ func GetRecommendationSet(c echo.Context) error { } memoryUnitParam := c.QueryParam("memory-unit") - var memoryUnitOptions = map[string]bool{ + memoryUnitOptions := map[string]bool{ "bytes": true, "MiB": true, "GiB": true, @@ -221,26 +210,20 @@ func GetRecommendationSet(c echo.Context) error { recommendationSet, error := recommendationSetVar.GetRecommendationSetByID(OrgID, RecommendationUUID.String(), user_permissions) if error != nil { - log.Error("unable to fetch records from database", error) + log.Errorf("unable to fetch recommendation %s; error %v", RecommendationIDStr, error) + return c.JSON(http.StatusNotFound, echo.Map{"status": "not_found", "message": "recommendation not found"}) } - recommendationSlice := make(map[string]interface{}) - if len(recommendationSet.Recommendations) != 0 { - recommendationSlice["id"] = recommendationSet.ID - recommendationSlice["source_id"] = recommendationSet.Workload.Cluster.SourceId - recommendationSlice["cluster_uuid"] = recommendationSet.Workload.Cluster.ClusterUUID - recommendationSlice["cluster_alias"] = recommendationSet.Workload.Cluster.ClusterAlias - recommendationSlice["project"] = recommendationSet.Workload.Namespace - recommendationSlice["workload_type"] = recommendationSet.Workload.WorkloadType - recommendationSlice["workload"] = recommendationSet.Workload.WorkloadName - recommendationSlice["container"] = recommendationSet.ContainerName - recommendationSlice["last_reported"] = recommendationSet.Workload.Cluster.LastReportedAtStr - recommendationSlice["recommendations"] = UpdateRecommendationJSON(handlerName, recommendationSet.ID, recommendationSet.Workload.Cluster.ClusterUUID, unitChoices, setk8sUnits, recommendationSet.Recommendations) - } - - return c.JSON(http.StatusOK, recommendationSlice) - + recommendationSet.RecommendationsJSON = UpdateRecommendationJSON( + handlerName, + recommendationSet.ID, + recommendationSet.ClusterUUID, + unitChoices, + setk8sUnits, + recommendationSet.Recommendations) + } + return c.JSON(http.StatusOK, recommendationSet) } func GetAppStatus(c echo.Context) error { diff --git a/internal/api/server.go b/internal/api/server.go index 165cab57..15d24d67 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "time" "github.com/labstack/echo-contrib/echoprometheus" "github.com/labstack/echo/v4" @@ -53,8 +54,9 @@ func StartAPIServer() { v1.GET("/recommendations/openshift/:recommendation-id", GetRecommendationSet) s := http.Server{ - Addr: ":" + cfg.API_PORT, //local dev server - Handler: app, + Addr: ":" + cfg.API_PORT, // local dev server + Handler: app, + ReadHeaderTimeout: time.Duration(cfg.ReadHeaderTimeout) * time.Second, } if err := s.ListenAndServe(); err != http.ErrServerClosed { log.Fatal(err) diff --git a/internal/api/utils.go b/internal/api/utils.go index 92f37bbf..d958f8e8 100644 --- a/internal/api/utils.go +++ b/internal/api/utils.go @@ -64,45 +64,41 @@ func CollectionResponse(collection []interface{}, req *http.Request, count, limi } } -func MapQueryParameters(c echo.Context) (map[string][]string, error) { +func MapQueryParameters(c echo.Context) (map[string]interface{}, error) { log := logging.GetLogger() - queryParams := make(map[string][]string) - var startDate, endDate time.Time + queryParams := make(map[string]interface{}) + var startTimestamp, endTimestamp time.Time var clusters, projects, workloadNames, workloadTypes, containers []string now := time.Now().UTC() firstOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) - dateSlice := []string{} startDateStr := c.QueryParam("start_date") if startDateStr == "" { - startDate = firstOfMonth + startTimestamp = firstOfMonth } else { var err error - startDate, err = time.Parse(timeLayout, startDateStr) + startTimestamp, err = time.Parse(timeLayout, startDateStr) if err != nil { log.Error("error parsing start_date:", err) return queryParams, err } } - startDateSlice := append(dateSlice, startDate.Format(timeLayout)) - queryParams["DATE(recommendation_sets.monitoring_end_time) >= ?"] = startDateSlice - endDateStr := c.QueryParam("end_date") + queryParams["recommendation_sets.monitoring_end_time >= ?"] = startTimestamp.Truncate(time.Second) + endDateStr := c.QueryParam("end_date") if endDateStr == "" { - endDate = now + endTimestamp = now } else { var err error - endDate, err = time.Parse(timeLayout, endDateStr) + endTimestamp, err = time.Parse(timeLayout, endDateStr) if err != nil { log.Error("error parsing end_date:", err) return queryParams, err } } - endDateSlice := append(dateSlice, endDate.Format(timeLayout)) - - queryParams["DATE(recommendation_sets.monitoring_end_time) <= ?"] = endDateSlice + queryParams["recommendation_sets.monitoring_end_time <= ?"] = endTimestamp.Truncate(time.Second) clusters = c.QueryParams()["cluster"] if len(clusters) > 0 { @@ -135,15 +131,13 @@ func MapQueryParameters(c echo.Context) (map[string][]string, error) { } return queryParams, nil - } func parseQueryParams(param string, values []string) (string, []string) { - parsedKeyMultipleVal := "" valuesSlice := []string{} - var paramMap = map[string]string{ + paramMap := map[string]string{ "cluster": "clusters.cluster_alias ILIKE ?", "workload_type": "workloads.workload_type = ?", "workload": "workloads.workload_name ILIKE ?", @@ -180,7 +174,6 @@ func parseQueryParams(param string, values []string) (string, []string) { } return paramMap[param], valuesSlice } - } func get_user_permissions(c echo.Context) map[string][]string { @@ -221,11 +214,9 @@ func convertCPUUnit(cpuUnit string, cpuValue float64) float64 { } return convertedValueCPU - } func convertMemoryUnit(memoryUnit string, memoryValue float64) float64 { - var convertedValueMemory float64 if memoryUnit == "MiB" { @@ -239,7 +230,6 @@ func convertMemoryUnit(memoryUnit string, memoryValue float64) float64 { } return convertedValueMemory - } func transformComponentUnits(unitsToTransform map[string]string, updateUnitsk8s bool, recommendationJSON map[string]interface{}) map[string]interface{} { @@ -266,7 +256,6 @@ func transformComponentUnits(unitsToTransform map[string]string, updateUnitsk8s } for _, section := range []string{"limits", "requests"} { - sectionObject, ok := current_config[section].(map[string]interface{}) if ok { memoryObject, ok := sectionObject["memory"].(map[string]interface{}) @@ -367,13 +356,11 @@ func transformComponentUnits(unitsToTransform map[string]string, updateUnitsk8s } } } - } } } if intervalData["recommendation_engines"] != nil { - for _, recommendationType := range []string{"cost", "performance"} { engineData, ok := intervalData["recommendation_engines"].(map[string]interface{})[recommendationType].(map[string]interface{}) if !ok { @@ -387,7 +374,6 @@ func transformComponentUnits(unitsToTransform map[string]string, updateUnitsk8s } for _, section := range []string{"limits", "requests"} { - sectionObject, ok := recommendationSection[section].(map[string]interface{}) if ok { memoryObject, ok := sectionObject["memory"].(map[string]interface{}) @@ -416,11 +402,9 @@ func transformComponentUnits(unitsToTransform map[string]string, updateUnitsk8s cpuObject["format"] = cpuUnit } } - } } } - } } } @@ -430,7 +414,6 @@ func transformComponentUnits(unitsToTransform map[string]string, updateUnitsk8s } func filterNotifications(recommendationID string, clusterUUID string, recommendationJSON map[string]interface{}) map[string]interface{} { - var droppedNotifications []string deleteNotificationObject := func(recommendationSection map[string]interface{}) { @@ -444,7 +427,6 @@ func filterNotifications(recommendationID string, clusterUUID string, recommenda } } } - } // level 1 notifications are not stored in the database @@ -480,7 +462,6 @@ func filterNotifications(recommendationID string, clusterUUID string, recommenda } func dropBoxPlotsObject(recommendationJSON map[string]interface{}) map[string]interface{} { - recommendation_terms, ok := recommendationJSON["recommendation_terms"].(map[string]interface{}) if !ok { log.Error("recommendation data not found in JSON") @@ -519,7 +500,6 @@ func convertVariationToPercentage(recommendationJSON map[string]interface{}) map } for _, section := range []string{"limits", "requests"} { - sectionObject, ok := current_config[section].(map[string]interface{}) if ok { memoryObject, ok := sectionObject["memory"].(map[string]interface{}) @@ -572,7 +552,6 @@ func convertVariationToPercentage(recommendationJSON map[string]interface{}) map } for _, section := range []string{"limits", "requests"} { - sectionObject, ok := recommendationSection[section].(map[string]interface{}) if ok { memoryObject, ok := sectionObject["memory"].(map[string]interface{}) @@ -602,11 +581,9 @@ func convertVariationToPercentage(recommendationJSON map[string]interface{}) map cpuObject["format"] = "percent" } } - } } } - } } } @@ -614,7 +591,6 @@ func convertVariationToPercentage(recommendationJSON map[string]interface{}) map } func UpdateRecommendationJSON(handlerName string, recommendationID string, clusterUUID string, unitsToTransform map[string]string, updateUnitsk8s bool, jsonData datatypes.JSON) map[string]interface{} { - var data map[string]interface{} err := json.Unmarshal([]byte(jsonData), &data) if err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index 32d10e03..96ae450a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,6 +18,7 @@ type Config struct { LogLevel string `mapstructure:"LOG_LEVEL"` RecommendationPollIntervalHours int `mapstructure:"RECOMMENDATION_POLL_INTERVAL_HOURS"` DataRetentionPeriod int `mapstructure:"DATA_RETENTION_PERIOD"` + ReadHeaderTimeout int `mapstructure:"READ_HEADER_TIMEOUT"` // Kafka config KafkaBootstrapServers string `mapstructure:"KAFKA_BOOTSTRAP_SERVERS"` @@ -174,6 +175,7 @@ func initConfig() { viper.SetDefault("KRUIZE_URL", fmt.Sprintf("http://%s:%s", viper.GetString("KRUIZE_HOST"), viper.GetString("KRUIZE_PORT"))) viper.SetDefault("RECOMMENDATION_POLL_INTERVAL_HOURS", 24) viper.SetDefault("DATA_RETENTION_PERIOD", 15) + viper.SetDefault("READ_HEADER_TIMEOUT", 15) // Hack till viper issue get fix - https://github.com/spf13/viper/issues/761 envKeysMap := &map[string]interface{}{} diff --git a/internal/model/recommendation_set.go b/internal/model/recommendation_set.go index 9c7b577a..57cf6a93 100644 --- a/internal/model/recommendation_set.go +++ b/internal/model/recommendation_set.go @@ -26,10 +26,27 @@ type RecommendationSet struct { UpdatedAtStr string `gorm:"-"` } +type RecommendationSetResult struct { + /* + Intended to be an API-ready struct + Updated recommendation data is saved to RecommendationsJSON + Before the API response is sent + */ + ClusterAlias string `json:"cluster_alias"` + ClusterUUID string `json:"cluster_uuid"` + Container string `json:"container"` + ID string `json:"id"` + LastReported string `json:"last_reported"` + Project string `json:"project"` + Recommendations datatypes.JSON `json:"-"` + RecommendationsJSON map[string]interface{} `gorm:"-" json:"recommendations"` + SourceID string `json:"source_id"` + Workload string `json:"workload"` + WorkloadType string `json:"workload_type"` +} + func (r *RecommendationSet) AfterFind(tx *gorm.DB) error { - r.MonitoringStartTimeStr = r.MonitoringStartTime.Format(time.RFC3339) r.MonitoringEndTimeStr = r.MonitoringEndTime.Format(time.RFC3339) - r.UpdatedAtStr = r.UpdatedAt.Format(time.RFC3339) return nil } @@ -43,51 +60,68 @@ func GetFirstRecommendationSetsByWorkloadID(workload_id uint) (RecommendationSet return recommendationSets, query.Error } -func (r *RecommendationSet) GetRecommendationSets(orgID string, orderQuery string, limit int, offset int, queryParams map[string][]string, user_permissions map[string][]string) ([]RecommendationSet, int, error) { - - var recommendationSets []RecommendationSet +func (r *RecommendationSet) GetRecommendationSets(orgID string, orderQuery string, limit int, offset int, queryParams map[string]interface{}, user_permissions map[string][]string) ([]RecommendationSetResult, int, error) { db := database.GetDB() - - query := db.Table("recommendation_sets").Joins(` + var recommendationSets []RecommendationSetResult + + query := db.Table("recommendation_sets"). + Select("recommendation_sets.id, "+ + "recommendation_sets.container_name AS container, "+ + "workloads.namespace AS project, "+ + "workloads.workload_name as workload, "+ + "workloads.workload_type, "+ + "clusters.source_id, "+ + "clusters.cluster_uuid, "+ + "clusters.cluster_alias, "+ + "clusters.last_reported_at AS last_reported, "+ + "recommendation_sets.recommendations"). + Joins(` JOIN workloads ON recommendation_sets.workload_id = workloads.id JOIN clusters ON workloads.cluster_id = clusters.id JOIN rh_accounts ON clusters.tenant_id = rh_accounts.id - `).Model(r).Preload("Workload.Cluster.RHAccount").Where("rh_accounts.org_id = ?", orgID) + `).Model(&RecommendationSetResult{}). + Where("rh_accounts.org_id = ?", orgID) add_rbac_filter(query, user_permissions) - for key, values := range queryParams { - valuesInterface := make([]interface{}, len(values)) - for i, v := range values { - valuesInterface[i] = v - } - query.Where(key, valuesInterface...) + for key, value := range queryParams { + query.Where(key, value) } var count int64 = 0 query.Count(&count) query.Order(orderQuery) - err := query.Offset(offset).Limit(limit).Find(&recommendationSets).Error + err := query.Offset(offset).Limit(limit).Scan(&recommendationSets).Error return recommendationSets, int(count), err } -func (r *RecommendationSet) GetRecommendationSetByID(orgID string, recommendationID string, user_permissions map[string][]string) (RecommendationSet, error) { - - var recommendationSet RecommendationSet +func (r *RecommendationSet) GetRecommendationSetByID(orgID string, recommendationID string, user_permissions map[string][]string) (RecommendationSetResult, error) { + var recommendationSet RecommendationSetResult db := database.GetDB() - query := db.Joins("JOIN workloads ON recommendation_sets.workload_id = workloads.id"). - Joins("JOIN clusters ON workloads.cluster_id = clusters.id"). - Joins("JOIN rh_accounts ON clusters.tenant_id = rh_accounts.id"). - Preload("Workload.Cluster.RHAccount"). + query := db.Table("recommendation_sets"). + Select("recommendation_sets.id, "+ + "recommendation_sets.container_name AS container, "+ + "workloads.namespace AS project, "+ + "workloads.workload_name as workload, "+ + "workloads.workload_type, "+ + "clusters.source_id, "+ + "clusters.cluster_uuid, "+ + "clusters.cluster_alias, "+ + "clusters.last_reported_at AS last_reported, "+ + "recommendation_sets.recommendations"). + Joins(` + JOIN workloads ON recommendation_sets.workload_id = workloads.id + JOIN clusters ON workloads.cluster_id = clusters.id + JOIN rh_accounts ON clusters.tenant_id = rh_accounts.id + `).Model(&RecommendationSetResult{}). Where("rh_accounts.org_id = ?", orgID). Where("recommendation_sets.id = ?", recommendationID) add_rbac_filter(query, user_permissions) - query.First(&recommendationSet) - - return recommendationSet, nil + err := query.First(&recommendationSet).Error + return recommendationSet, err } func (r *RecommendationSet) CreateRecommendationSet(tx *gorm.DB) error { diff --git a/migrations/000002_create_clusters_table.up.sql b/migrations/000002_create_clusters_table.up.sql index 05560fd9..6ab87ec3 100644 --- a/migrations/000002_create_clusters_table.up.sql +++ b/migrations/000002_create_clusters_table.up.sql @@ -13,3 +13,6 @@ ON DELETE CASCADE; ALTER TABLE clusters ADD UNIQUE (tenant_id, source_id, cluster_uuid, cluster_alias); + +-- GET Recommendations optimization +CREATE INDEX IF NOT EXISTS idx_cluster_last_reported_at ON clusters (last_reported_at); diff --git a/migrations/000003_create_workloads_table.up.sql b/migrations/000003_create_workloads_table.up.sql index e725b910..c67714fa 100644 --- a/migrations/000003_create_workloads_table.up.sql +++ b/migrations/000003_create_workloads_table.up.sql @@ -20,3 +20,6 @@ CREATE INDEX idx_workloads_containers ON workloads USING gin(containers); ALTER TABLE workloads ADD UNIQUE (org_id, cluster_id, experiment_name); + +-- GET Recommendations optimization +CREATE INDEX IF NOT EXISTS idx_workloads_cluster_id ON workloads (cluster_id); diff --git a/migrations/000004_create_recommendation_sets_table.up.sql b/migrations/000004_create_recommendation_sets_table.up.sql index ab254410..fbd9eb79 100644 --- a/migrations/000004_create_recommendation_sets_table.up.sql +++ b/migrations/000004_create_recommendation_sets_table.up.sql @@ -14,3 +14,6 @@ ON DELETE CASCADE; ALTER TABLE recommendation_sets ADD CONSTRAINT UQ_Recommendation UNIQUE (workload_id, container_name); + +-- GET Recommendations optimization +CREATE INDEX IF NOT EXISTS idx_recommendation_set_workload_id ON recommendation_sets (workload_id);