diff --git a/api/docs.go b/api/docs.go index 0c885eb..0376d82 100644 --- a/api/docs.go +++ b/api/docs.go @@ -16,9 +16,9 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/api/v1/cost/info": { + "/api/v1/cost/estimate": { "get": { - "description": "Retrieve cost information for specified parameters within a defined date range. The date range must be within a 6-month period. Optionally, you can specify cost aggregation type and date order for the results.", + "description": "Fetch estimated cost details based on provider, region, instance type, and resource specifications. Pagination support is provided through ` + "`" + `Page` + "`" + ` and ` + "`" + `Size` + "`" + ` parameters.", "consumes": [ "application/json" ], @@ -26,21 +26,152 @@ const docTemplate = `{ "application/json" ], "tags": [ - "[Cost Management]" + "[Cost Estimate]" ], - "summary": "Get Cost Information", - "operationId": "GetCostInfo", + "summary": "Retrieve Estimated Cost Information", + "operationId": "GetEstimateCost", "parameters": [ { "type": "string", - "description": "Start date for the cost information retrieval in 'YYYY-MM-DD' format", + "description": "Cloud provider name to filter estimated costs", + "name": "providerName", + "in": "query" + }, + { + "type": "string", + "description": "Region name to filter estimated costs", + "name": "regionName", + "in": "query" + }, + { + "type": "string", + "description": "Instance type to filter estimated costs", + "name": "instanceType", + "in": "query" + }, + { + "type": "string", + "description": "Number of vCPUs to filter estimated costs", + "name": "vCpu", + "in": "query" + }, + { + "type": "string", + "description": "Memory size to filter estimated costs", + "name": "memory", + "in": "query" + }, + { + "type": "string", + "description": "Operating system type to filter estimated costs", + "name": "osType", + "in": "query" + }, + { + "type": "integer", + "description": "Page number for pagination (default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Number of records per page (default: 100, max: 100)", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successfully retrieved estimated cost information", + "schema": { + "$ref": "#/definitions/app.AntResponse-cost_EstimateCostInfoResults" + } + }, + "400": { + "description": "Invalid request parameters", + "schema": { + "$ref": "#/definitions/app.AntResponse-string" + } + }, + "500": { + "description": "Failed to retrieve estimated cost information", + "schema": { + "$ref": "#/definitions/app.AntResponse-string" + } + } + } + }, + "post": { + "description": "Update the estimate cost based on provided specifications and retrieve the updated cost estimation. Required fields for each specification include ` + "`" + `ProviderName` + "`" + `, ` + "`" + `RegionName` + "`" + `, and ` + "`" + `InstanceType` + "`" + `. Specifications can also be provided in a formatted string using ` + "`" + `+` + "`" + ` delimiter.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "[Cost Estimate]" + ], + "summary": "Update and Retrieve Estimated Cost Information", + "operationId": "UpdateAndGetEstimateCost", + "parameters": [ + { + "description": "Request body for updating and retrieving estimated cost information", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.UpdateAndGetEstimateCostReq" + } + } + ], + "responses": { + "200": { + "description": "Successfully updated and retrieved estimated cost information", + "schema": { + "$ref": "#/definitions/app.AntResponse-cost_EstimateCostResults" + } + }, + "400": { + "description": "Invalid request parameters or format", + "schema": { + "$ref": "#/definitions/app.AntResponse-string" + } + }, + "500": { + "description": "Failed to update or retrieve estimated cost information", + "schema": { + "$ref": "#/definitions/app.AntResponse-string" + } + } + } + } + }, + "/api/v1/cost/estimate/forecast": { + "get": { + "description": "Fetch estimated forecast cost data based on specified parameters, including a date range that must be within 6 months. Supports pagination and filtering by namespace IDs, migration configuration IDs, and resource types.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "[Cost Estimate]" + ], + "summary": "Retrieve Estimated Forecast Cost Information", + "operationId": "GetEstimateForecastCost", + "parameters": [ + { + "type": "string", + "description": "Start date for the forecast cost retrieval in 'YYYY-MM-DD' format", "name": "startDate", "in": "query", "required": true }, { "type": "string", - "description": "End date for the cost information retrieval in 'YYYY-MM-DD' format", + "description": "End date for the forecast cost retrieval in 'YYYY-MM-DD' format", "name": "endDate", "in": "query", "required": true @@ -51,8 +182,18 @@ const docTemplate = `{ "type": "string" }, "collectionFormat": "csv", - "description": "List of migration IDs to filter the cost information", - "name": "migrationIds", + "description": "List of namespace IDs to filter forecast cost information", + "name": "nsIds", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "List of migration configuration IDs to filter forecast cost information", + "name": "mciIds", "in": "query" }, { @@ -61,8 +202,8 @@ const docTemplate = `{ "type": "string" }, "collectionFormat": "csv", - "description": "List of cloud providers to filter the cost information", - "name": "provider", + "description": "List of cloud providers to filter forecast cost information", + "name": "providers", "in": "query" }, { @@ -71,7 +212,7 @@ const docTemplate = `{ "type": "string" }, "collectionFormat": "csv", - "description": "List of resource types to filter the cost information", + "description": "List of resource types to filter forecast cost information", "name": "resourceTypes", "in": "query" }, @@ -81,16 +222,15 @@ const docTemplate = `{ "type": "string" }, "collectionFormat": "csv", - "description": "List of resource IDs to filter the cost information", + "description": "List of resource IDs to filter forecast cost information", "name": "resourceIds", "in": "query" }, { "type": "string", - "description": "Type of cost aggregation for the results (e.g., 'daily', 'weekly', 'monthly')", + "description": "Type of cost aggregation (e.g., 'daily', 'weekly', 'monthly')", "name": "costAggregationType", - "in": "query", - "required": true + "in": "query" }, { "type": "string", @@ -103,23 +243,35 @@ const docTemplate = `{ "description": "Order of resource types in the result (e.g., 'asc', 'desc')", "name": "resourceTypeOrder", "in": "query" + }, + { + "type": "integer", + "description": "Page number for pagination (default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Number of records per page (default: 10000, max: 10000)", + "name": "size", + "in": "query" } ], "responses": { "200": { - "description": "Successfully retrieved cost information", + "description": "Successfully retrieved estimated forecast cost information", "schema": { - "$ref": "#/definitions/app.AntResponse-array_cost_GetCostInfoResult" + "$ref": "#/definitions/app.AntResponse-cost_GetEstimateForecastCostInfoResults" } }, "400": { - "description": "Invalid request parameters", + "description": "Invalid request parameters or date format errors", "schema": { "$ref": "#/definitions/app.AntResponse-string" } }, "500": { - "description": "Failed to retrieve cost information", + "description": "Failed to retrieve estimated forecast cost information", "schema": { "$ref": "#/definitions/app.AntResponse-string" } @@ -127,7 +279,7 @@ const docTemplate = `{ } }, "post": { - "description": "Update cost information for specified resources, including details such as migration ID, cost resources, and additional AWS info if applicable. The request body must include a valid migration ID and a list of cost resources. If AWS-specific details are provided, ensure all required fields are populated.", + "description": "Update and retrieve forecasted cost estimates for a specified namespace and migration configuration ID over the past 14 days.", "consumes": [ "application/json" ], @@ -135,36 +287,83 @@ const docTemplate = `{ "application/json" ], "tags": [ - "[Cost Management]" + "[Cost Estimate]" ], - "summary": "Update Cost Information", - "operationId": "UpdateCostInfo", + "summary": "Update and Retrieve Estimated Forecast Cost", + "operationId": "UpdateEstimateForecastCost", "parameters": [ { - "description": "Request body containing cost update information", + "description": "Request body containing NsId (Namespace ID) and MciId (Migration Configuration ID) for cost estimation forecast", "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/app.UpdateCostInfoReq" + "$ref": "#/definitions/app.UpdateEstimateForecastCostReq" } } ], "responses": { "200": { - "description": "Successfully updated cost information", + "description": "Successfully updated and retrieved estimated forecast cost information", "schema": { - "$ref": "#/definitions/app.AntResponse-cost_UpdateCostInfoResult" + "$ref": "#/definitions/app.AntResponse-cost_UpdateEstimateForecastCostInfoResult" } }, "400": { - "description": "Invalid request parameters", + "description": "Request body binding error", "schema": { "$ref": "#/definitions/app.AntResponse-string" } }, "500": { - "description": "Failed to update cost information", + "description": "Failed to update or retrieve forecast cost information", + "schema": { + "$ref": "#/definitions/app.AntResponse-string" + } + } + } + } + }, + "/api/v1/cost/estimate/forecast/raw": { + "post": { + "description": "Update and retrieve raw forecasted cost estimates for specified cost resources and additional AWS information over the past 14 days.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "[Cost Estimate]" + ], + "summary": "Update and Retrieve Raw Estimated Forecast Cost", + "operationId": "UpdateEstimateForecastCostRaw", + "parameters": [ + { + "description": "Request body containing details for cost estimation forecast", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.UpdateEstimateForecastCostRawReq" + } + } + ], + "responses": { + "200": { + "description": "Successfully updated and retrieved raw estimated forecast cost information in raw data", + "schema": { + "$ref": "#/definitions/app.AntResponse-cost_UpdateEstimateForecastCostInfoResult" + } + }, + "400": { + "description": "Migrated resource id list is required", + "schema": { + "$ref": "#/definitions/app.AntResponse-string" + } + }, + "500": { + "description": "Error updating or retrieving forecast cost information", "schema": { "$ref": "#/definitions/app.AntResponse-string" } @@ -887,126 +1086,6 @@ const docTemplate = `{ } } }, - "/api/v1/price/info": { - "get": { - "description": "Retrieve pricing information for cloud resources based on specified query parameters. Returns price data based on provider, region, instance type, vCPU, memory, and OS type. It offer instances with the lowest monthly prices what in the database.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Price Management" - ], - "summary": "Get Price Information", - "operationId": "GetPriceInfos", - "parameters": [ - { - "type": "string", - "description": "Cloud provider name - aws|alibaba|tencent|gcp|azure|ibm", - "name": "providerName", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "Region name", - "name": "regionName", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "Instance type", - "name": "instanceType", - "in": "query" - }, - { - "type": "string", - "description": "Number of vCPUs", - "name": "vCpu", - "in": "query" - }, - { - "type": "string", - "description": "Amount of memory", - "name": "memory", - "in": "query" - }, - { - "type": "string", - "description": "Operating system type", - "name": "osType", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Successfully retrieved pricing information", - "schema": { - "$ref": "#/definitions/app.AntResponse-string" - } - }, - "400": { - "description": "Invalid request parameters", - "schema": { - "$ref": "#/definitions/app.AntResponse-string" - } - }, - "500": { - "description": "Failed to retrieve pricing information", - "schema": { - "$ref": "#/definitions/app.AntResponse-string" - } - } - } - }, - "post": { - "description": "Retrieve pricing information for cloud resources based on specified parameters. If saved data is more than 7 days, fetch new data and insert new price data even if same price as before.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "[Price Management]" - ], - "operationId": "UpdatePriceInfos", - "parameters": [ - { - "description": "Request body containing get price information", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/app.UpdatePriceInfosReq" - } - } - ], - "responses": { - "200": { - "description": "Successfully retrieved pricing information", - "schema": { - "$ref": "#/definitions/app.AntResponse-string" - } - }, - "400": { - "description": "Invalid request parameters", - "schema": { - "$ref": "#/definitions/app.AntResponse-string" - } - }, - "500": { - "description": "Failed to retrieve pricing information", - "schema": { - "$ref": "#/definitions/app.AntResponse-string" - } - } - } - } - }, "/readyz": { "get": { "description": "This endpoint checks if the CB-Ant API server is ready by verifying the status of both the load service and the cost service. If either service is unavailable, it returns a 503 status indicating the server is not ready.", @@ -1045,7 +1124,7 @@ const docTemplate = `{ } }, "definitions": { - "app.AntResponse-array_cost_GetCostInfoResult": { + "app.AntResponse-array_load_LoadTestStatistics": { "type": "object", "properties": { "code": { @@ -1057,7 +1136,7 @@ const docTemplate = `{ "result": { "type": "array", "items": { - "$ref": "#/definitions/cost.GetCostInfoResult" + "$ref": "#/definitions/load.LoadTestStatistics" } }, "successMessage": { @@ -1065,7 +1144,7 @@ const docTemplate = `{ } } }, - "app.AntResponse-array_load_LoadTestStatistics": { + "app.AntResponse-array_load_MetricsSummary": { "type": "object", "properties": { "code": { @@ -1077,7 +1156,7 @@ const docTemplate = `{ "result": { "type": "array", "items": { - "$ref": "#/definitions/load.LoadTestStatistics" + "$ref": "#/definitions/load.MetricsSummary" } }, "successMessage": { @@ -1085,7 +1164,7 @@ const docTemplate = `{ } } }, - "app.AntResponse-array_load_MetricsSummary": { + "app.AntResponse-array_load_ResultSummary": { "type": "object", "properties": { "code": { @@ -1097,7 +1176,7 @@ const docTemplate = `{ "result": { "type": "array", "items": { - "$ref": "#/definitions/load.MetricsSummary" + "$ref": "#/definitions/load.ResultSummary" } }, "successMessage": { @@ -1105,7 +1184,7 @@ const docTemplate = `{ } } }, - "app.AntResponse-array_load_ResultSummary": { + "app.AntResponse-cost_EstimateCostInfoResults": { "type": "object", "properties": { "code": { @@ -1115,17 +1194,31 @@ const docTemplate = `{ "type": "string" }, "result": { - "type": "array", - "items": { - "$ref": "#/definitions/load.ResultSummary" - } + "$ref": "#/definitions/cost.EstimateCostInfoResults" + }, + "successMessage": { + "type": "string" + } + } + }, + "app.AntResponse-cost_EstimateCostResults": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "errorMessage": { + "type": "string" + }, + "result": { + "$ref": "#/definitions/cost.EstimateCostResults" }, "successMessage": { "type": "string" } } }, - "app.AntResponse-cost_UpdateCostInfoResult": { + "app.AntResponse-cost_GetEstimateForecastCostInfoResults": { "type": "object", "properties": { "code": { @@ -1135,7 +1228,24 @@ const docTemplate = `{ "type": "string" }, "result": { - "$ref": "#/definitions/cost.UpdateCostInfoResult" + "$ref": "#/definitions/cost.GetEstimateForecastCostInfoResults" + }, + "successMessage": { + "type": "string" + } + } + }, + "app.AntResponse-cost_UpdateEstimateForecastCostInfoResult": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "errorMessage": { + "type": "string" + }, + "result": { + "$ref": "#/definitions/cost.UpdateEstimateForecastCostInfoResult" }, "successMessage": { "type": "string" @@ -1443,45 +1553,77 @@ const docTemplate = `{ } } }, - "app.UpdateCostInfoReq": { + "app.UpdateAndGetEstimateCostReq": { + "type": "object", + "properties": { + "specs": { + "type": "array", + "items": { + "type": "object", + "required": [ + "instanceType", + "providerName", + "regionName" + ], + "properties": { + "image": { + "type": "string" + }, + "instanceType": { + "type": "string" + }, + "providerName": { + "type": "string" + }, + "regionName": { + "type": "string" + } + } + } + }, + "specsWithFormat": { + "type": "array", + "items": { + "type": "object", + "required": [ + "commonSpec" + ], + "properties": { + "commonImage": { + "type": "string" + }, + "commonSpec": { + "type": "string" + } + } + } + } + } + }, + "app.UpdateEstimateForecastCostRawReq": { "type": "object", "required": [ - "connectionName", "costResources" ], "properties": { "awsAdditionalInfo": { "$ref": "#/definitions/app.AwsAdditionalInfoReq" }, - "connectionName": { - "type": "string" - }, "costResources": { "type": "array", "items": { "$ref": "#/definitions/app.CostResourceReq" } - }, - "migrationId": { - "type": "string" } } }, - "app.UpdatePriceInfosReq": { + "app.UpdateEstimateForecastCostReq": { "type": "object", - "required": [ - "instanceType", - "providerName", - "regionName" - ], "properties": { - "instanceType": { - "type": "string" - }, - "providerName": { + "mciId": { "type": "string" }, - "regionName": { + "nsId": { "type": "string" } } @@ -1526,6 +1668,37 @@ const docTemplate = `{ "Remote" ] }, + "constant.PriceCurrency": { + "type": "string", + "enum": [ + "USD", + "KRW" + ], + "x-enum-varnames": [ + "USD", + "KRW" + ] + }, + "constant.PricePolicy": { + "type": "string", + "enum": [ + "OnDemand" + ], + "x-enum-varnames": [ + "OnDemand" + ] + }, + "constant.PriceUnit": { + "type": "string", + "enum": [ + "PerHour", + "PerYear" + ], + "x-enum-varnames": [ + "PerHour", + "PerYear" + ] + }, "constant.ResourceType": { "type": "string", "enum": [ @@ -1541,7 +1714,164 @@ const docTemplate = `{ "Etc" ] }, - "cost.GetCostInfoResult": { + "cost.EsimateCostSpecResults": { + "type": "object", + "properties": { + "estimateForecastCostSpecDetailResults": { + "type": "array", + "items": { + "$ref": "#/definitions/cost.EstimateCostSpecDetailResult" + } + }, + "imageName": { + "type": "string" + }, + "instanceType": { + "type": "string" + }, + "providerName": { + "type": "string" + }, + "regionName": { + "type": "string" + }, + "totalMaxMonthlyPrice": { + "type": "number" + }, + "totalMinMonthlyPrice": { + "type": "number" + } + } + }, + "cost.EstimateCostInfoResult": { + "type": "object", + "properties": { + "calculatedMonthlyPrice": { + "type": "number" + }, + "currency": { + "$ref": "#/definitions/constant.PriceCurrency" + }, + "id": { + "type": "integer" + }, + "instanceType": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "originalPricePolicy": { + "type": "string" + }, + "osType": { + "type": "string" + }, + "price": { + "type": "string" + }, + "priceDescription": { + "type": "string" + }, + "pricePolicy": { + "$ref": "#/definitions/constant.PricePolicy" + }, + "productDescription": { + "type": "string" + }, + "providerName": { + "type": "string" + }, + "regionName": { + "type": "string" + }, + "storage": { + "type": "string" + }, + "unit": { + "$ref": "#/definitions/constant.PriceUnit" + }, + "vCpu": { + "type": "string" + } + } + }, + "cost.EstimateCostInfoResults": { + "type": "object", + "properties": { + "estimateCostInfoResult": { + "type": "array", + "items": { + "$ref": "#/definitions/cost.EstimateCostInfoResult" + } + }, + "resultCount": { + "type": "integer" + } + } + }, + "cost.EstimateCostResults": { + "type": "object", + "properties": { + "esimateCostSpecResults": { + "type": "array", + "items": { + "$ref": "#/definitions/cost.EsimateCostSpecResults" + } + } + } + }, + "cost.EstimateCostSpecDetailResult": { + "type": "object", + "properties": { + "calculatedMonthlyPrice": { + "type": "number" + }, + "currency": { + "$ref": "#/definitions/constant.PriceCurrency" + }, + "id": { + "type": "integer" + }, + "lastUpdatedAt": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "originalPricePolicy": { + "type": "string" + }, + "osType": { + "type": "string" + }, + "price": { + "type": "string" + }, + "priceDescription": { + "type": "string" + }, + "pricePolicy": { + "$ref": "#/definitions/constant.PricePolicy" + }, + "productDescription": { + "type": "string" + }, + "storage": { + "type": "string" + }, + "unit": { + "$ref": "#/definitions/constant.PriceUnit" + }, + "vCpu": { + "type": "string" + } + } + }, + "cost.GetEstimateForecastCostInfoResult": { "type": "object", "properties": { "category": { @@ -1567,7 +1897,21 @@ const docTemplate = `{ } } }, - "cost.UpdateCostInfoResult": { + "cost.GetEstimateForecastCostInfoResults": { + "type": "object", + "properties": { + "getEstimateForecastCostInfoResults": { + "type": "array", + "items": { + "$ref": "#/definitions/cost.GetEstimateForecastCostInfoResult" + } + }, + "resultCount": { + "type": "integer" + } + } + }, + "cost.UpdateEstimateForecastCostInfoResult": { "type": "object", "properties": { "fetchedDataCount": { diff --git a/api/swagger.json b/api/swagger.json index 42848e0..601693c 100644 --- a/api/swagger.json +++ b/api/swagger.json @@ -8,9 +8,9 @@ }, "basePath": "/ant", "paths": { - "/api/v1/cost/info": { + "/api/v1/cost/estimate": { "get": { - "description": "Retrieve cost information for specified parameters within a defined date range. The date range must be within a 6-month period. Optionally, you can specify cost aggregation type and date order for the results.", + "description": "Fetch estimated cost details based on provider, region, instance type, and resource specifications. Pagination support is provided through `Page` and `Size` parameters.", "consumes": [ "application/json" ], @@ -18,21 +18,152 @@ "application/json" ], "tags": [ - "[Cost Management]" + "[Cost Estimate]" ], - "summary": "Get Cost Information", - "operationId": "GetCostInfo", + "summary": "Retrieve Estimated Cost Information", + "operationId": "GetEstimateCost", "parameters": [ { "type": "string", - "description": "Start date for the cost information retrieval in 'YYYY-MM-DD' format", + "description": "Cloud provider name to filter estimated costs", + "name": "providerName", + "in": "query" + }, + { + "type": "string", + "description": "Region name to filter estimated costs", + "name": "regionName", + "in": "query" + }, + { + "type": "string", + "description": "Instance type to filter estimated costs", + "name": "instanceType", + "in": "query" + }, + { + "type": "string", + "description": "Number of vCPUs to filter estimated costs", + "name": "vCpu", + "in": "query" + }, + { + "type": "string", + "description": "Memory size to filter estimated costs", + "name": "memory", + "in": "query" + }, + { + "type": "string", + "description": "Operating system type to filter estimated costs", + "name": "osType", + "in": "query" + }, + { + "type": "integer", + "description": "Page number for pagination (default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Number of records per page (default: 100, max: 100)", + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successfully retrieved estimated cost information", + "schema": { + "$ref": "#/definitions/app.AntResponse-cost_EstimateCostInfoResults" + } + }, + "400": { + "description": "Invalid request parameters", + "schema": { + "$ref": "#/definitions/app.AntResponse-string" + } + }, + "500": { + "description": "Failed to retrieve estimated cost information", + "schema": { + "$ref": "#/definitions/app.AntResponse-string" + } + } + } + }, + "post": { + "description": "Update the estimate cost based on provided specifications and retrieve the updated cost estimation. Required fields for each specification include `ProviderName`, `RegionName`, and `InstanceType`. Specifications can also be provided in a formatted string using `+` delimiter.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "[Cost Estimate]" + ], + "summary": "Update and Retrieve Estimated Cost Information", + "operationId": "UpdateAndGetEstimateCost", + "parameters": [ + { + "description": "Request body for updating and retrieving estimated cost information", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.UpdateAndGetEstimateCostReq" + } + } + ], + "responses": { + "200": { + "description": "Successfully updated and retrieved estimated cost information", + "schema": { + "$ref": "#/definitions/app.AntResponse-cost_EstimateCostResults" + } + }, + "400": { + "description": "Invalid request parameters or format", + "schema": { + "$ref": "#/definitions/app.AntResponse-string" + } + }, + "500": { + "description": "Failed to update or retrieve estimated cost information", + "schema": { + "$ref": "#/definitions/app.AntResponse-string" + } + } + } + } + }, + "/api/v1/cost/estimate/forecast": { + "get": { + "description": "Fetch estimated forecast cost data based on specified parameters, including a date range that must be within 6 months. Supports pagination and filtering by namespace IDs, migration configuration IDs, and resource types.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "[Cost Estimate]" + ], + "summary": "Retrieve Estimated Forecast Cost Information", + "operationId": "GetEstimateForecastCost", + "parameters": [ + { + "type": "string", + "description": "Start date for the forecast cost retrieval in 'YYYY-MM-DD' format", "name": "startDate", "in": "query", "required": true }, { "type": "string", - "description": "End date for the cost information retrieval in 'YYYY-MM-DD' format", + "description": "End date for the forecast cost retrieval in 'YYYY-MM-DD' format", "name": "endDate", "in": "query", "required": true @@ -43,8 +174,18 @@ "type": "string" }, "collectionFormat": "csv", - "description": "List of migration IDs to filter the cost information", - "name": "migrationIds", + "description": "List of namespace IDs to filter forecast cost information", + "name": "nsIds", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "List of migration configuration IDs to filter forecast cost information", + "name": "mciIds", "in": "query" }, { @@ -53,8 +194,8 @@ "type": "string" }, "collectionFormat": "csv", - "description": "List of cloud providers to filter the cost information", - "name": "provider", + "description": "List of cloud providers to filter forecast cost information", + "name": "providers", "in": "query" }, { @@ -63,7 +204,7 @@ "type": "string" }, "collectionFormat": "csv", - "description": "List of resource types to filter the cost information", + "description": "List of resource types to filter forecast cost information", "name": "resourceTypes", "in": "query" }, @@ -73,16 +214,15 @@ "type": "string" }, "collectionFormat": "csv", - "description": "List of resource IDs to filter the cost information", + "description": "List of resource IDs to filter forecast cost information", "name": "resourceIds", "in": "query" }, { "type": "string", - "description": "Type of cost aggregation for the results (e.g., 'daily', 'weekly', 'monthly')", + "description": "Type of cost aggregation (e.g., 'daily', 'weekly', 'monthly')", "name": "costAggregationType", - "in": "query", - "required": true + "in": "query" }, { "type": "string", @@ -95,23 +235,35 @@ "description": "Order of resource types in the result (e.g., 'asc', 'desc')", "name": "resourceTypeOrder", "in": "query" + }, + { + "type": "integer", + "description": "Page number for pagination (default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Number of records per page (default: 10000, max: 10000)", + "name": "size", + "in": "query" } ], "responses": { "200": { - "description": "Successfully retrieved cost information", + "description": "Successfully retrieved estimated forecast cost information", "schema": { - "$ref": "#/definitions/app.AntResponse-array_cost_GetCostInfoResult" + "$ref": "#/definitions/app.AntResponse-cost_GetEstimateForecastCostInfoResults" } }, "400": { - "description": "Invalid request parameters", + "description": "Invalid request parameters or date format errors", "schema": { "$ref": "#/definitions/app.AntResponse-string" } }, "500": { - "description": "Failed to retrieve cost information", + "description": "Failed to retrieve estimated forecast cost information", "schema": { "$ref": "#/definitions/app.AntResponse-string" } @@ -119,7 +271,7 @@ } }, "post": { - "description": "Update cost information for specified resources, including details such as migration ID, cost resources, and additional AWS info if applicable. The request body must include a valid migration ID and a list of cost resources. If AWS-specific details are provided, ensure all required fields are populated.", + "description": "Update and retrieve forecasted cost estimates for a specified namespace and migration configuration ID over the past 14 days.", "consumes": [ "application/json" ], @@ -127,36 +279,83 @@ "application/json" ], "tags": [ - "[Cost Management]" + "[Cost Estimate]" ], - "summary": "Update Cost Information", - "operationId": "UpdateCostInfo", + "summary": "Update and Retrieve Estimated Forecast Cost", + "operationId": "UpdateEstimateForecastCost", "parameters": [ { - "description": "Request body containing cost update information", + "description": "Request body containing NsId (Namespace ID) and MciId (Migration Configuration ID) for cost estimation forecast", "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/app.UpdateCostInfoReq" + "$ref": "#/definitions/app.UpdateEstimateForecastCostReq" } } ], "responses": { "200": { - "description": "Successfully updated cost information", + "description": "Successfully updated and retrieved estimated forecast cost information", "schema": { - "$ref": "#/definitions/app.AntResponse-cost_UpdateCostInfoResult" + "$ref": "#/definitions/app.AntResponse-cost_UpdateEstimateForecastCostInfoResult" } }, "400": { - "description": "Invalid request parameters", + "description": "Request body binding error", "schema": { "$ref": "#/definitions/app.AntResponse-string" } }, "500": { - "description": "Failed to update cost information", + "description": "Failed to update or retrieve forecast cost information", + "schema": { + "$ref": "#/definitions/app.AntResponse-string" + } + } + } + } + }, + "/api/v1/cost/estimate/forecast/raw": { + "post": { + "description": "Update and retrieve raw forecasted cost estimates for specified cost resources and additional AWS information over the past 14 days.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "[Cost Estimate]" + ], + "summary": "Update and Retrieve Raw Estimated Forecast Cost", + "operationId": "UpdateEstimateForecastCostRaw", + "parameters": [ + { + "description": "Request body containing details for cost estimation forecast", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.UpdateEstimateForecastCostRawReq" + } + } + ], + "responses": { + "200": { + "description": "Successfully updated and retrieved raw estimated forecast cost information in raw data", + "schema": { + "$ref": "#/definitions/app.AntResponse-cost_UpdateEstimateForecastCostInfoResult" + } + }, + "400": { + "description": "Migrated resource id list is required", + "schema": { + "$ref": "#/definitions/app.AntResponse-string" + } + }, + "500": { + "description": "Error updating or retrieving forecast cost information", "schema": { "$ref": "#/definitions/app.AntResponse-string" } @@ -879,126 +1078,6 @@ } } }, - "/api/v1/price/info": { - "get": { - "description": "Retrieve pricing information for cloud resources based on specified query parameters. Returns price data based on provider, region, instance type, vCPU, memory, and OS type. It offer instances with the lowest monthly prices what in the database.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Price Management" - ], - "summary": "Get Price Information", - "operationId": "GetPriceInfos", - "parameters": [ - { - "type": "string", - "description": "Cloud provider name - aws|alibaba|tencent|gcp|azure|ibm", - "name": "providerName", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "Region name", - "name": "regionName", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "Instance type", - "name": "instanceType", - "in": "query" - }, - { - "type": "string", - "description": "Number of vCPUs", - "name": "vCpu", - "in": "query" - }, - { - "type": "string", - "description": "Amount of memory", - "name": "memory", - "in": "query" - }, - { - "type": "string", - "description": "Operating system type", - "name": "osType", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Successfully retrieved pricing information", - "schema": { - "$ref": "#/definitions/app.AntResponse-string" - } - }, - "400": { - "description": "Invalid request parameters", - "schema": { - "$ref": "#/definitions/app.AntResponse-string" - } - }, - "500": { - "description": "Failed to retrieve pricing information", - "schema": { - "$ref": "#/definitions/app.AntResponse-string" - } - } - } - }, - "post": { - "description": "Retrieve pricing information for cloud resources based on specified parameters. If saved data is more than 7 days, fetch new data and insert new price data even if same price as before.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "[Price Management]" - ], - "operationId": "UpdatePriceInfos", - "parameters": [ - { - "description": "Request body containing get price information", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/app.UpdatePriceInfosReq" - } - } - ], - "responses": { - "200": { - "description": "Successfully retrieved pricing information", - "schema": { - "$ref": "#/definitions/app.AntResponse-string" - } - }, - "400": { - "description": "Invalid request parameters", - "schema": { - "$ref": "#/definitions/app.AntResponse-string" - } - }, - "500": { - "description": "Failed to retrieve pricing information", - "schema": { - "$ref": "#/definitions/app.AntResponse-string" - } - } - } - } - }, "/readyz": { "get": { "description": "This endpoint checks if the CB-Ant API server is ready by verifying the status of both the load service and the cost service. If either service is unavailable, it returns a 503 status indicating the server is not ready.", @@ -1037,7 +1116,7 @@ } }, "definitions": { - "app.AntResponse-array_cost_GetCostInfoResult": { + "app.AntResponse-array_load_LoadTestStatistics": { "type": "object", "properties": { "code": { @@ -1049,7 +1128,7 @@ "result": { "type": "array", "items": { - "$ref": "#/definitions/cost.GetCostInfoResult" + "$ref": "#/definitions/load.LoadTestStatistics" } }, "successMessage": { @@ -1057,7 +1136,7 @@ } } }, - "app.AntResponse-array_load_LoadTestStatistics": { + "app.AntResponse-array_load_MetricsSummary": { "type": "object", "properties": { "code": { @@ -1069,7 +1148,7 @@ "result": { "type": "array", "items": { - "$ref": "#/definitions/load.LoadTestStatistics" + "$ref": "#/definitions/load.MetricsSummary" } }, "successMessage": { @@ -1077,7 +1156,7 @@ } } }, - "app.AntResponse-array_load_MetricsSummary": { + "app.AntResponse-array_load_ResultSummary": { "type": "object", "properties": { "code": { @@ -1089,7 +1168,7 @@ "result": { "type": "array", "items": { - "$ref": "#/definitions/load.MetricsSummary" + "$ref": "#/definitions/load.ResultSummary" } }, "successMessage": { @@ -1097,7 +1176,7 @@ } } }, - "app.AntResponse-array_load_ResultSummary": { + "app.AntResponse-cost_EstimateCostInfoResults": { "type": "object", "properties": { "code": { @@ -1107,17 +1186,31 @@ "type": "string" }, "result": { - "type": "array", - "items": { - "$ref": "#/definitions/load.ResultSummary" - } + "$ref": "#/definitions/cost.EstimateCostInfoResults" + }, + "successMessage": { + "type": "string" + } + } + }, + "app.AntResponse-cost_EstimateCostResults": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "errorMessage": { + "type": "string" + }, + "result": { + "$ref": "#/definitions/cost.EstimateCostResults" }, "successMessage": { "type": "string" } } }, - "app.AntResponse-cost_UpdateCostInfoResult": { + "app.AntResponse-cost_GetEstimateForecastCostInfoResults": { "type": "object", "properties": { "code": { @@ -1127,7 +1220,24 @@ "type": "string" }, "result": { - "$ref": "#/definitions/cost.UpdateCostInfoResult" + "$ref": "#/definitions/cost.GetEstimateForecastCostInfoResults" + }, + "successMessage": { + "type": "string" + } + } + }, + "app.AntResponse-cost_UpdateEstimateForecastCostInfoResult": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "errorMessage": { + "type": "string" + }, + "result": { + "$ref": "#/definitions/cost.UpdateEstimateForecastCostInfoResult" }, "successMessage": { "type": "string" @@ -1435,45 +1545,77 @@ } } }, - "app.UpdateCostInfoReq": { + "app.UpdateAndGetEstimateCostReq": { + "type": "object", + "properties": { + "specs": { + "type": "array", + "items": { + "type": "object", + "required": [ + "instanceType", + "providerName", + "regionName" + ], + "properties": { + "image": { + "type": "string" + }, + "instanceType": { + "type": "string" + }, + "providerName": { + "type": "string" + }, + "regionName": { + "type": "string" + } + } + } + }, + "specsWithFormat": { + "type": "array", + "items": { + "type": "object", + "required": [ + "commonSpec" + ], + "properties": { + "commonImage": { + "type": "string" + }, + "commonSpec": { + "type": "string" + } + } + } + } + } + }, + "app.UpdateEstimateForecastCostRawReq": { "type": "object", "required": [ - "connectionName", "costResources" ], "properties": { "awsAdditionalInfo": { "$ref": "#/definitions/app.AwsAdditionalInfoReq" }, - "connectionName": { - "type": "string" - }, "costResources": { "type": "array", "items": { "$ref": "#/definitions/app.CostResourceReq" } - }, - "migrationId": { - "type": "string" } } }, - "app.UpdatePriceInfosReq": { + "app.UpdateEstimateForecastCostReq": { "type": "object", - "required": [ - "instanceType", - "providerName", - "regionName" - ], "properties": { - "instanceType": { - "type": "string" - }, - "providerName": { + "mciId": { "type": "string" }, - "regionName": { + "nsId": { "type": "string" } } @@ -1518,6 +1660,37 @@ "Remote" ] }, + "constant.PriceCurrency": { + "type": "string", + "enum": [ + "USD", + "KRW" + ], + "x-enum-varnames": [ + "USD", + "KRW" + ] + }, + "constant.PricePolicy": { + "type": "string", + "enum": [ + "OnDemand" + ], + "x-enum-varnames": [ + "OnDemand" + ] + }, + "constant.PriceUnit": { + "type": "string", + "enum": [ + "PerHour", + "PerYear" + ], + "x-enum-varnames": [ + "PerHour", + "PerYear" + ] + }, "constant.ResourceType": { "type": "string", "enum": [ @@ -1533,7 +1706,164 @@ "Etc" ] }, - "cost.GetCostInfoResult": { + "cost.EsimateCostSpecResults": { + "type": "object", + "properties": { + "estimateForecastCostSpecDetailResults": { + "type": "array", + "items": { + "$ref": "#/definitions/cost.EstimateCostSpecDetailResult" + } + }, + "imageName": { + "type": "string" + }, + "instanceType": { + "type": "string" + }, + "providerName": { + "type": "string" + }, + "regionName": { + "type": "string" + }, + "totalMaxMonthlyPrice": { + "type": "number" + }, + "totalMinMonthlyPrice": { + "type": "number" + } + } + }, + "cost.EstimateCostInfoResult": { + "type": "object", + "properties": { + "calculatedMonthlyPrice": { + "type": "number" + }, + "currency": { + "$ref": "#/definitions/constant.PriceCurrency" + }, + "id": { + "type": "integer" + }, + "instanceType": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "originalPricePolicy": { + "type": "string" + }, + "osType": { + "type": "string" + }, + "price": { + "type": "string" + }, + "priceDescription": { + "type": "string" + }, + "pricePolicy": { + "$ref": "#/definitions/constant.PricePolicy" + }, + "productDescription": { + "type": "string" + }, + "providerName": { + "type": "string" + }, + "regionName": { + "type": "string" + }, + "storage": { + "type": "string" + }, + "unit": { + "$ref": "#/definitions/constant.PriceUnit" + }, + "vCpu": { + "type": "string" + } + } + }, + "cost.EstimateCostInfoResults": { + "type": "object", + "properties": { + "estimateCostInfoResult": { + "type": "array", + "items": { + "$ref": "#/definitions/cost.EstimateCostInfoResult" + } + }, + "resultCount": { + "type": "integer" + } + } + }, + "cost.EstimateCostResults": { + "type": "object", + "properties": { + "esimateCostSpecResults": { + "type": "array", + "items": { + "$ref": "#/definitions/cost.EsimateCostSpecResults" + } + } + } + }, + "cost.EstimateCostSpecDetailResult": { + "type": "object", + "properties": { + "calculatedMonthlyPrice": { + "type": "number" + }, + "currency": { + "$ref": "#/definitions/constant.PriceCurrency" + }, + "id": { + "type": "integer" + }, + "lastUpdatedAt": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "originalPricePolicy": { + "type": "string" + }, + "osType": { + "type": "string" + }, + "price": { + "type": "string" + }, + "priceDescription": { + "type": "string" + }, + "pricePolicy": { + "$ref": "#/definitions/constant.PricePolicy" + }, + "productDescription": { + "type": "string" + }, + "storage": { + "type": "string" + }, + "unit": { + "$ref": "#/definitions/constant.PriceUnit" + }, + "vCpu": { + "type": "string" + } + } + }, + "cost.GetEstimateForecastCostInfoResult": { "type": "object", "properties": { "category": { @@ -1559,7 +1889,21 @@ } } }, - "cost.UpdateCostInfoResult": { + "cost.GetEstimateForecastCostInfoResults": { + "type": "object", + "properties": { + "getEstimateForecastCostInfoResults": { + "type": "array", + "items": { + "$ref": "#/definitions/cost.GetEstimateForecastCostInfoResult" + } + }, + "resultCount": { + "type": "integer" + } + } + }, + "cost.UpdateEstimateForecastCostInfoResult": { "type": "object", "properties": { "fetchedDataCount": { diff --git a/api/swagger.yaml b/api/swagger.yaml index a835ed8..83317a5 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1,6 +1,6 @@ basePath: /ant definitions: - app.AntResponse-array_cost_GetCostInfoResult: + app.AntResponse-array_load_LoadTestStatistics: properties: code: type: integer @@ -8,12 +8,12 @@ definitions: type: string result: items: - $ref: '#/definitions/cost.GetCostInfoResult' + $ref: '#/definitions/load.LoadTestStatistics' type: array successMessage: type: string type: object - app.AntResponse-array_load_LoadTestStatistics: + app.AntResponse-array_load_MetricsSummary: properties: code: type: integer @@ -21,12 +21,12 @@ definitions: type: string result: items: - $ref: '#/definitions/load.LoadTestStatistics' + $ref: '#/definitions/load.MetricsSummary' type: array successMessage: type: string type: object - app.AntResponse-array_load_MetricsSummary: + app.AntResponse-array_load_ResultSummary: properties: code: type: integer @@ -34,32 +34,52 @@ definitions: type: string result: items: - $ref: '#/definitions/load.MetricsSummary' + $ref: '#/definitions/load.ResultSummary' type: array successMessage: type: string type: object - app.AntResponse-array_load_ResultSummary: + app.AntResponse-cost_EstimateCostInfoResults: properties: code: type: integer errorMessage: type: string result: - items: - $ref: '#/definitions/load.ResultSummary' - type: array + $ref: '#/definitions/cost.EstimateCostInfoResults' successMessage: type: string type: object - app.AntResponse-cost_UpdateCostInfoResult: + app.AntResponse-cost_EstimateCostResults: properties: code: type: integer errorMessage: type: string result: - $ref: '#/definitions/cost.UpdateCostInfoResult' + $ref: '#/definitions/cost.EstimateCostResults' + successMessage: + type: string + type: object + app.AntResponse-cost_GetEstimateForecastCostInfoResults: + properties: + code: + type: integer + errorMessage: + type: string + result: + $ref: '#/definitions/cost.GetEstimateForecastCostInfoResults' + successMessage: + type: string + type: object + app.AntResponse-cost_UpdateEstimateForecastCostInfoResult: + properties: + code: + type: integer + errorMessage: + type: string + result: + $ref: '#/definitions/cost.UpdateEstimateForecastCostInfoResult' successMessage: type: string type: object @@ -258,34 +278,54 @@ definitions: loadTestKey: type: string type: object - app.UpdateCostInfoReq: + app.UpdateAndGetEstimateCostReq: + properties: + specs: + items: + properties: + image: + type: string + instanceType: + type: string + providerName: + type: string + regionName: + type: string + required: + - instanceType + - providerName + - regionName + type: object + type: array + specsWithFormat: + items: + properties: + commonImage: + type: string + commonSpec: + type: string + required: + - commonSpec + type: object + type: array + type: object + app.UpdateEstimateForecastCostRawReq: properties: awsAdditionalInfo: $ref: '#/definitions/app.AwsAdditionalInfoReq' - connectionName: - type: string costResources: items: $ref: '#/definitions/app.CostResourceReq' type: array - migrationId: - type: string required: - - connectionName - costResources type: object - app.UpdatePriceInfosReq: + app.UpdateEstimateForecastCostReq: properties: - instanceType: - type: string - providerName: + mciId: type: string - regionName: + nsId: type: string - required: - - instanceType - - providerName - - regionName type: object constant.ExecutionStatus: enum: @@ -321,6 +361,28 @@ definitions: x-enum-varnames: - Local - Remote + constant.PriceCurrency: + enum: + - USD + - KRW + type: string + x-enum-varnames: + - USD + - KRW + constant.PricePolicy: + enum: + - OnDemand + type: string + x-enum-varnames: + - OnDemand + constant.PriceUnit: + enum: + - PerHour + - PerYear + type: string + x-enum-varnames: + - PerHour + - PerYear constant.ResourceType: enum: - VM @@ -333,7 +395,110 @@ definitions: - VNet - DataDisk - Etc - cost.GetCostInfoResult: + cost.EsimateCostSpecResults: + properties: + estimateForecastCostSpecDetailResults: + items: + $ref: '#/definitions/cost.EstimateCostSpecDetailResult' + type: array + imageName: + type: string + instanceType: + type: string + providerName: + type: string + regionName: + type: string + totalMaxMonthlyPrice: + type: number + totalMinMonthlyPrice: + type: number + type: object + cost.EstimateCostInfoResult: + properties: + calculatedMonthlyPrice: + type: number + currency: + $ref: '#/definitions/constant.PriceCurrency' + id: + type: integer + instanceType: + type: string + lastUpdatedAt: + type: string + memory: + type: string + originalPricePolicy: + type: string + osType: + type: string + price: + type: string + priceDescription: + type: string + pricePolicy: + $ref: '#/definitions/constant.PricePolicy' + productDescription: + type: string + providerName: + type: string + regionName: + type: string + storage: + type: string + unit: + $ref: '#/definitions/constant.PriceUnit' + vCpu: + type: string + type: object + cost.EstimateCostInfoResults: + properties: + estimateCostInfoResult: + items: + $ref: '#/definitions/cost.EstimateCostInfoResult' + type: array + resultCount: + type: integer + type: object + cost.EstimateCostResults: + properties: + esimateCostSpecResults: + items: + $ref: '#/definitions/cost.EsimateCostSpecResults' + type: array + type: object + cost.EstimateCostSpecDetailResult: + properties: + calculatedMonthlyPrice: + type: number + currency: + $ref: '#/definitions/constant.PriceCurrency' + id: + type: integer + lastUpdatedAt: + type: string + memory: + type: string + originalPricePolicy: + type: string + osType: + type: string + price: + type: string + priceDescription: + type: string + pricePolicy: + $ref: '#/definitions/constant.PricePolicy' + productDescription: + type: string + storage: + type: string + unit: + $ref: '#/definitions/constant.PriceUnit' + vCpu: + type: string + type: object + cost.GetEstimateForecastCostInfoResult: properties: category: type: string @@ -350,7 +515,16 @@ definitions: unit: type: string type: object - cost.UpdateCostInfoResult: + cost.GetEstimateForecastCostInfoResults: + properties: + getEstimateForecastCostInfoResults: + items: + $ref: '#/definitions/cost.GetEstimateForecastCostInfoResult' + type: array + resultCount: + type: integer + type: object + cost.UpdateEstimateForecastCostInfoResult: properties: fetchedDataCount: type: integer @@ -660,59 +834,155 @@ info: title: CM-ANT REST API version: 0.2.2 paths: - /api/v1/cost/info: + /api/v1/cost/estimate: + get: + consumes: + - application/json + description: Fetch estimated cost details based on provider, region, instance + type, and resource specifications. Pagination support is provided through + `Page` and `Size` parameters. + operationId: GetEstimateCost + parameters: + - description: Cloud provider name to filter estimated costs + in: query + name: providerName + type: string + - description: Region name to filter estimated costs + in: query + name: regionName + type: string + - description: Instance type to filter estimated costs + in: query + name: instanceType + type: string + - description: Number of vCPUs to filter estimated costs + in: query + name: vCpu + type: string + - description: Memory size to filter estimated costs + in: query + name: memory + type: string + - description: Operating system type to filter estimated costs + in: query + name: osType + type: string + - description: 'Page number for pagination (default: 1)' + in: query + name: page + type: integer + - description: 'Number of records per page (default: 100, max: 100)' + in: query + name: size + type: integer + produces: + - application/json + responses: + "200": + description: Successfully retrieved estimated cost information + schema: + $ref: '#/definitions/app.AntResponse-cost_EstimateCostInfoResults' + "400": + description: Invalid request parameters + schema: + $ref: '#/definitions/app.AntResponse-string' + "500": + description: Failed to retrieve estimated cost information + schema: + $ref: '#/definitions/app.AntResponse-string' + summary: Retrieve Estimated Cost Information + tags: + - '[Cost Estimate]' + post: + consumes: + - application/json + description: Update the estimate cost based on provided specifications and retrieve + the updated cost estimation. Required fields for each specification include + `ProviderName`, `RegionName`, and `InstanceType`. Specifications can also + be provided in a formatted string using `+` delimiter. + operationId: UpdateAndGetEstimateCost + parameters: + - description: Request body for updating and retrieving estimated cost information + in: body + name: body + required: true + schema: + $ref: '#/definitions/app.UpdateAndGetEstimateCostReq' + produces: + - application/json + responses: + "200": + description: Successfully updated and retrieved estimated cost information + schema: + $ref: '#/definitions/app.AntResponse-cost_EstimateCostResults' + "400": + description: Invalid request parameters or format + schema: + $ref: '#/definitions/app.AntResponse-string' + "500": + description: Failed to update or retrieve estimated cost information + schema: + $ref: '#/definitions/app.AntResponse-string' + summary: Update and Retrieve Estimated Cost Information + tags: + - '[Cost Estimate]' + /api/v1/cost/estimate/forecast: get: consumes: - application/json - description: Retrieve cost information for specified parameters within a defined - date range. The date range must be within a 6-month period. Optionally, you - can specify cost aggregation type and date order for the results. - operationId: GetCostInfo + description: Fetch estimated forecast cost data based on specified parameters, + including a date range that must be within 6 months. Supports pagination and + filtering by namespace IDs, migration configuration IDs, and resource types. + operationId: GetEstimateForecastCost parameters: - - description: Start date for the cost information retrieval in 'YYYY-MM-DD' - format + - description: Start date for the forecast cost retrieval in 'YYYY-MM-DD' format in: query name: startDate required: true type: string - - description: End date for the cost information retrieval in 'YYYY-MM-DD' format + - description: End date for the forecast cost retrieval in 'YYYY-MM-DD' format in: query name: endDate required: true type: string - collectionFormat: csv - description: List of migration IDs to filter the cost information + description: List of namespace IDs to filter forecast cost information in: query items: type: string - name: migrationIds + name: nsIds type: array - collectionFormat: csv - description: List of cloud providers to filter the cost information + description: List of migration configuration IDs to filter forecast cost information in: query items: type: string - name: provider + name: mciIds type: array - collectionFormat: csv - description: List of resource types to filter the cost information + description: List of cloud providers to filter forecast cost information + in: query + items: + type: string + name: providers + type: array + - collectionFormat: csv + description: List of resource types to filter forecast cost information in: query items: type: string name: resourceTypes type: array - collectionFormat: csv - description: List of resource IDs to filter the cost information + description: List of resource IDs to filter forecast cost information in: query items: type: string name: resourceIds type: array - - description: Type of cost aggregation for the results (e.g., 'daily', 'weekly', - 'monthly') + - description: Type of cost aggregation (e.g., 'daily', 'weekly', 'monthly') in: query name: costAggregationType - required: true type: string - description: Order of dates in the result (e.g., 'asc', 'desc') in: query @@ -722,57 +992,98 @@ paths: in: query name: resourceTypeOrder type: string + - description: 'Page number for pagination (default: 1)' + in: query + name: page + type: integer + - description: 'Number of records per page (default: 10000, max: 10000)' + in: query + name: size + type: integer produces: - application/json responses: "200": - description: Successfully retrieved cost information + description: Successfully retrieved estimated forecast cost information schema: - $ref: '#/definitions/app.AntResponse-array_cost_GetCostInfoResult' + $ref: '#/definitions/app.AntResponse-cost_GetEstimateForecastCostInfoResults' "400": - description: Invalid request parameters + description: Invalid request parameters or date format errors schema: $ref: '#/definitions/app.AntResponse-string' "500": - description: Failed to retrieve cost information + description: Failed to retrieve estimated forecast cost information schema: $ref: '#/definitions/app.AntResponse-string' - summary: Get Cost Information + summary: Retrieve Estimated Forecast Cost Information tags: - - '[Cost Management]' + - '[Cost Estimate]' post: consumes: - application/json - description: Update cost information for specified resources, including details - such as migration ID, cost resources, and additional AWS info if applicable. - The request body must include a valid migration ID and a list of cost resources. - If AWS-specific details are provided, ensure all required fields are populated. - operationId: UpdateCostInfo + description: Update and retrieve forecasted cost estimates for a specified namespace + and migration configuration ID over the past 14 days. + operationId: UpdateEstimateForecastCost parameters: - - description: Request body containing cost update information + - description: Request body containing NsId (Namespace ID) and MciId (Migration + Configuration ID) for cost estimation forecast in: body name: body required: true schema: - $ref: '#/definitions/app.UpdateCostInfoReq' + $ref: '#/definitions/app.UpdateEstimateForecastCostReq' produces: - application/json responses: "200": - description: Successfully updated cost information + description: Successfully updated and retrieved estimated forecast cost + information schema: - $ref: '#/definitions/app.AntResponse-cost_UpdateCostInfoResult' + $ref: '#/definitions/app.AntResponse-cost_UpdateEstimateForecastCostInfoResult' "400": - description: Invalid request parameters + description: Request body binding error + schema: + $ref: '#/definitions/app.AntResponse-string' + "500": + description: Failed to update or retrieve forecast cost information + schema: + $ref: '#/definitions/app.AntResponse-string' + summary: Update and Retrieve Estimated Forecast Cost + tags: + - '[Cost Estimate]' + /api/v1/cost/estimate/forecast/raw: + post: + consumes: + - application/json + description: Update and retrieve raw forecasted cost estimates for specified + cost resources and additional AWS information over the past 14 days. + operationId: UpdateEstimateForecastCostRaw + parameters: + - description: Request body containing details for cost estimation forecast + in: body + name: body + required: true + schema: + $ref: '#/definitions/app.UpdateEstimateForecastCostRawReq' + produces: + - application/json + responses: + "200": + description: Successfully updated and retrieved raw estimated forecast cost + information in raw data + schema: + $ref: '#/definitions/app.AntResponse-cost_UpdateEstimateForecastCostInfoResult' + "400": + description: Migrated resource id list is required schema: $ref: '#/definitions/app.AntResponse-string' "500": - description: Failed to update cost information + description: Error updating or retrieving forecast cost information schema: $ref: '#/definitions/app.AntResponse-string' - summary: Update Cost Information + summary: Update and Retrieve Raw Estimated Forecast Cost tags: - - '[Cost Management]' + - '[Cost Estimate]' /api/v1/load/generators: get: consumes: @@ -1250,91 +1561,6 @@ paths: summary: Stop Load Test tags: - '[Load Test Execution Management]' - /api/v1/price/info: - get: - consumes: - - application/json - description: Retrieve pricing information for cloud resources based on specified - query parameters. Returns price data based on provider, region, instance type, - vCPU, memory, and OS type. It offer instances with the lowest monthly prices - what in the database. - operationId: GetPriceInfos - parameters: - - description: Cloud provider name - aws|alibaba|tencent|gcp|azure|ibm - in: query - name: providerName - required: true - type: string - - description: Region name - in: query - name: regionName - required: true - type: string - - description: Instance type - in: query - name: instanceType - type: string - - description: Number of vCPUs - in: query - name: vCpu - type: string - - description: Amount of memory - in: query - name: memory - type: string - - description: Operating system type - in: query - name: osType - type: string - produces: - - application/json - responses: - "200": - description: Successfully retrieved pricing information - schema: - $ref: '#/definitions/app.AntResponse-string' - "400": - description: Invalid request parameters - schema: - $ref: '#/definitions/app.AntResponse-string' - "500": - description: Failed to retrieve pricing information - schema: - $ref: '#/definitions/app.AntResponse-string' - summary: Get Price Information - tags: - - Price Management - post: - consumes: - - application/json - description: Retrieve pricing information for cloud resources based on specified - parameters. If saved data is more than 7 days, fetch new data and insert new - price data even if same price as before. - operationId: UpdatePriceInfos - parameters: - - description: Request body containing get price information - in: body - name: body - required: true - schema: - $ref: '#/definitions/app.UpdatePriceInfosReq' - produces: - - application/json - responses: - "200": - description: Successfully retrieved pricing information - schema: - $ref: '#/definitions/app.AntResponse-string' - "400": - description: Invalid request parameters - schema: - $ref: '#/definitions/app.AntResponse-string' - "500": - description: Failed to retrieve pricing information - schema: - $ref: '#/definitions/app.AntResponse-string' - tags: - - '[Price Management]' /readyz: get: consumes: diff --git a/cmd/cm-ant/main.go b/cmd/cm-ant/main.go index b6b4fd2..9814d3b 100644 --- a/cmd/cm-ant/main.go +++ b/cmd/cm-ant/main.go @@ -1,14 +1,20 @@ package main import ( - "log" + "fmt" + "os" "os/signal" + "runtime" + "strings" "syscall" + "time" "github.com/cloud-barista/cm-ant/internal/app" "github.com/cloud-barista/cm-ant/internal/config" "github.com/cloud-barista/cm-ant/internal/utils" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" ) // InitRouter initializes the routing for CM-ANT API server. @@ -17,55 +23,99 @@ import ( // @version 0.2.2 // @description CM-ANT REST API swagger document. // @basePath /ant + +type CallerHook struct{} + +func (h CallerHook) Run(e *zerolog.Event, level zerolog.Level, msg string) { + if pc, file, line, ok := runtime.Caller(3); ok { + shortFile := file[strings.LastIndex(file, "/")+1:] + e.Str("file", fmt.Sprintf("%s:%d", shortFile, line)) + funcName := strings.Replace(runtime.FuncForPC(pc).Name(), "github.com/cloud-barista/", "", 1) + e.Str("func", funcName) + } +} + func main() { + output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339} + output.FormatLevel = func(i interface{}) string { + level := strings.ToUpper(fmt.Sprintf("%s", i)) + switch level { + case "DEBUG": + return fmt.Sprintf("\033[36m| %-6s|\033[0m", level) // Cyan + case "INFO": + return fmt.Sprintf("\033[32m| %-6s|\033[0m", level) // Green + case "WARN": + return fmt.Sprintf("\033[33m| %-6s|\033[0m", level) // Yellow + case "ERROR": + return fmt.Sprintf("\033[31m| %-6s|\033[0m", level) // Red + case "FATAL": + return fmt.Sprintf("\033[35m| %-6s|\033[0m", level) // Magenta + default: + return fmt.Sprintf("| %-6s|", level) // Default color + } + } + output.FormatMessage = func(i interface{}) string { + if i == nil { + return "" + } + return fmt.Sprintf("message: \033[1m%s\033[0m", i) + } + + output.FormatFieldName = func(i interface{}) string { + return fmt.Sprintf("%s:", i) + } + output.FormatFieldValue = func(i interface{}) string { + return fmt.Sprintf("\033[1m%s\033[0m", i) + } + + log.Logger = zerolog.New(output).With().Timestamp().Logger().Hook(CallerHook{}) err := utils.Script(utils.JoinRootPathWith("/script/install_required_utils.sh"), []string{}) if err != nil { - log.Fatal("required tool can not install") + log.Fatal().Msg("required tool can not install") } - utils.LogInfo("Starting CM-Ant server initialization...") // Initialize the configuration for CM-Ant server err = config.InitConfig() if err != nil { - log.Fatalf("[ERROR] CM-Ant server config error: %v", err) + log.Fatal().Msgf("CM-Ant server config error: %v", err) } // Create a new instance of the CM-Ant server s, err := app.NewAntServer() if err != nil { - log.Fatalf("[ERROR] CM-Ant server creation error: %v", err) + log.Fatal().Msgf("CM-Ant server creation error: %v", err) } // Initialize the router for the CM-Ant server err = s.InitRouter() if err != nil { - log.Fatalf("[ERROR] CM-Ant server init router error: %v", err) + log.Fatal().Msgf("CM-Ant server init router error: %v", err) } - utils.LogInfo("CM-Ant server initialization completed successfully.") + log.Info().Msgf("CM-Ant server initialization completed successfully.") // Create a channel to listen for OS signals stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) - utils.LogInfo("Starting the CM-Ant server...") + log.Info().Msgf("Starting the CM-Ant server...") go func() { if err := s.Start(); err != nil { - log.Fatalf("[ERROR] CM-Ant start server error: %v", err) + log.Fatal().Msgf("CM-Ant start server error: %v", err) } }() - utils.LogInfo("CM-Ant server started successfully. Waiting for termination signal...") + log.Info().Msgf("CM-Ant server started successfully. Waiting for termination signal...") // Wait for termination signal <-stop - utils.LogInfo("Shutting down CM-Ant server...") + log.Info().Msgf("Shutting down CM-Ant server...") // Perform any necessary cleanup actions here, such as closing connections or saving state. // Optionally wait for pending operations to complete gracefully. - utils.LogInfo("CM-Ant server stopped gracefully.") + log.Info().Msgf("CM-Ant server stopped gracefully.") os.Exit(0) } diff --git a/config.yaml b/config.yaml index 8585a21..0285095 100644 --- a/config.yaml +++ b/config.yaml @@ -1,6 +1,3 @@ -root: - path: - server: port: 8880 @@ -16,13 +13,17 @@ tumblebug: username: default password: default +cost: + estimation: + updateInterval: "168h" + load: retry: 2 jmeter: dir: "/opt/ant/jmeter" version: 5.6 -logging: +log: level: info database: diff --git a/docker-compose.yaml b/docker-compose.yaml index 0f13a7f..beae8cb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -32,6 +32,7 @@ services: restart: unless-stopped ant-postgres: + container_name: ant-postgres image: timescale/timescaledb:latest-pg16 ports: - "5432:5432" @@ -50,7 +51,7 @@ services: restart: unless-stopped cb-tumblebug: - image: cloudbaristaorg/cb-tumblebug:0.9.13 + image: cloudbaristaorg/cb-tumblebug:0.9.21 container_name: cb-tumblebug platform: linux/amd64 ports: @@ -147,7 +148,7 @@ services: restart: unless-stopped cb-spider: - image: cloudbaristaorg/cb-spider:0.9.4 + image: cloudbaristaorg/cb-spider:0.9.8 container_name: cb-spider platform: linux/amd64 networks: diff --git a/internal/app/cost_estimation_handler.go b/internal/app/cost_estimation_handler.go deleted file mode 100644 index 75f4788..0000000 --- a/internal/app/cost_estimation_handler.go +++ /dev/null @@ -1,233 +0,0 @@ -package app - -import ( - "fmt" - "net/http" - "strings" - "time" - - "github.com/cloud-barista/cm-ant/internal/core/common/constant" - "github.com/cloud-barista/cm-ant/internal/core/cost" - "github.com/labstack/echo/v4" -) - -// @Id UpdatePriceInfos -// @Summar Update Price Information -// @Description Retrieve pricing information for cloud resources based on specified parameters. If saved data is more than 7 days, fetch new data and insert new price data even if same price as before. -// @Tags [Price Management] -// @Accept json -// @Produce json -// @Param body body app.UpdatePriceInfosReq true "Request body containing get price information" -// @Success 200 {object} app.AntResponse[string] "Successfully retrieved pricing information" -// @Failure 400 {object} app.AntResponse[string] "Invalid request parameters" -// @Failure 500 {object} app.AntResponse[string] "Failed to retrieve pricing information" -// @Router /api/v1/price/info [post] -func (server *AntServer) updatePriceInfos(c echo.Context) error { - var req UpdatePriceInfosReq - if err := c.Bind(&req); err != nil { - return errorResponseJson(http.StatusBadRequest, err.Error()) - } - - if strings.TrimSpace(req.RegionName) == "" || - // strings.TrimSpace(req.InstanceType) == "" || - strings.TrimSpace(req.ProviderName) == "" { - return errorResponseJson(http.StatusBadRequest, "provier name, region name, instance type must be set") - } - - arg := cost.UpdatePriceInfosParam{ - ProviderName: strings.TrimSpace(req.ProviderName), - RegionName: strings.TrimSpace(req.RegionName), - InstanceType: strings.TrimSpace(req.InstanceType), - } - - err := server.services.costService.UpdatePriceInfos(arg) - - if err != nil { - return errorResponseJson(http.StatusInternalServerError, err.Error()) - } - - return successResponseJson( - c, - "retrieved pricing information", - fmt.Sprintf("%s csp price information updated. region: %s, instance type: %s", arg.ProviderName, arg.RegionName, arg.InstanceType), - ) -} - -// @Id GetPriceInfos -// @Summary Get Price Information -// @Description Retrieve pricing information for cloud resources based on specified query parameters. Returns price data based on provider, region, instance type, vCPU, memory, and OS type. It offer instances with the lowest monthly prices what in the database. -// @Tags Price Management -// @Accept json -// @Produce json -// @Param providerName query string true "Cloud provider name - aws|alibaba|tencent|gcp|azure|ibm" -// @Param regionName query string true "Region name" -// @Param instanceType query string false "Instance type" -// @Param vCpu query string false "Number of vCPUs" -// @Param memory query string false "Amount of memory" -// @Param osType query string false "Operating system type" -// @Success 200 {object} AntResponse[string] "Successfully retrieved pricing information" -// @Failure 400 {object} AntResponse[string] "Invalid request parameters" -// @Failure 500 {object} AntResponse[string] "Failed to retrieve pricing information" -// @Router /api/v1/price/info [get] -func (server *AntServer) getPriceInfos(c echo.Context) error { - var req GetPriceInfosReq - if err := c.Bind(&req); err != nil { - return errorResponseJson(http.StatusBadRequest, err.Error()) - } - - arg := cost.GetPriceInfosParam{ - ProviderName: strings.TrimSpace(req.ProviderName), - RegionName: strings.TrimSpace(req.RegionName), - InstanceType: strings.TrimSpace(req.InstanceType), - VCpu: strings.TrimSpace(req.VCpu), - Memory: strings.TrimSpace(req.Memory), - OsType: strings.TrimSpace(req.OsType), - } - - r, err := server.services.costService.GetPriceInfos(arg) - - if err != nil { - return errorResponseJson(http.StatusInternalServerError, err.Error()) - } - - return successResponseJson( - c, - "Successfully get price info.", - r, - ) -} - -// @Id UpdateCostInfo -// @Summary Update Cost Information -// @Description Update cost information for specified resources, including details such as migration ID, cost resources, and additional AWS info if applicable. The request body must include a valid migration ID and a list of cost resources. If AWS-specific details are provided, ensure all required fields are populated. -// @Tags [Cost Management] -// @Accept json -// @Produce json -// @Param body body app.UpdateCostInfoReq true "Request body containing cost update information" -// @Success 200 {object} app.AntResponse[cost.UpdateCostInfoResult] "Successfully updated cost information" -// @Failure 400 {object} app.AntResponse[string] "Invalid request parameters" -// @Failure 500 {object} app.AntResponse[string] "Failed to update cost information" -// @Router /api/v1/cost/info [post] -func (server *AntServer) updateCostInfos(c echo.Context) error { - var req UpdateCostInfoReq - - if err := c.Bind(&req); err != nil { - return errorResponseJson(http.StatusBadRequest, "request body binding error") - } - - if strings.TrimSpace(req.MigrationId) == "" { - return errorResponseJson(http.StatusBadRequest, "migration id is required") - } - - if len(req.CostResources) == 0 { - return errorResponseJson(http.StatusBadRequest, "Migrated resource id list are required") - } - - costResources := make([]cost.CostResourceParam, 0) - - for _, v := range req.CostResources { - costResources = append(costResources, cost.CostResourceParam{ - ResourceType: v.ResourceType, - ResourceIds: v.ResourceIds, - }) - } - - endDate := time.Now().Truncate(24*time.Hour).AddDate(0, 0, 1) - startDate := endDate.AddDate(0, 0, -14) - param := cost.UpdateCostInfoParam{ - MigrationId: req.MigrationId, - Provider: "aws", - StartDate: startDate, - EndDate: endDate, - CostResources: costResources, - ConnectionName: req.ConnectionName, - AwsAdditionalInfo: cost.AwsAdditionalInfoParam{ - OwnerId: req.AwsAdditionalInfo.OwnerId, - Regions: req.AwsAdditionalInfo.Regions, - }, - } - - r, err := server.services.costService.UpdateCostInfo(param) - - if err != nil { - return errorResponseJson(http.StatusInternalServerError, err.Error()) - } - - return successResponseJson( - c, - "Successfully updated cost info.", - r, - ) -} - -// @Id GetCostInfo -// @Summary Get Cost Information -// @Description Retrieve cost information for specified parameters within a defined date range. The date range must be within a 6-month period. Optionally, you can specify cost aggregation type and date order for the results. -// @Tags [Cost Management] -// @Accept json -// @Produce json -// @Param startDate query string true "Start date for the cost information retrieval in 'YYYY-MM-DD' format" -// @Param endDate query string true "End date for the cost information retrieval in 'YYYY-MM-DD' format" -// @Param migrationIds query []string false "List of migration IDs to filter the cost information" -// @Param provider query []string false "List of cloud providers to filter the cost information" -// @Param resourceTypes query []string false "List of resource types to filter the cost information" -// @Param resourceIds query []string false "List of resource IDs to filter the cost information" -// @Param costAggregationType query string true "Type of cost aggregation for the results (e.g., 'daily', 'weekly', 'monthly')" -// @Param dateOrder query string false "Order of dates in the result (e.g., 'asc', 'desc')" -// @Param resourceTypeOrder query string false "Order of resource types in the result (e.g., 'asc', 'desc')" -// @Success 200 {object} app.AntResponse[[]cost.GetCostInfoResult] "Successfully retrieved cost information" -// @Failure 400 {object} app.AntResponse[string] "Invalid request parameters" -// @Failure 500 {object} app.AntResponse[string] "Failed to retrieve cost information" -// @Router /api/v1/cost/info [get] -func (s *AntServer) getCostInfos(c echo.Context) error { - var req GetCostInfoReq - if err := c.Bind(&req); err != nil { - return errorResponseJson(http.StatusBadRequest, "Invalid request parameters") - } - - startDate, err := time.Parse("2006-01-02", req.StartDate) - - if err != nil { - return errorResponseJson(http.StatusBadRequest, "start date format is incorrect") - } - - endDate, err := time.Parse("2006-01-02", req.EndDate) - - if err != nil { - return errorResponseJson(http.StatusBadRequest, "end date format is incorrect") - } - - sixMonthsLater := startDate.AddDate(0, 6, 0) - - if endDate.After(sixMonthsLater) { - return errorResponseJson(http.StatusBadRequest, "date range must in 6 month") - } - - if req.CostAggregationType == "" { - req.CostAggregationType = constant.Daily - } - - if req.DateOrder == "" { - req.DateOrder = constant.Asc - } - - arg := cost.GetCostInfoParam{ - StartDate: startDate, - EndDate: endDate, - MigrationIds: req.MigrationIds, - Providers: req.Providers, - ResourceTypes: req.ResourceTypes, - ResourceIds: req.ResourceIds, - CostAggregationType: req.CostAggregationType, - DateOrder: req.DateOrder, - ResourceTypeOrder: req.ResourceTypeOrder, - } - - result, err := s.services.costService.GetCostInfos(arg) - - if err != nil { - return errorResponseJson(http.StatusInternalServerError, "Failed to retrieve load test result") - } - - return successResponseJson(c, "Successfully retrieved load test result", result) -} diff --git a/internal/app/estimate_cost_handler.go b/internal/app/estimate_cost_handler.go new file mode 100644 index 0000000..0b5846e --- /dev/null +++ b/internal/app/estimate_cost_handler.go @@ -0,0 +1,369 @@ +package app + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/cloud-barista/cm-ant/internal/config" + "github.com/cloud-barista/cm-ant/internal/core/common/constant" + "github.com/cloud-barista/cm-ant/internal/core/cost" + "github.com/cloud-barista/cm-ant/internal/utils" + "github.com/labstack/echo/v4" +) + +// @Id UpdateAndGetEstimateCost +// @Summary Update and Retrieve Estimated Cost Information +// @Description Update the estimate cost based on provided specifications and retrieve the updated cost estimation. Required fields for each specification include `ProviderName`, `RegionName`, and `InstanceType`. Specifications can also be provided in a formatted string using `+` delimiter. +// @Tags [Cost Estimate] +// @Accept json +// @Produce json +// @Param body body UpdateAndGetEstimateCostReq true "Request body for updating and retrieving estimated cost information" +// @Success 200 {object} app.AntResponse[cost.EstimateCostResults] "Successfully updated and retrieved estimated cost information" +// @Failure 400 {object} app.AntResponse[string] "Invalid request parameters or format" +// @Failure 500 {object} app.AntResponse[string] "Failed to update or retrieve estimated cost information" +// @Router /api/v1/cost/estimate [post] +func (a *AntServer) updateAndGetEstimateCost(c echo.Context) error { + var req UpdateAndGetEstimateCostReq + if err := c.Bind(&req); err != nil { + return errorResponseJson(http.StatusBadRequest, err.Error()) + } + + if len(req.Specs) == 0 && len(req.SpecsWithFormat) == 0 { + return errorResponseJson(http.StatusBadRequest, "request is invalid. check the required request body properties") + } + + pastTime := time.Now().Add(-config.AppConfig.Cost.Estimation.UpdateInterval) + + recommendSpecs := make([]cost.RecommendSpecParam, 0) + + if len(req.Specs) > 0 { + for _, v := range req.Specs { + pn := strings.TrimSpace(strings.ToLower(v.ProviderName)) + rn := strings.TrimSpace(v.RegionName) + it := strings.TrimSpace(v.InstanceType) + + if pn == "" || rn == "" || it == "" { + return errorResponseJson(http.StatusBadRequest, "request is invalid. check the required request body properties") + } + + param := cost.RecommendSpecParam{ + ProviderName: pn, + RegionName: rn, + InstanceType: it, + Image: strings.TrimSpace(v.Image), + } + recommendSpecs = append(recommendSpecs, param) + } + } + + if len(req.SpecsWithFormat) > 0 { + delim := "+" + + for _, v := range req.SpecsWithFormat { + + cs := strings.TrimSpace(v.CommonSpec) + ci := strings.TrimSpace(v.CommonImage) + + if cs == "" { + return errorResponseJson(http.StatusBadRequest, "request is invalid. check the required request body properties") + } + + splitedCommonSpec := strings.Split(cs, delim) + splitedCommonImage := strings.Split(ci, delim) + + if len(splitedCommonSpec) != 3 { + utils.LogErrorf("common spec format is not correct; image: %s; spec: %s", ci, cs) + return errorResponseJson(http.StatusBadRequest, fmt.Sprintf("common spec format is not correct; image: %s; spec: %s", ci, cs)) + } + + if len(splitedCommonImage) == 3 && (splitedCommonImage[0] != splitedCommonSpec[0] || splitedCommonImage[1] != splitedCommonSpec[1]) { + utils.LogErrorf("common image and spec recommendation is wrong; image: %s; spec: %s", ci, cs) + return errorResponseJson(http.StatusBadRequest, fmt.Sprintf("common image and spec recommendation is wrong; image: %s; spec: %s", ci, cs)) + } + + pn := strings.TrimSpace(strings.ToLower(splitedCommonSpec[0])) + rn := strings.TrimSpace(splitedCommonSpec[1]) + it := strings.TrimSpace(splitedCommonSpec[2]) + + if pn == "" || rn == "" || it == "" { + return errorResponseJson(http.StatusBadRequest, "request is invalid. check the required request body properties") + } + + param := cost.RecommendSpecParam{ + ProviderName: pn, + RegionName: rn, + InstanceType: it, + } + + if len(splitedCommonImage) == 3 { + param.Image = strings.TrimSpace(splitedCommonImage[2]) + } + + recommendSpecs = append(recommendSpecs, param) + } + } + + arg := cost.UpdateAndGetEstimateCostParam{ + RecommendSpecs: recommendSpecs, + TimeStandard: time.Date(pastTime.Year(), pastTime.Month(), pastTime.Day(), 0, 0, 0, 0, pastTime.Location()), + PricePolicy: constant.OnDemand, + } + + res, err := a.services.costService.UpdateAndGetEstimateCost(arg) + + if err != nil { + return errorResponseJson(http.StatusInternalServerError, err.Error()) + } + + return successResponseJson( + c, + "Successfully update and get estimate cost info", + res, + ) +} + +// @Id UpdateEstimateForecastCost +// @Summary Update and Retrieve Estimated Forecast Cost +// @Description Update and retrieve forecasted cost estimates for a specified namespace and migration configuration ID over the past 14 days. +// @Tags [Cost Estimate] +// @Accept json +// @Produce json +// @Param body body UpdateEstimateForecastCostReq true "Request body containing NsId (Namespace ID) and MciId (Migration Configuration ID) for cost estimation forecast" +// @Success 200 {object} app.AntResponse[cost.UpdateEstimateForecastCostInfoResult] "Successfully updated and retrieved estimated forecast cost information" +// @Failure 400 {object} app.AntResponse[string] "Request body binding error" +// @Failure 500 {object} app.AntResponse[string] "Failed to update or retrieve forecast cost information" +// @Router /api/v1/cost/estimate/forecast [post] +func (a *AntServer) updateEstimateForecastCost(c echo.Context) error { + var req UpdateEstimateForecastCostReq + + if err := c.Bind(&req); err != nil { + return errorResponseJson(http.StatusBadRequest, "request body binding error") + } + + endDate := time.Now().Truncate(24*time.Hour).AddDate(0, 0, 1) + startDate := endDate.AddDate(0, 0, -14) + + param := cost.UpdateEstimateForecastCostParam{ + NsId: req.NsId, + MciId: req.MciId, + StartDate: startDate, + EndDate: endDate, + } + + r, err := a.services.costService.UpdateEstimateForecastCost(param) + + if err != nil { + return errorResponseJson(http.StatusInternalServerError, err.Error()) + } + + return successResponseJson( + c, + "Successfully update estimate forecast cost info.", + r, + ) +} + +// @Id GetEstimateCost +// @Summary Retrieve Estimated Cost Information +// @Description Fetch estimated cost details based on provider, region, instance type, and resource specifications. Pagination support is provided through `Page` and `Size` parameters. +// @Tags [Cost Estimate] +// @Accept json +// @Produce json +// @Param providerName query string false "Cloud provider name to filter estimated costs" +// @Param regionName query string false "Region name to filter estimated costs" +// @Param instanceType query string false "Instance type to filter estimated costs" +// @Param vCpu query string false "Number of vCPUs to filter estimated costs" +// @Param memory query string false "Memory size to filter estimated costs" +// @Param osType query string false "Operating system type to filter estimated costs" +// @Param page query int false "Page number for pagination (default: 1)" +// @Param size query int false "Number of records per page (default: 100, max: 100)" +// @Success 200 {object} app.AntResponse[cost.EstimateCostInfoResults] "Successfully retrieved estimated cost information" +// @Failure 400 {object} app.AntResponse[string] "Invalid request parameters" +// @Failure 500 {object} app.AntResponse[string] "Failed to retrieve estimated cost information" +// @Router /api/v1/cost/estimate [get] +func (server *AntServer) getEstimateCost(c echo.Context) error { + var req GetEstimateCostInfosReq + if err := c.Bind(&req); err != nil { + return errorResponseJson(http.StatusBadRequest, err.Error()) + } + + if req.Page < 1 { + req.Page = 1 + } + + if req.Size < 1 || req.Size > 100 { + req.Size = 100 + } + + arg := cost.GetEstimateCostParam{ + ProviderName: strings.TrimSpace(req.ProviderName), + RegionName: strings.TrimSpace(req.RegionName), + InstanceType: strings.TrimSpace(req.InstanceType), + VCpu: strings.TrimSpace(req.VCpu), + Memory: strings.TrimSpace(req.Memory), + OsType: strings.TrimSpace(req.OsType), + Page: req.Page, + Size: req.Size, + } + + r, err := server.services.costService.GetEstimateCost(arg) + + if err != nil { + return errorResponseJson(http.StatusInternalServerError, err.Error()) + } + + return successResponseJson( + c, + "Successfully get price info.", + r, + ) +} + +// @Id GetEstimateForecastCost +// @Summary Retrieve Estimated Forecast Cost Information +// @Description Fetch estimated forecast cost data based on specified parameters, including a date range that must be within 6 months. Supports pagination and filtering by namespace IDs, migration configuration IDs, and resource types. +// @Tags [Cost Estimate] +// @Accept json +// @Produce json +// @Param startDate query string true "Start date for the forecast cost retrieval in 'YYYY-MM-DD' format" +// @Param endDate query string true "End date for the forecast cost retrieval in 'YYYY-MM-DD' format" +// @Param nsIds query []string false "List of namespace IDs to filter forecast cost information" +// @Param mciIds query []string false "List of migration configuration IDs to filter forecast cost information" +// @Param providers query []string false "List of cloud providers to filter forecast cost information" +// @Param resourceTypes query []string false "List of resource types to filter forecast cost information" +// @Param resourceIds query []string false "List of resource IDs to filter forecast cost information" +// @Param costAggregationType query string false "Type of cost aggregation (e.g., 'daily', 'weekly', 'monthly')" +// @Param dateOrder query string false "Order of dates in the result (e.g., 'asc', 'desc')" +// @Param resourceTypeOrder query string false "Order of resource types in the result (e.g., 'asc', 'desc')" +// @Param page query int false "Page number for pagination (default: 1)" +// @Param size query int false "Number of records per page (default: 10000, max: 10000)" +// @Success 200 {object} app.AntResponse[cost.GetEstimateForecastCostInfoResults] "Successfully retrieved estimated forecast cost information" +// @Failure 400 {object} app.AntResponse[string] "Invalid request parameters or date format errors" +// @Failure 500 {object} app.AntResponse[string] "Failed to retrieve estimated forecast cost information" +// @Router /api/v1/cost/estimate/forecast [get] +func (s *AntServer) getEstimateForecastCost(c echo.Context) error { + var req GetEstimateForecastCostReq + if err := c.Bind(&req); err != nil { + return errorResponseJson(http.StatusBadRequest, "Invalid request parameters") + } + + startDate, err := time.Parse("2006-01-02", req.StartDate) + + if err != nil { + return errorResponseJson(http.StatusBadRequest, "start date format is incorrect") + } + + endDate, err := time.Parse("2006-01-02", req.EndDate) + + if err != nil { + return errorResponseJson(http.StatusBadRequest, "end date format is incorrect") + } + + if endDate.Before(startDate) { + return errorResponseJson(http.StatusBadRequest, "end date must be after than start date") + } + + sixMonthsLater := startDate.AddDate(0, 6, 0) + + if endDate.After(sixMonthsLater) { + return errorResponseJson(http.StatusBadRequest, "date range must in 6 month") + } + + if req.CostAggregationType == "" { + req.CostAggregationType = constant.Daily + } + + if req.DateOrder == "" { + req.DateOrder = constant.Asc + } + + if req.Page < 1 { + req.Page = 1 + } + + if req.Size < 1 || req.Size > 10000 { + req.Size = 10000 + } + + arg := cost.GetEstimateForecastCostParam{ + Page: req.Page, + Size: req.Size, + StartDate: startDate, + EndDate: endDate, + NsIds: req.NsIds, + MciIds: req.MciIds, + Providers: req.Providers, + ResourceTypes: req.ResourceTypes, + ResourceIds: req.ResourceIds, + CostAggregationType: req.CostAggregationType, + DateOrder: req.DateOrder, + ResourceTypeOrder: req.ResourceTypeOrder, + } + + result, err := s.services.costService.GetEstimateForecastCostInfos(arg) + + if err != nil { + return errorResponseJson(http.StatusInternalServerError, "Failed to get estimate forecast cost") + } + + return successResponseJson(c, "Successfully get estimate forecast cost", result) +} + +// @Id UpdateEstimateForecastCostRaw +// @Summary Update and Retrieve Raw Estimated Forecast Cost +// @Description Update and retrieve raw forecasted cost estimates for specified cost resources and additional AWS information over the past 14 days. +// @Tags [Cost Estimate] +// @Accept json +// @Produce json +// @Param body body UpdateEstimateForecastCostRawReq true "Request body containing details for cost estimation forecast" +// @Success 200 {object} app.AntResponse[cost.UpdateEstimateForecastCostInfoResult] "Successfully updated and retrieved raw estimated forecast cost information in raw data" +// @Failure 400 {object} app.AntResponse[string] "Migrated resource id list is required" +// @Failure 500 {object} app.AntResponse[string] "Error updating or retrieving forecast cost information" +// @Router /api/v1/cost/estimate/forecast/raw [post] +func (server *AntServer) updateEstimateForecastCostRaw(c echo.Context) error { + var req UpdateEstimateForecastCostRawReq + + if err := c.Bind(&req); err != nil { + return errorResponseJson(http.StatusBadRequest, "request body binding error") + } + + if len(req.CostResources) == 0 { + return errorResponseJson(http.StatusBadRequest, "Migrated resource id list are required") + } + + costResources := make([]cost.CostResourceParam, 0) + + for _, v := range req.CostResources { + costResources = append(costResources, cost.CostResourceParam{ + ResourceType: v.ResourceType, + ResourceIds: v.ResourceIds, + }) + } + + endDate := time.Now().Truncate(24*time.Hour).AddDate(0, 0, 1) + startDate := endDate.AddDate(0, 0, -14) + param := cost.UpdateEstimateForecastCostRawParam{ + Provider: "aws", + StartDate: startDate, + EndDate: endDate, + CostResources: costResources, + AwsAdditionalInfo: cost.AwsAdditionalInfoParam{ + OwnerId: req.AwsAdditionalInfo.OwnerId, + Regions: req.AwsAdditionalInfo.Regions, + }, + } + + r, err := server.services.costService.UpdateEstimateForecastCostRaw(param) + + if err != nil { + return errorResponseJson(http.StatusInternalServerError, err.Error()) + } + + return successResponseJson( + c, + "Successfully updated cost info.", + r, + ) +} diff --git a/internal/app/cost_estimation_req.go b/internal/app/estimate_cost_req.go similarity index 55% rename from internal/app/cost_estimation_req.go rename to internal/app/estimate_cost_req.go index f857555..b8186f6 100644 --- a/internal/app/cost_estimation_req.go +++ b/internal/app/estimate_cost_req.go @@ -2,24 +2,54 @@ package app import "github.com/cloud-barista/cm-ant/internal/core/common/constant" -type UpdatePriceInfosReq struct { - ProviderName string `json:"providerName" validate:"required"` - RegionName string `json:"regionName" validate:"required"` - InstanceType string `json:"instanceType" validate:"required"` +type UpdateAndGetEstimateCostReq struct { + Specs []struct { + ProviderName string `json:"providerName" validate:"required"` + RegionName string `json:"regionName" validate:"required"` + InstanceType string `json:"instanceType" validate:"required"` + Image string `json:"image"` + } `json:"specs"` + + SpecsWithFormat []struct { + CommonSpec string `json:"commonSpec" validate:"required"` + CommonImage string `json:"commonImage"` + } `json:"specsWithFormat"` +} + +type UpdateEstimateForecastCostReq struct { + NsId string `json:"nsId"` + MciId string `json:"mciId"` } -type GetPriceInfosReq struct { +type GetEstimateCostInfosReq struct { ProviderName string `query:"providerName" validate:"required"` RegionName string `query:"regionName" validate:"required"` InstanceType string `query:"instanceType"` VCpu string `query:"vCpu"` Memory string `query:"memory"` OsType string `query:"osType"` + Page int `query:"page"` + Size int `query:"size"` +} + +type GetEstimateForecastCostReq struct { + Page int `query:"page"` + Size int `query:"size"` + StartDate string `query:"startDate" validate:"required"` + EndDate string `query:"endDate" validate:"required"` + NsIds []string `query:"nsIds"` + MciIds []string `query:"mciIds"` + Providers []string `query:"provider"` + ResourceTypes []constant.ResourceType `query:"resourceTypes"` + ResourceIds []string `query:"resourceIds"` + CostAggregationType constant.CostAggregationType `query:"costAggregationType" validate:"required"` + DateOrder constant.OrderType `query:"dateOrder"` + ResourceTypeOrder constant.OrderType `query:"resourceTypeOrder"` } -type UpdateCostInfoReq struct { - MigrationId string `json:"migrationId"` - ConnectionName string `json:"connectionName" validate:"required"` +// ------------------------------------------------------------------------------------------------------------------- + +type UpdateEstimateForecastCostRawReq struct { CostResources []CostResourceReq `json:"costResources" validate:"required"` AwsAdditionalInfo AwsAdditionalInfoReq `json:"awsAdditionalInfo"` } @@ -33,15 +63,3 @@ type AwsAdditionalInfoReq struct { OwnerId string `json:"ownerId"` Regions []string `json:"regions"` } - -type GetCostInfoReq struct { - StartDate string `query:"startDate" validate:"required"` - EndDate string `query:"endDate" validate:"required"` - MigrationIds []string `query:"migrationIds"` - Providers []string `query:"provider"` - ResourceTypes []constant.ResourceType `query:"resourceTypes"` - ResourceIds []string `query:"resourceIds"` - CostAggregationType constant.CostAggregationType `query:"costAggregationType" validate:"required"` - DateOrder constant.OrderType `query:"dateOrder"` - ResourceTypeOrder constant.OrderType `query:"resourceTypeOrder"` -} diff --git a/internal/app/middlewares.go b/internal/app/middlewares.go index 54d5bed..73b318e 100644 --- a/internal/app/middlewares.go +++ b/internal/app/middlewares.go @@ -1,6 +1,7 @@ package app import ( + "net/http" "strings" "time" @@ -10,78 +11,24 @@ import ( "github.com/cloud-barista/cm-ant/internal/utils" "github.com/labstack/echo/v4" - zerolog "github.com/rs/zerolog/log" + "github.com/rs/zerolog/log" ) -// setMiddleware configures middleware for the Echo server. -func setMiddleware(e *echo.Echo) { - logSkipPattern := [][]string{ +var ( + logSkipPattern = [][]string{ {"/ant/swagger/*"}, + {"/ant/readyz"}, } +) + +func setMiddleware(e *echo.Echo) { e.Use( - middleware.RequestLoggerWithConfig( - middleware.RequestLoggerConfig{ - Skipper: func(c echo.Context) bool { - path := c.Request().URL.Path - query := c.Request().URL.RawQuery - for _, patterns := range logSkipPattern { - isAllMatched := true - for _, pattern := range patterns { - if !strings.Contains(path+query, pattern) { - isAllMatched = false - break - } - } - if isAllMatched { - return true - } - } - return false - }, - LogError: true, - LogRequestID: true, - LogRemoteIP: true, - LogHost: true, - LogMethod: true, - LogURI: true, - LogUserAgent: false, - LogStatus: true, - LogLatency: true, - LogContentLength: true, - LogResponseSize: true, - LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { - if v.Error == nil { - zerolog.Info(). - Str("id", v.RequestID). - Str("client_ip", v.RemoteIP). - // Str("host", v.Host). - Str("method", v.Method). - Str("URI", v.URI). - Int("status", v.Status). - // Int64("latency", v.Latency.Nanoseconds()). - Str("latency_human", v.Latency.String()). - Str("bytes_in", v.ContentLength). - Int64("bytes_out", v.ResponseSize). - Msg("request") - } else { - zerolog.Error(). - Err(v.Error). - Str("id", v.RequestID). - Str("client_ip", v.RemoteIP). - // Str("host", v.Host). - Str("method", v.Method). - Str("URI", v.URI). - Int("status", v.Status). - // Int64("latency", v.Latency.Nanoseconds()). - Str("latency_human", v.Latency.String()). - Str("bytes_in", v.ContentLength). - Int64("bytes_out", v.ResponseSize). - Msg("request error") - } - return nil - }, - }, - ), + middleware.Secure(), + middleware.RequestID(), + middleware.Recover(), + middleware.Gzip(), + middleware.CORS(), + Zerologger(logSkipPattern), middleware.TimeoutWithConfig( middleware.TimeoutConfig{ Skipper: middleware.DefaultSkipper, @@ -92,27 +39,76 @@ func setMiddleware(e *echo.Echo) { Timeout: 300 * time.Second, }, ), - middleware.Recover(), - middleware.RequestID(), + middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)), - middleware.CORS(), ) } -func RequestIdAndDetailsIssuer(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - // Make X-Request-Id visible to all handlers - c.Response().Header().Set("Access-Control-Expose-Headers", echo.HeaderXRequestID) - - // Get or generate Request ID - reqID := c.Request().Header.Get(echo.HeaderXRequestID) - if reqID == "" { - reqID = utils.CreateUniqIdBaseOnUnixTime() - } - - // Set Request on the context - c.Set("RequestID", reqID) - - return next(c) - } +func Zerologger(skipPatterns [][]string) echo.MiddlewareFunc { + return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + Skipper: func(c echo.Context) bool { + path := c.Request().URL.Path + query := c.Request().URL.RawQuery + for _, patterns := range skipPatterns { + isAllMatched := true + for _, pattern := range patterns { + if !strings.Contains(path+query, pattern) { + isAllMatched = false + break + } + } + if isAllMatched { + return true + } + } + return false + }, + LogError: true, + LogRequestID: true, + LogRemoteIP: true, + LogHost: true, + LogMethod: true, + LogURI: true, + LogUserAgent: false, + LogStatus: true, + LogLatency: true, + LogContentLength: true, + LogResponseSize: true, + // HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code + LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { + if v.Error == nil { + if v.Method != http.MethodOptions { + log.Info(). + Str("ID", v.RequestID). + Str("Method", v.Method). + Str("URI", v.URI). + Str("clientIP", v.RemoteIP). + //Str("host", v.Host). + //Str("user_agent", v.UserAgent). + Int("status", v.Status). + //Int64("latency", v.Latency.Nanoseconds()). + Str("latency", v.Latency.String()). + //Str("bytes_in", v.ContentLength). + //Int64("bytes_out", v.ResponseSize). + Msg("") + } + } else { + log.Error(). + Err(v.Error). + Str("ID", v.RequestID). + Str("Method", v.Method). + Str("URI", v.URI). + Str("clientIP", v.RemoteIP). + // Str("host", v.Host). + //Str("user_agent", v.UserAgent). + Int("status", v.Status). + // Int64("latency", v.Latency.Nanoseconds()). + Str("latency", v.Latency.String()). + //Str("bytes_in", v.ContentLength). + //Int64("bytes_out", v.ResponseSize). + Msg("") + } + return nil + }, + }) } diff --git a/internal/app/router.go b/internal/app/router.go index 4a05fdb..270b806 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -58,17 +58,15 @@ func (server *AntServer) InitRouter() error { } { - priceRouter := versionRouter.Group("/price") - { - priceRouter.POST("/info", server.updatePriceInfos) - priceRouter.GET("/info", server.getPriceInfos) - } + costEstimationHandler := versionRouter.Group("/cost/estimate") - costRouter := versionRouter.Group("/cost") - { - costRouter.POST("/info", server.updateCostInfos) - costRouter.GET("/info", server.getCostInfos) - } + costEstimationHandler.POST("", server.updateAndGetEstimateCost) + costEstimationHandler.GET("", server.getEstimateCost) + + costEstimationHandler.POST("/forecast", server.updateEstimateForecastCost) + costEstimationHandler.GET("/forecast", server.getEstimateForecastCost) + + costEstimationHandler.POST("/forecast/raw", server.updateEstimateForecastCostRaw) } return nil diff --git a/internal/app/server.go b/internal/app/server.go index e538a57..338b36c 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -90,7 +90,7 @@ func initializeRepositories(conn *gorm.DB) *antRepositories { func initializeServices(repos *antRepositories, tbClient *tumblebug.TumblebugClient, sClient *spider.SpiderClient) *antServices { loadServ := load.NewLoadService(repos.loadRepo, tbClient) - cc := cost.NewAwsCostExplorerSpiderCostCollector(sClient) + cc := cost.NewAwsCostExplorerSpiderCostCollector(sClient, tbClient) pc := cost.NewSpiderPriceCollector(sClient) costServ := cost.NewCostService(repos.costRepo, pc, cc) diff --git a/internal/config/config.go b/internal/config/config.go index 9364f12..4ccf806 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,8 +3,10 @@ package config import ( "fmt" "strings" + "time" "github.com/cloud-barista/cm-ant/internal/utils" + "github.com/rs/zerolog/log" "github.com/spf13/viper" ) @@ -31,6 +33,12 @@ type AntConfig struct { Username string `yaml:"username"` Password string `yaml:"password"` } `yaml:"tumblebug"` + + Cost struct { + Estimation struct { + UpdateInterval time.Duration `yaml:"updateInterval"` + } `yaml:"estimation"` + } `yaml:"cost"` Load struct { Retry int `yaml:"retry"` JMeter struct { @@ -38,9 +46,9 @@ type AntConfig struct { Version string `yaml:"version"` } `yaml:"jmeter"` } `yaml:"load"` - Logging struct { + Log struct { Level string `yaml:"level"` - } `yaml:"logging"` + } `yaml:"log"` Database struct { Driver string `yaml:"driver"` Host string `yaml:"host"` @@ -52,7 +60,7 @@ type AntConfig struct { } func InitConfig() error { - utils.LogInfo("Initializing configuration...") + log.Info().Msg("Initializing configuration...") cfg := AntConfig{} @@ -66,17 +74,17 @@ func InitConfig() error { err := viper.ReadInConfig() if err != nil { - utils.LogErrorf("Fatal error while reading config file: %v", err) + log.Error().Msgf("Fatal error while reading config file: %v", err) return fmt.Errorf("fatal error while read config file: %w", err) } err = viper.Unmarshal(&cfg) if err != nil { - utils.LogErrorf("Fatal error while unmarshaling config: %v", err) + log.Error().Msgf("Fatal error while unmarshaling config: %v", err) return fmt.Errorf("fatal error while unmarshal from config to ant config: %w", err) } - utils.LogInfof("Configuration loaded successfully: %+v", cfg) + log.Info().Msgf("Configuration loaded successfully: %+v", cfg) AppConfig = cfg return nil diff --git a/internal/core/cost/cost_collector.go b/internal/core/cost/cost_collector.go index 9033f2c..c74c070 100644 --- a/internal/core/cost/cost_collector.go +++ b/internal/core/cost/cost_collector.go @@ -11,25 +11,29 @@ import ( "github.com/cloud-barista/cm-ant/internal/core/common/constant" "github.com/cloud-barista/cm-ant/internal/infra/outbound/spider" + "github.com/cloud-barista/cm-ant/internal/infra/outbound/tumblebug" "github.com/cloud-barista/cm-ant/internal/utils" ) type CostCollector interface { Readyz(context.Context) error - GetCostInfos(context.Context, UpdateCostInfoParam) (CostInfos, error) + UpdateEstimateForecastCost(context.Context, UpdateEstimateForecastCostParam) (EstimateForecastCostInfos, error) + GetCostInfos(context.Context, UpdateEstimateForecastCostRawParam) (EstimateForecastCostInfos, error) } -type AwsCostExplorerSpiderCostCollector struct { +type AwsCostExplorerBaristaCostCollector struct { sc *spider.SpiderClient + tc *tumblebug.TumblebugClient } -func NewAwsCostExplorerSpiderCostCollector(sc *spider.SpiderClient) CostCollector { - return &AwsCostExplorerSpiderCostCollector{ +func NewAwsCostExplorerSpiderCostCollector(sc *spider.SpiderClient, tc *tumblebug.TumblebugClient) CostCollector { + return &AwsCostExplorerBaristaCostCollector{ sc: sc, + tc: tc, } } -func (a *AwsCostExplorerSpiderCostCollector) Readyz(ctx context.Context) error { +func (a *AwsCostExplorerBaristaCostCollector) Readyz(ctx context.Context) error { err := a.sc.ReadyzWithContext(ctx) if err != nil { return err @@ -88,7 +92,7 @@ type groupBy struct { Type string `json:"type"` // DIMENSION | TAG | COST_CATEGORY } -func (a *AwsCostExplorerSpiderCostCollector) generateFilterValue( +func (a *AwsCostExplorerBaristaCostCollector) generateFilterValue( costResources []CostResourceParam, awsAdditionalInfo AwsAdditionalInfoParam, ) ( []string, []string, error, @@ -124,7 +128,12 @@ func (a *AwsCostExplorerSpiderCostCollector) generateFilterValue( return serviceValue, resourceIdValues, nil } -func (a *AwsCostExplorerSpiderCostCollector) GetCostInfos(ctx context.Context, param UpdateCostInfoParam) (CostInfos, error) { +func (a *AwsCostExplorerBaristaCostCollector) GetCostInfos(ctx context.Context, param UpdateEstimateForecastCostRawParam) (EstimateForecastCostInfos, error) { + + if param.ConnectionName == "" { + param.ConnectionName = costExplorerConnectionName + } + serviceFilterValue, resourceIdFilterValue, err := a.generateFilterValue(param.CostResources, param.AwsAdditionalInfo) if err != nil { utils.LogError("parsing service and resource id for filtering cost explorer value") @@ -218,7 +227,7 @@ func (a *AwsCostExplorerSpiderCostCollector) GetCostInfos(ctx context.Context, p return nil, ErrCostResultEmpty } - var costInfos = make([]CostInfo, 0) + var costInfos = make([]EstimateForecastCostInfo, 0) for _, result := range res.ResultsByTime { if result.Groups == nil { utils.LogError("groups is nil; it must not be nil") @@ -275,8 +284,7 @@ func (a *AwsCostExplorerSpiderCostCollector) GetCostInfos(ctx context.Context, p continue } - costInfo := CostInfo{ - MigrationId: param.MigrationId, + costInfo := EstimateForecastCostInfo{ Provider: param.Provider, ConnectionName: param.ConnectionName, ResourceType: resourceType, @@ -296,3 +304,87 @@ func (a *AwsCostExplorerSpiderCostCollector) GetCostInfos(ctx context.Context, p return costInfos, nil } + +const ( + nsKey = "sys.namespace" + provider = "aws" + costExplorerConnectionName = "aws-us-east-1" + defaultNsId = "ns01" + defaultMciId = "mmci01" +) + +func (a *AwsCostExplorerBaristaCostCollector) UpdateEstimateForecastCost(ctx context.Context, param UpdateEstimateForecastCostParam) (EstimateForecastCostInfos, error) { + + if param.NsId == "" { + param.NsId = defaultNsId + } + + if param.MciId == "" { + param.MciId = defaultMciId + } + + res := EstimateForecastCostInfos{} + + mci, err := a.tc.GetMciWithContext(ctx, param.NsId, param.MciId) + + if err != nil { + utils.LogError("error while get mci from tumblebug; ", err) + return res, err + } + + if len(mci.Vm) == 0 { + return nil, errors.New("there is no vm in mci") + } + + mciLabels := mci.Label + _ = mciLabels[nsKey] + + arg := UpdateEstimateForecastCostRawParam{ + Provider: provider, + ConnectionName: costExplorerConnectionName, + StartDate: param.StartDate, + EndDate: param.EndDate, + CostResources: make([]CostResourceParam, 0), + } + + vmIds := make([]string, 0) + + for _, mci := range mci.Vm { + pn := mci.ConnectionConfig.ProviderName + + if pn == "" || !strings.EqualFold(strings.ToLower(pn), "aws") { + utils.LogWarnf("CSP: %s, does not support yet", pn) + continue + } + + vmId := mci.CspResourceId + _ = mci.ConnectionName + _ = mci.Label + + vmIds = append(vmIds, vmId) + } + + if len(vmIds) != 0 { + arg.CostResources = append(arg.CostResources, CostResourceParam{ + ResourceType: constant.VM, + ResourceIds: vmIds, + }) + } else { + return res, errors.New("no vm resource create on aws") + } + + infos, err := a.GetCostInfos(ctx, arg) + + if err != nil { + utils.LogError("error while get cost info from spider;", err) + return res, fmt.Errorf("error from get cost infos +%w", err) + } + + for i := range infos { + info := infos[i] + info.NsId = param.NsId + info.MciId = param.MciId + } + + return infos, nil +} diff --git a/internal/core/cost/dtos.go b/internal/core/cost/dtos.go index ebead09..7770efd 100644 --- a/internal/core/cost/dtos.go +++ b/internal/core/cost/dtos.go @@ -1,11 +1,70 @@ package cost import ( + "crypto/sha256" + "encoding/hex" "time" "github.com/cloud-barista/cm-ant/internal/core/common/constant" ) +type UpdateAndGetEstimateCostParam struct { + RecommendSpecs []RecommendSpecParam `json:"recommendSpecs"` + + TimeStandard time.Time `json:"timeStandard"` + PricePolicy constant.PricePolicy `json:"pricePolicy"` +} + +type RecommendSpecParam struct { + ProviderName string `json:"providerName"` + RegionName string `json:"regionName"` + InstanceType string `json:"instanceType"` + Image string `json:"image"` +} + +func (r RecommendSpecParam) Hash() string { + h := sha256.New() + + h.Write([]byte(r.ProviderName)) + h.Write([]byte(r.RegionName)) + h.Write([]byte(r.InstanceType)) + h.Write([]byte(r.Image)) + + hashBytes := h.Sum(nil) + return hex.EncodeToString(hashBytes) +} + +type EstimateCostResults struct { + EsimateCostSpecResults []EsimateCostSpecResults `json:"esimateCostSpecResults,omitempty"` +} + +type EsimateCostSpecResults struct { + ProviderName string `json:"providerName,omitempty"` + RegionName string `json:"regionName,omitempty"` + InstanceType string `json:"instanceType,omitempty"` + ImageName string `json:"imageName,omitempty"` + SpecMinMonthlyPrice float64 `json:"totalMinMonthlyPrice,omitempty"` + SpecMaxMonthlyPrice float64 `json:"totalMaxMonthlyPrice,omitempty"` + EstimateCostSpecDetailResults []EstimateCostSpecDetailResult `json:"estimateForecastCostSpecDetailResults,omitempty"` +} + +type EstimateCostSpecDetailResult struct { + ID uint `json:"id"` + VCpu string `json:"vCpu,omitempty"` + Memory string `json:"memory,omitempty"` + Storage string `json:"storage,omitempty"` + OsType string `json:"osType,omitempty"` + ProductDescription string `json:"productDescription,omitempty"` + OriginalPricePolicy string `json:"originalPricePolicy,omitempty"` + PricePolicy constant.PricePolicy `json:"pricePolicy,omitempty"` + Unit constant.PriceUnit `json:"unit,omitempty"` + Currency constant.PriceCurrency `json:"currency,omitempty"` + Price string `json:"price,omitempty"` + CalculatedMonthlyPrice float64 `json:"calculatedMonthlyPrice,omitempty"` + PriceDescription string `json:"priceDescription,omitempty"` + LastUpdatedAt time.Time `json:"lastUpdatedAt,omitempty"` +} + type UpdatePriceInfosParam struct { MigrationId string ProviderName string @@ -16,7 +75,7 @@ type UpdatePriceInfosParam struct { PricePolicy constant.PricePolicy } -type GetPriceInfosParam struct { +type GetEstimateCostParam struct { ProviderName string RegionName string InstanceType string @@ -27,14 +86,16 @@ type GetPriceInfosParam struct { TimeStandard time.Time PricePolicy constant.PricePolicy + Page int + Size int } -type AllPriceInfoResult struct { - PriceInfoList []PriceInfoResult `json:"priceInfoList,omitempty"` - ResultCount int64 `json:"resultCount"` +type EstimateCostInfoResults struct { + EstimateCostInfoResult []EstimateCostInfoResult `json:"estimateCostInfoResult,omitempty"` + ResultCount int64 `json:"resultCount"` } -type PriceInfoResult struct { +type EstimateCostInfoResult struct { ID uint `json:"id"` ProviderName string `json:"providerName"` RegionName string `json:"regionName"` @@ -55,36 +116,26 @@ type PriceInfoResult struct { LastUpdatedAt time.Time `json:"lastUpdatedAt,omitempty"` } -type UpdateCostInfoParam struct { - MigrationId string - Provider string // currently only aws - ConnectionName string - StartDate time.Time - EndDate time.Time - CostResources []CostResourceParam - AwsAdditionalInfo AwsAdditionalInfoParam +type UpdateEstimateForecastCostParam struct { + NsId string + MciId string + StartDate time.Time + EndDate time.Time } -type CostResourceParam struct { - ResourceType constant.ResourceType - ResourceIds []string -} - -type AwsAdditionalInfoParam struct { - OwnerId string `json:"ownerId"` - Regions []string `json:"regions"` -} - -type UpdateCostInfoResult struct { +type UpdateEstimateForecastCostInfoResult struct { FetchedDataCount int64 `json:"fetchedDataCount"` UpdatedDataCount int64 `json:"updatedDataCount"` - InsertedDataCount int64 `insertedDataCount` + InsertedDataCount int64 `json:"insertedDataCount"` } -type GetCostInfoParam struct { +type GetEstimateForecastCostParam struct { + Page int + Size int StartDate time.Time EndDate time.Time - MigrationIds []string + NsIds []string + MciIds []string Providers []string ResourceTypes []constant.ResourceType ResourceIds []string @@ -93,7 +144,11 @@ type GetCostInfoParam struct { ResourceTypeOrder constant.OrderType } -type GetCostInfoResult struct { +type GetEstimateForecastCostInfoResults struct { + GetEstimateForecastCostInfoResults []GetEstimateForecastCostInfoResult `json:"getEstimateForecastCostInfoResults,omitempty"` + ResultCount int64 `json:"resultCount"` +} +type GetEstimateForecastCostInfoResult struct { Provider string `json:"provider"` ResourceType string `json:"resourceType"` Category string `json:"category"` @@ -102,3 +157,24 @@ type GetCostInfoResult struct { Date time.Time `json:"date"` TotalCost float64 `json:"totalCost"` } + +// ------------------------------------------------------------------- + +type UpdateEstimateForecastCostRawParam struct { + Provider string // currently only aws + ConnectionName string + StartDate time.Time + EndDate time.Time + CostResources []CostResourceParam + AwsAdditionalInfo AwsAdditionalInfoParam +} + +type CostResourceParam struct { + ResourceType constant.ResourceType + ResourceIds []string +} + +type AwsAdditionalInfoParam struct { + OwnerId string `json:"ownerId"` + Regions []string `json:"regions"` +} diff --git a/internal/core/cost/models.go b/internal/core/cost/models.go index b779fc7..3c66532 100644 --- a/internal/core/cost/models.go +++ b/internal/core/cost/models.go @@ -7,9 +7,9 @@ import ( "gorm.io/gorm" ) -type PriceInfos []*PriceInfo +type EstimateCostInfos []*EstimateCostInfo -type PriceInfo struct { +type EstimateCostInfo struct { gorm.Model ProviderName string `gorm:"index"` RegionName string `gorm:"index"` @@ -31,13 +31,14 @@ type PriceInfo struct { OriginalCurrency string CalculatedMonthlyPrice float64 `gorm:"index"` PriceDescription string + LastUpdatedAt time.Time + ImageName string `gorm:"index"` } -type CostInfos []CostInfo +type EstimateForecastCostInfos []EstimateForecastCostInfo -type CostInfo struct { +type EstimateForecastCostInfo struct { gorm.Model - MigrationId string `gorm:"index"` Provider string `gorm:"index"` ConnectionName string ResourceType constant.ResourceType `gorm:"index"` @@ -49,10 +50,6 @@ type CostInfo struct { Granularity string `gorm:"index"` StartDate time.Time `gorm:"index"` EndDate time.Time `gorm:"index"` -} - -type CostUpdateRestrict struct { - gorm.Model - StandardDate time.Time - UpdateCount int64 + NsId string `gorm:"index"` + MciId string `gorm:"index"` } diff --git a/internal/core/cost/price_collector.go b/internal/core/cost/price_collector.go index 9403315..6ca512e 100644 --- a/internal/core/cost/price_collector.go +++ b/internal/core/cost/price_collector.go @@ -7,6 +7,7 @@ import ( "sort" "strconv" "strings" + "time" "unicode" "github.com/cloud-barista/cm-ant/internal/core/common/constant" @@ -16,7 +17,7 @@ import ( type PriceCollector interface { Readyz(context.Context) error - GetPriceInfos(context.Context, UpdatePriceInfosParam) (PriceInfos, error) + FetchPriceInfos(context.Context, RecommendSpecParam) (EstimateCostInfos, error) } var ( @@ -31,21 +32,21 @@ var ( "ncpvpc": "Monthly Flat Rate", } - priceValidator = map[string]func(res *PriceInfo) bool{ - "aws": func(res *PriceInfo) bool { + priceValidator = map[string]func(res *EstimateCostInfo) bool{ + "aws": func(res *EstimateCostInfo) bool { return !strings.Contains(res.PriceDescription, "Reservation") }, - "gcp": func(res *PriceInfo) bool { return true }, - "azure": func(res *PriceInfo) bool { return true }, - "tencent": func(res *PriceInfo) bool { + "gcp": func(res *EstimateCostInfo) bool { return true }, + "azure": func(res *EstimateCostInfo) bool { return true }, + "tencent": func(res *EstimateCostInfo) bool { return res.OriginalPricePolicy == onDemandPricingPolicyMap[res.ProviderName] }, - "alibaba": func(res *PriceInfo) bool { return true }, - "ibm": func(res *PriceInfo) bool { + "alibaba": func(res *EstimateCostInfo) bool { return true }, + "ibm": func(res *EstimateCostInfo) bool { return strings.Contains(res.OriginalUnit, "Instance-Hour") }, - "ncp": func(res *PriceInfo) bool { return true }, - "ncpvpc": func(res *PriceInfo) bool { return true }, + "ncp": func(res *EstimateCostInfo) bool { return true }, + "ncpvpc": func(res *EstimateCostInfo) bool { return true }, } units = map[string]bool{ @@ -77,12 +78,12 @@ func (s *SpiderPriceCollector) Readyz(ctx context.Context) error { return nil } -func (s *SpiderPriceCollector) GetPriceInfos(ctx context.Context, param UpdatePriceInfosParam) (PriceInfos, error) { +func (s *SpiderPriceCollector) FetchPriceInfos(ctx context.Context, param RecommendSpecParam) (EstimateCostInfos, error) { connectionName := fmt.Sprintf("%s-%s", strings.ToLower(param.ProviderName), strings.ToLower(param.RegionName)) req := spider.PriceInfoReq{ ConnectionName: connectionName, - FilterList: s.generateFilterList(param), + FilterList: s.generateFilter(param), } result, err := s.sc.GetPriceInfoWithContext(ctx, param.RegionName, req) @@ -95,7 +96,7 @@ func (s *SpiderPriceCollector) GetPriceInfos(ctx context.Context, param UpdatePr return nil, err } - createdPriceInfo := make([]*PriceInfo, 0) + createdPriceInfo := make([]*EstimateCostInfo, 0) if result.CloudPriceList != nil { for i := range result.CloudPriceList { p := result.CloudPriceList[i] @@ -136,19 +137,27 @@ func (s *SpiderPriceCollector) GetPriceInfos(ctx context.Context, param UpdatePr currency = s.parseCurrency(policy.Currency) convertedPrice, err := strconv.ParseFloat(policy.Price, 64) if err != nil { + utils.LogWarnf("not allowed for error; %s", err) continue } if convertedPrice == float64(0) { + utils.LogWarn("not allowed for empty price") continue } price = s.naChecker(policy.Price) if price == "" { + utils.LogWarn("not allowed for empty price") continue } - pi := PriceInfo{ + if strings.Contains(strings.ToLower(priceDescription), "dedicated") { + utils.LogWarnf("not allowed for dedicated instance hour; %s", priceDescription) + continue + } + + pi := EstimateCostInfo{ ProviderName: param.ProviderName, RegionName: productInfo.RegionName, InstanceType: productInfo.InstanceType, @@ -169,6 +178,8 @@ func (s *SpiderPriceCollector) GetPriceInfos(ctx context.Context, param UpdatePr OriginalCurrency: originalCurrency, PriceDescription: priceDescription, CalculatedMonthlyPrice: s.calculatePrice(price, unit), + LastUpdatedAt: time.Now(), + ImageName: param.Image, } if !priceValidator[param.ProviderName](&pi) { @@ -191,7 +202,7 @@ func (s *SpiderPriceCollector) GetPriceInfos(ctx context.Context, param UpdatePr return createdPriceInfo, nil } -func (s *SpiderPriceCollector) generateFilterList(param UpdatePriceInfosParam) []spider.FilterReq { +func (s *SpiderPriceCollector) generateFilter(param RecommendSpecParam) []spider.FilterReq { providerName := strings.ToLower(param.ProviderName) param.ProviderName = providerName @@ -205,13 +216,10 @@ func (s *SpiderPriceCollector) generateFilterList(param UpdatePriceInfosParam) [ Key: "regionName", Value: param.RegionName, }, - } - - if param.InstanceType != "" { - ret = append(ret, spider.FilterReq{ + { Key: "instanceType", Value: param.InstanceType, - }) + }, } return ret diff --git a/internal/core/cost/repository.go b/internal/core/cost/repository.go index e0c9b4f..57f934f 100644 --- a/internal/core/cost/repository.go +++ b/internal/core/cost/repository.go @@ -2,8 +2,10 @@ package cost import ( "context" + "errors" "fmt" "strings" + "time" "github.com/cloud-barista/cm-ant/internal/core/common/constant" "gorm.io/gorm" @@ -36,11 +38,12 @@ func (r *CostRepository) execInTransaction(ctx context.Context, fn func(*gorm.DB return tx.Commit().Error } -func (r *CostRepository) GetAllMatchingPriceInfoList(ctx context.Context, param GetPriceInfosParam) (PriceInfos, error) { - var priceInfoList []*PriceInfo +func (r *CostRepository) GetMatchingEstimateCostInfosTx(ctx context.Context, param GetEstimateCostParam) (EstimateCostInfos, int64, error) { + var priceInfoList []*EstimateCostInfo + var totalRows int64 err := r.execInTransaction(ctx, func(d *gorm.DB) error { - q := d.Model(&PriceInfo{}). + q := d.Model(&EstimateCostInfo{}). Where( "price_policy = ? AND updated_at >= ?", param.PricePolicy, param.TimeStandard, @@ -72,6 +75,14 @@ func (r *CostRepository) GetAllMatchingPriceInfoList(ctx context.Context, param q = q.Where("LOWER(os_type) = ?", strings.ToLower(param.OsType)) } + if err := q.Count(&totalRows).Error; err != nil { + return err + } + + offset := (param.Page - 1) * param.Size + q = q.Offset(offset). + Limit(param.Size) + if err := q.Find(&priceInfoList).Error; err != nil { return err } @@ -79,31 +90,73 @@ func (r *CostRepository) GetAllMatchingPriceInfoList(ctx context.Context, param return nil }) - return priceInfoList, err + return priceInfoList, totalRows, err } -func (r *CostRepository) CountMatchingPriceInfoList(ctx context.Context, param UpdatePriceInfosParam) (int64, error) { - var totalCount int64 +func (r *CostRepository) GetMatchingEstimateCostWithoutTypeTx(ctx context.Context, param RecommendSpecParam, timeStandard time.Time, pricePolicy constant.PricePolicy) (EstimateCostInfos, error) { + var priceInfos []*EstimateCostInfo err := r.execInTransaction(ctx, func(d *gorm.DB) error { - q := d.Model(&PriceInfo{}). + q := d.Model(&EstimateCostInfo{}). Where( - "LOWER(provider_name) = ? AND LOWER(region_name) = ? AND price_policy = ? AND updated_at >= ?", - strings.ToLower(param.ProviderName), strings.ToLower(param.RegionName), param.PricePolicy, param.TimeStandard, + "LOWER(provider_name) = ? AND LOWER(region_name) = ? AND price_policy = ? AND last_updated_at >= ?", + strings.ToLower(param.ProviderName), + strings.ToLower(param.RegionName), + pricePolicy, + timeStandard, ) - if param.InstanceType != "" { - q = q.Where("LOWER(instance_type) = ?", strings.ToLower(param.InstanceType)) + if param.Image != "" { + q = q.Where("image_name = ?", strings.ToLower(param.Image)) } - return q.Count(&totalCount).Error + if err := q.Find(&priceInfos).Error; err != nil { + return err + } + + return nil }) - return totalCount, err + if err != nil { + return nil, err + } + return priceInfos, nil } -func (r *CostRepository) BatchInsertAllResult(ctx context.Context, param UpdatePriceInfosParam, created PriceInfos) error { +func (r *CostRepository) GetMatchingEstimateCostTx(ctx context.Context, param RecommendSpecParam, timeStandard time.Time, pricePolicy constant.PricePolicy) (EstimateCostInfos, error) { + var priceInfos []*EstimateCostInfo + + err := r.execInTransaction(ctx, func(d *gorm.DB) error { + q := d.Model(&EstimateCostInfo{}). + Where( + "LOWER(provider_name) = ? AND LOWER(region_name) = ? AND instance_type = ? AND price_policy = ? AND last_updated_at >= ?", + strings.ToLower(param.ProviderName), + strings.ToLower(param.RegionName), + strings.ToLower(param.InstanceType), + pricePolicy, + timeStandard, + ) + + if param.Image != "" { + q = q.Where("image_name = ?", strings.ToLower(param.Image)) + } + + if err := q.Find(&priceInfos).Error; err != nil { + return err + } + + return nil + }) + + if err != nil { + return nil, err + } + + return priceInfos, nil +} + +func (r *CostRepository) BatchInsertAllEstimateCostResultTx(ctx context.Context, created EstimateCostInfos) error { batchSize := 100 err := r.execInTransaction(ctx, func(d *gorm.DB) error { @@ -125,14 +178,13 @@ func (r *CostRepository) BatchInsertAllResult(ctx context.Context, param UpdateP } -func (r *CostRepository) UpsertCostInfo(ctx context.Context, costInfo CostInfo) (int64, int64, error) { +func (r *CostRepository) UpsertCostInfo(ctx context.Context, costInfo EstimateForecastCostInfo) (int64, int64, error) { var updateCount = int64(0) var insertCount = int64(0) err := r.execInTransaction(ctx, func(d *gorm.DB) error { err := d. Model(costInfo). - Where(&CostInfo{ - MigrationId: costInfo.MigrationId, + Where(&EstimateForecastCostInfo{ Provider: costInfo.Provider, ResourceType: costInfo.ResourceType, Category: costInfo.Category, @@ -153,12 +205,13 @@ func (r *CostRepository) UpsertCostInfo(ctx context.Context, costInfo CostInfo) insertCount++ } else { if err := d.Model(&costInfo).Updates(map[string]interface{}{ - "cost": costInfo.Cost, - "unit": costInfo.Unit, + "cost": costInfo.Cost, + "unit": costInfo.Unit, + "ns_id": costInfo.NsId, + "mci_id": costInfo.MciId, }).Error; err != nil { return err } - updateCount++ } @@ -169,11 +222,12 @@ func (r *CostRepository) UpsertCostInfo(ctx context.Context, costInfo CostInfo) } -func (r *CostRepository) GetCostInfoWithFilter(ctx context.Context, param GetCostInfoParam) ([]GetCostInfoResult, error) { - var costInfo []GetCostInfoResult +func (r *CostRepository) GetEstimateForecastCostInfosTx(ctx context.Context, param GetEstimateForecastCostParam) ([]GetEstimateForecastCostInfoResult, int64, error) { + var costInfo []GetEstimateForecastCostInfoResult + var totalRows int64 err := r.execInTransaction(ctx, func(d *gorm.DB) error { - query := d.Model(&CostInfo{}) + query := d.Model(&EstimateForecastCostInfo{}) if len(param.Providers) > 0 { query = query.Where("provider IN ?", param.Providers) @@ -186,6 +240,14 @@ func (r *CostRepository) GetCostInfoWithFilter(ctx context.Context, param GetCos query = query.Where("actual_resource_id IN ?", param.ResourceIds) } + if len(param.NsIds) > 0 { + query = query.Where("ns_id IN ?", param.NsIds) + } + + if len(param.MciIds) > 0 { + query = query.Where("mci_id IN ?", param.MciIds) + } + query = query.Where("start_date >= ? AND end_date <= ?", param.StartDate, param.EndDate) switch param.CostAggregationType { @@ -208,6 +270,14 @@ func (r *CostRepository) GetCostInfoWithFilter(ctx context.Context, param GetCos query = query.Order("resource_type " + string(param.ResourceTypeOrder)) } + if err := query.Count(&totalRows).Error; err != nil { + return err + } + + offset := (param.Page - 1) * param.Size + query = query.Offset(offset). + Limit(param.Size) + if err := query.Find(&costInfo).Error; err != nil { return err } @@ -215,5 +285,11 @@ func (r *CostRepository) GetCostInfoWithFilter(ctx context.Context, param GetCos return nil }) - return costInfo, err + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return costInfo, totalRows, err + } + } + + return costInfo, totalRows, nil } diff --git a/internal/core/cost/service.go b/internal/core/cost/service.go index 3976ec2..8170b72 100644 --- a/internal/core/cost/service.go +++ b/internal/core/cost/service.go @@ -4,11 +4,14 @@ import ( "context" "errors" "fmt" + "sort" "strings" + "sync" "time" "github.com/cloud-barista/cm-ant/internal/core/common/constant" "github.com/cloud-barista/cm-ant/internal/utils" + "github.com/rs/zerolog/log" ) type CostService struct { @@ -52,57 +55,201 @@ func (c *CostService) Readyz() error { return nil } -func (c *CostService) UpdatePriceInfos(param UpdatePriceInfosParam) error { +var estimateCostUpdateLockMap sync.Map + +func (c *CostService) UpdateAndGetEstimateCost(param UpdateAndGetEstimateCostParam) (EstimateCostResults, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - param.TimeStandard = time.Now().AddDate(0, 0, -7).Truncate(24 * time.Hour) - param.PricePolicy = constant.OnDemand + var wg sync.WaitGroup + var mu sync.Mutex + var results []EsimateCostSpecResults + var errList []error + var esimateCostSpecResult EstimateCostResults - count, err := c.costRepo.CountMatchingPriceInfoList(ctx, param) - if err != nil { - return err - } + log.Info().Msgf("Fetching estimate cost info for spec: %+v", param) - if count <= int64(0) { - resList, err := c.priceCollector.GetPriceInfos(ctx, param) + fail := func(msgFormat string, err error, p RecommendSpecParam) { + mu.Lock() + errList = append(errList, err) + mu.Unlock() + log.Error().Msgf(msgFormat, p, err) + } - if err != nil { - if strings.Contains(err.Error(), "you don't have any permission") { - return fmt.Errorf("you don't have permission to query the price for %s", param.ProviderName) + for _, v := range param.RecommendSpecs { + wg.Add(1) + go func(p RecommendSpecParam) { + defer wg.Done() + + // memory lock + rl, _ := estimateCostUpdateLockMap.LoadOrStore(p.Hash(), &sync.Mutex{}) + lock := rl.(*sync.Mutex) + + lock.Lock() + defer lock.Unlock() + + var err error + var estimateCostInfos EstimateCostInfos + possibleFetch := true + + if p.ProviderName == "ibm" || p.ProviderName == "azure" { + r, err := c.costRepo.GetMatchingEstimateCostWithoutTypeTx(ctx, v, param.TimeStandard, param.PricePolicy) + if err != nil { + fail("Error fetching estimate cost info: %v; %s", err, p) + return + } + + // ibm and azure price fetching filter is not working so if user put never exist instance type, + // it always fetch price data from collector. + // check and matching with database values + if len(r) != 0 { + temp := make([]*EstimateCostInfo, 0) + + for i := range r { + val := r[i] + + if val == nil { + log.Warn().Msg("estimate cost info value is nil") + continue + } + + if strings.EqualFold(v.InstanceType, p.InstanceType) { + temp = append(temp, val) + } + } + + if len(temp) > 0 { + estimateCostInfos = temp + } else { + possibleFetch = false + } + } + } else { + estimateCostInfos, err = c.costRepo.GetMatchingEstimateCostTx(ctx, p, param.TimeStandard, param.PricePolicy) } - return err - } - if len(resList) > 0 { - err := c.costRepo.BatchInsertAllResult(ctx, param, resList) if err != nil { - return err + fail("Error fetching estimate cost info for spec: %v; %s", err, p) + return } - } + + if len(estimateCostInfos) == 0 || possibleFetch { + log.Info().Msgf("No matching estimate cost found from database for spec: %+v, fetching from price collector", p) + + resList, err := c.priceCollector.FetchPriceInfos(ctx, p) + if err != nil { + fail("Error retrieving estimate cost info spec: %v; %s", fmt.Errorf("error retrieving estimate cost info for %+v: %w", p, err), p) + return + } + + if len(resList) > 0 { + log.Info().Msgf("Inserting fetched estimate cost info results for spec: %+v", p) + + err = c.costRepo.BatchInsertAllEstimateCostResultTx(ctx, resList) + if err != nil { + fail("Error batch inserting estimate cost info spec: %v; %s", fmt.Errorf("error batch inserting results for %+v: %w", p, err), p) + return + } + } + estimateCostInfos = resList + } + + res := EsimateCostSpecResults{ + ProviderName: p.ProviderName, + RegionName: p.RegionName, + InstanceType: p.InstanceType, + ImageName: p.Image, + EstimateCostSpecDetailResults: make([]EstimateCostSpecDetailResult, 0), + } + + if len(estimateCostInfos) > 0 { + minPrice := estimateCostInfos[0].CalculatedMonthlyPrice + maxPrice := estimateCostInfos[0].CalculatedMonthlyPrice + + for _, va := range estimateCostInfos { + + if !strings.EqualFold(v.InstanceType, p.InstanceType) { + log.Warn().Msgf("%s instance type is not matching with provided condition %s", va.InstanceType, p.InstanceType) + continue + } + calculatedPrice := va.CalculatedMonthlyPrice + log.Info().Msgf("Price calculated for spec %+v: %f", p, calculatedPrice) + + if calculatedPrice < minPrice { + minPrice = calculatedPrice + } + if calculatedPrice > maxPrice { + maxPrice = calculatedPrice + } + + specDetail := EstimateCostSpecDetailResult{ + ID: va.ID, + VCpu: va.VCpu, + Memory: fmt.Sprintf("%s %s", va.Memory, va.MemoryUnit), + Storage: va.Storage, + OsType: va.OsType, + ProductDescription: va.ProductDescription, + OriginalPricePolicy: va.OriginalPricePolicy, + PricePolicy: va.PricePolicy, + Unit: va.Unit, + Currency: va.Currency, + Price: va.Price, + CalculatedMonthlyPrice: calculatedPrice, + PriceDescription: va.PriceDescription, + LastUpdatedAt: va.LastUpdatedAt, + } + + res.SpecMinMonthlyPrice = minPrice + res.SpecMaxMonthlyPrice = maxPrice + res.EstimateCostSpecDetailResults = append(res.EstimateCostSpecDetailResults, specDetail) + } + + if len(res.EstimateCostSpecDetailResults) > 0 { + sort.Slice(res.EstimateCostSpecDetailResults, func(i, j int) bool { + return res.EstimateCostSpecDetailResults[i].CalculatedMonthlyPrice < res.EstimateCostSpecDetailResults[j].CalculatedMonthlyPrice + }) + } + + mu.Lock() + results = append(results, res) + mu.Unlock() + log.Info().Msgf("Successfully calculated cost for spec: %+v", param) + } + + }(v) } - return nil + wg.Wait() + + if len(errList) > 0 { + return esimateCostSpecResult, fmt.Errorf("errors occurred during processing: %v", errList) + } + + if len(results) > 0 { + esimateCostSpecResult.EsimateCostSpecResults = results + } + + return esimateCostSpecResult, nil } -func (c *CostService) GetPriceInfos(param GetPriceInfosParam) (AllPriceInfoResult, error) { +func (c *CostService) GetEstimateCost(param GetEstimateCostParam) (EstimateCostInfoResults, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() param.TimeStandard = time.Now().AddDate(0, 0, -7).Truncate(24 * time.Hour) param.PricePolicy = constant.OnDemand - var res AllPriceInfoResult + var res EstimateCostInfoResults - priceInfos, err := c.costRepo.GetAllMatchingPriceInfoList(ctx, param) + estimateCostInfos, totalCount, err := c.costRepo.GetMatchingEstimateCostInfosTx(ctx, param) if err != nil { return res, err } - priceInfoList := make([]PriceInfoResult, 0) + priceInfoList := make([]EstimateCostInfoResult, 0) - if len(priceInfos) > 0 { - for _, v := range priceInfos { - result := PriceInfoResult{ + if len(estimateCostInfos) > 0 { + for _, v := range estimateCostInfos { + result := EstimateCostInfoResult{ ID: v.ID, ProviderName: v.ProviderName, RegionName: v.RegionName, @@ -124,8 +271,8 @@ func (c *CostService) GetPriceInfos(param GetPriceInfosParam) (AllPriceInfoResul priceInfoList = append(priceInfoList, result) } - res.PriceInfoList = priceInfoList - res.ResultCount = int64(len(priceInfoList)) + res.EstimateCostInfoResult = priceInfoList + res.ResultCount = int64(totalCount) return res, nil } @@ -133,17 +280,68 @@ func (c *CostService) GetPriceInfos(param GetPriceInfosParam) (AllPriceInfoResul return res, nil } +func (c *CostService) UpdateEstimateForecastCost(param UpdateEstimateForecastCostParam) (UpdateEstimateForecastCostInfoResult, error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + var updateEstimateForecastCostInfoResult UpdateEstimateForecastCostInfoResult + + r, err := c.costCollector.UpdateEstimateForecastCost(ctx, param) + if err != nil { + return updateEstimateForecastCostInfoResult, err + } + + updateEstimateForecastCostInfoResult.FetchedDataCount = int64(len(r)) + + var updatedCount int64 + var insertedCount int64 + + for _, costInfo := range r { + u, i, err := c.costRepo.UpsertCostInfo(ctx, costInfo) + if err != nil { + utils.LogErrorf("upsert error: %+v", costInfo) + } + + updatedCount += u + insertedCount += i + } + + utils.LogInfof("updated count: %d; inserted count : %d", updatedCount, insertedCount) + + updateEstimateForecastCostInfoResult.UpdatedDataCount = updatedCount + updateEstimateForecastCostInfoResult.InsertedDataCount = insertedCount + + return updateEstimateForecastCostInfoResult, nil +} + +func (c *CostService) GetEstimateForecastCostInfos(param GetEstimateForecastCostParam) (GetEstimateForecastCostInfoResults, error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + res := GetEstimateForecastCostInfoResults{} + + r, totalCount, err := c.costRepo.GetEstimateForecastCostInfosTx(ctx, param) + if err != nil { + return res, err + } + + res.GetEstimateForecastCostInfoResults = r + res.ResultCount = totalCount + + return res, nil +} + var ( ErrRequestResourceEmpty = errors.New("cost request info is not enough") ErrCostResultEmpty = errors.New("cost information does not exist") ErrCostResultFormatInvalid = errors.New("cost result does not matching with interface") ) -func (c *CostService) UpdateCostInfo(param UpdateCostInfoParam) (UpdateCostInfoResult, error) { +func (c *CostService) UpdateEstimateForecastCostRaw(param UpdateEstimateForecastCostRawParam) (UpdateEstimateForecastCostInfoResult, error) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() - var updateCostInfoResult UpdateCostInfoResult + var updateCostInfoResult UpdateEstimateForecastCostInfoResult r, err := c.costCollector.GetCostInfos(ctx, param) if err != nil { @@ -171,13 +369,3 @@ func (c *CostService) UpdateCostInfo(param UpdateCostInfoParam) (UpdateCostInfoR return updateCostInfoResult, nil } - -func (c *CostService) GetCostInfos(param GetCostInfoParam) ([]GetCostInfoResult, error) { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) - defer cancel() - r, err := c.costRepo.GetCostInfoWithFilter(ctx, param) - if err != nil { - return nil, err - } - return r, nil -} diff --git a/internal/infra/db/db.go b/internal/infra/db/db.go index b9e56c6..fec3b35 100644 --- a/internal/infra/db/db.go +++ b/internal/infra/db/db.go @@ -27,8 +27,8 @@ func migrateDB(defaultDb *gorm.DB) error { &load.LoadTestExecutionHttpInfo{}, &load.LoadTestExecutionState{}, - &cost.PriceInfo{}, - &cost.CostInfo{}, + &cost.EstimateCostInfo{}, + &cost.EstimateForecastCostInfo{}, ) if err != nil { diff --git a/internal/utils/log.go b/internal/utils/log.go index 11c3e23..ce685bb 100644 --- a/internal/utils/log.go +++ b/internal/utils/log.go @@ -6,9 +6,10 @@ import ( ) const ( - colorReset = "\033[0m" - colorRed = "\033[31m" - colorGreen = "\033[32m" + colorReset = "\033[0m" + colorRed = "\033[31m" + colorGreen = "\033[32m" + colorYellow = "\033[33m" ) // LogLevel type to represent different log levels @@ -16,6 +17,7 @@ type LogLevel string const ( Info LogLevel = "INFO" + Warn LogLevel = "WARN" Error LogLevel = "ERROR" ) @@ -25,6 +27,8 @@ func Log(level LogLevel, v ...interface{}) { switch level { case Info: color = colorGreen + case Warn: + color = colorYellow case Error: color = colorRed default: @@ -56,6 +60,14 @@ func LogInfof(format string, v ...interface{}) { Logf(Info, format, v...) } +func LogWarn(v ...interface{}) { + Log(Warn, v...) +} + +func LogWarnf(format string, v ...interface{}) { + Logf(Warn, format, v...) +} + func LogError(v ...interface{}) { Log(Error, v...) }