diff --git a/.github/images/dashboard.png b/.github/images/dashboard.png new file mode 100644 index 000000000..c857550ff Binary files /dev/null and b/.github/images/dashboard.png differ diff --git a/.github/images/deploy-to-aws.png b/.github/images/deploy-to-aws.png new file mode 100644 index 000000000..f106e169d Binary files /dev/null and b/.github/images/deploy-to-aws.png differ diff --git a/.github/workflows/build_test_pr.yml b/.github/workflows/build_test_pr.yml index 6c394b530..b00470cca 100644 --- a/.github/workflows/build_test_pr.yml +++ b/.github/workflows/build_test_pr.yml @@ -33,6 +33,8 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: + cache: true + cache-dependency-path: go.sum go-version: 1.20.2 # FIXME: https://github.com/golangci/golangci-lint-action/issues/677 diff --git a/README.md b/README.md index a23d8f8d4..e4895ab66 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,21 @@

Discord | - Discussions | - Site

+ Tailwarden Cloud | + Website

Guide | How to Komiser | - Docs

+ Community Events

Contribute | Roadmap

+

+ + + +

+ [![Price](https://img.shields.io/badge/price-FREE-0098f7.svg)](https://github.com/tailwarden/komiser/blob/master/LICENSE) [![Docker Stars](https://img.shields.io/docker/pulls/mlabouardy/komiser.svg)](https://hub.docker.com/r/mlabouardy/komiser) [![ELv2 License](https://img.shields.io/badge/license-ELv2-green)](LICENSE) [![Docker Stars](https://img.shields.io/github/issues/tailwarden/komiser.svg)](https://github.com/tailwarden/komiser/issues) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.tailwarden.com/) @@ -22,7 +28,7 @@ Komiser is an open-source cloud-agnostic resource manager. It integrates with mu

Komiser gif

-*Cloud version is available in private beta, sign in for free at [https://cloud.tailwarden.com](https://cloud.tailwarden.com?utm_source=github&utm_medium=social)* +The fastest and most reliable way to get started with Komiser is signing up for free to [Tailwarden Cloud](https://cloud.tailwarden.com?utm_source=github&utm_medium=social) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40Komiser)](https://twitter.com/komiserdotio) [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Optimize%20Cost%20and%20Security%20on%20AWS&url=https://github.com/tailwarden/komiser&via=mlabouardy&hashtags=komiser,aws,gcp,cloud,serverless,devops) @@ -48,6 +54,7 @@ brew install komiser - [Deploy Komiser to single account access EKS cluster (Helm chart)](#deploy-komiser-to-single-account-access-eks-cluster-helm-chart) - [Deploy Komiser to a multi account access EKS cluster (Helm chart)](#deploy-komiser-to-a-multi-account-access-eks-cluster-helm-chart) - [Installation on Azure](#installation-on-azure) + - [Installation on GCP](#installation-on-gcp) - [Installation on Civo](#installation-on-civo) - [Installation on OCI](#installation-on-oci) - [Installation on Digital Ocean](#installation-on-digital-ocean) @@ -58,7 +65,6 @@ brew install komiser - [Jump right in:](#jump-right-in) - [Bugs and feature requests 🐞](#bugs-and-feature-requests-) - [Roadmap and Contributing 🛣️](#roadmap-and-contributing-️) - - [Watch! :](#watch-) - [Users 🧑‍🤝‍🧑](#users-) - [Versioning 🧮](#versioning-) - [Contributors](#contributors) @@ -67,11 +73,13 @@ brew install komiser # What is Komiser? 🤷 Komiser is an open source project created to **analyse** and **manage cloud cost**, **usage**, **security** and **governance** all in one place. With komiser you can also: +* Build an inventory of your cloud infrastructure assets. * Control your **resource usage** and gain visibility across all used services to achieve maximum cost-effectiveness. * Detect **potential vulnerabilities** that could put your cloud environment at risk. -* Get a deep understanding of **how you spend** on the AWS, Civo, OVH, DigitalOcean and OCI. +* Get a deep understanding of **how you spend** on the AWS, Azure, GCP, Civo, OVH, DigitalOcean and OCI. +* Uncover idle and untagged resources, ensuring that no resource goes unnoticed. -

Amp Logo

+

Komiser dashboard

## Who is using it? Komiser was built with every Cloud Engineer, Developer, DevOps engineer and SRE in mind. We understand that tackling cost savings, security improvements and resource usage analyse efforts can be hard, sometimes just knowing where to start, can be the most challenging part at times. Komiser is here to help those cloud practitioners see their cloud resources and accounts much more clearly. Only with clear insight can timely and efficient actions take place. @@ -100,6 +108,10 @@ Watch the installation [video here](https://www.youtube.com/watch?v=4veDmJpui44& Connect a local deployment of Komiser CLI to you [**Azure**](https://docs.komiser.io/docs/cloud-providers/azure?utm_source=github&utm_medium=social) account. +## Installation on GCP + +Connect a local deployment of Komiser CLI to you [**GCP**](https://docs.komiser.io/docs/cloud-providers/google-cloud-platform?utm_source=github&utm_medium=social) account. + ## Installation on Civo Connect a local deployment of Komiser CLI to your [**Civo**](https://docs.komiser.io/docs/cloud-providers/civo?utm_source=github&utm_medium=social) account. @@ -128,7 +140,7 @@ Connect a local deployment of Komiser CLI to you [**Scaleway**](https://docs.kom # Documentation 📖 -Head over to the official `Komiser` documentation at [docs.komiser.io](https://docs.komiser.io?utm_source=github&utm_medium=social). The source repository for the documentation website is [tailwarden/docs](https://github.com/tailwarden/docs). +Head over to the official `Komiser` documentation at [docs.komiser.io](https://docs.komiser.io?utm_source=github&utm_medium=social). The source repository for the documentation website is [tailwarden/docs.komiser.io](https://github.com/tailwarden/docs.komiser.io). We know that writing docs isn't usually at the top of too many peoples "What I like to do for fun" list, but if you feel so inclined, by all means, consider [contributing](https://docs.komiser.io/docs/contributing/contribute?utm_source=github&utm_medium=social) to our documentation repository, we will be very grateful. It's built using [Docusaurus](https://docusaurus.io/). @@ -146,12 +158,14 @@ Have a bug or a feature request? Please first read the issue guidelines and sear # Roadmap and Contributing 🛣️ -We are very excited about what is in store in the coming weeks and months, take a look at the [public roadmap](https://tailwarden.canny.io/) to stay on top of what's coming down the pipeline. +We are very excited about what is in store in the coming weeks and months, take a look at the [public roadmap](https://roadmap.tailwarden.com/) to stay on top of what's coming down the pipeline. Komiser is written in `Golang` and is `Elv2 licensed` - contributions are always welcome whether that means providing feedback, be it through GitHub, through the `#feedback` channel on our [Discord server](https://discord.tailwarden.com) or testing existing and new features. Feel free to check out our [contributor guidelines](./CONTRIBUTING.md) and consider becoming a **contributor** today. -### Watch! : -Learn how to contribute with this walkthrough [video](https://www.youtube.com/watch?v=Vn5uc2elcVg) +Learn how to contribute with this walkthrough videos: + +- [How to contributor to Komiser engine](https://www.youtube.com/watch?v=Vn5uc2elcVg) +- [How to contributor to Komiser dashboard](https://www.youtube.com/watch?v=uwxj11-eRt8) # Users 🧑‍🤝‍🧑 diff --git a/VERSION.md b/VERSION.md new file mode 100644 index 000000000..1d4ae4a59 --- /dev/null +++ b/VERSION.md @@ -0,0 +1,43 @@ +# Komiser Versioning Guidelines + +We use the [**SemVer**](https://semver.org/) versioning and maintain our own We follow the guidelines of Semantic Versioning (SemVer) and maintain our own standards on top of it. + +## For a Komiser version x.y.z: + +> X: Major | Y: Minor | Z: Patch + +### Patch releases include + +Patch releases incorporate performance and UX improving features without affecting the state in the forms of: + +- Minute bug patches +- Cost coverage enhancements +- Supporting new cloud resources in the release that are backward compatible +- Any changes do **NOT** involve dealing with + - Persistent state + - Database or model changes + +### Minor releases include + +Minor releases incorporate significant features that maintain backward compatibility by: + +- Keeping with the persistent state +- Handling logic for new as well old cases +- Database changes but have auto migrations that work with old models as well + +### Major releases include + +Major releases incorporate significant changes that fall into two main categories: + +- Breaking change that is **NOT** backward compatible +- Huge enhancements that might require a lot of refactoring + +> ⛔ We prioritize simplicity in our versioning approach and generally avoid the use of other somewhat complicated versioning labels such as `alpha, beta, and rc`. +> +> However, if necessary, we are open to utilizing these labels as well to ensure the most appropriate versioning for our releases. + +### Release Schedules + +Our ideal release schedule aims to have regular releases once every 2 weeks. The version format will follow the guidelines mentioned above, incorporating the principles of Semantic Versioning (SemVer) and our own additional standards. + +This systematic approach ensures clarity and consistency in our versioning process, making it easier for users and enterprises to understand the significance of each release and determine when to upgrade. diff --git a/dashboard/components/icons/ClearFilterIcon.tsx b/dashboard/components/icons/ClearFilterIcon.tsx new file mode 100644 index 000000000..f09449fc7 --- /dev/null +++ b/dashboard/components/icons/ClearFilterIcon.tsx @@ -0,0 +1,39 @@ +import { SVGProps } from 'react'; + +const ClearFilterIcon = (props: SVGProps) => ( + + + + + +); + +export default ClearFilterIcon; diff --git a/dashboard/components/inventory/components/InventoryActiveFilters.tsx b/dashboard/components/inventory/components/InventoryActiveFilters.tsx index 74198713f..cc6653b7f 100644 --- a/dashboard/components/inventory/components/InventoryActiveFilters.tsx +++ b/dashboard/components/inventory/components/InventoryActiveFilters.tsx @@ -1,3 +1,4 @@ +import { ReactNode } from 'react'; import { NextRouter } from 'next/router'; import Button from '../../button/Button'; import { InventoryFilterData } from '../hooks/useInventory/types/useInventoryTypes'; @@ -6,6 +7,7 @@ import PlusIcon from '../../icons/PlusIcon'; import useFilterWizard from './filter/hooks/useFilterWizard'; import useInventory from '../hooks/useInventory/useInventory'; import InventoryFilterDropdown from './InventoryFilterDropdown'; +import ClearFilterIcon from '../../icons/ClearFilterIcon'; type InventoryActiveFiltersProps = { hasFilters: boolean | undefined; @@ -13,6 +15,7 @@ type InventoryActiveFiltersProps = { isNotCustomView: boolean; deleteFilter: (idx: number) => void; router: NextRouter; + children?: ReactNode; }; function InventoryActiveFilters({ @@ -20,15 +23,33 @@ function InventoryActiveFilters({ displayedFilters, isNotCustomView, deleteFilter, - router + router, + children }: InventoryActiveFiltersProps) { const { setSkippedSearch } = useInventory(); const { toggle, isOpen } = useFilterWizard({ router, setSkippedSearch }); return ( - <> - {hasFilters && ( -
+
+ {!hasFilters ? ( + <> +
+ + Filter +
+ {isOpen && ( + + )} + + ) : ( +
Filters
{displayedFilters && displayedFilters.map((activeFilter, idx) => ( @@ -61,46 +82,16 @@ function InventoryActiveFilters({ style="ghost" onClick={() => router.push(router.pathname)} > - - - - - + Clear filters
)}
)} - + + {children} +
); } diff --git a/dashboard/components/inventory/components/InventoryHeader.tsx b/dashboard/components/inventory/components/InventoryHeader.tsx index 127dc37c4..1cf5390b0 100644 --- a/dashboard/components/inventory/components/InventoryHeader.tsx +++ b/dashboard/components/inventory/components/InventoryHeader.tsx @@ -2,10 +2,9 @@ import { ReactNode } from 'react'; type InventoryHeaderProps = { isNotCustomView: boolean; - children: ReactNode; }; -function InventoryHeader({ isNotCustomView, children }: InventoryHeaderProps) { +function InventoryHeader({ isNotCustomView }: InventoryHeaderProps) { return (
{isNotCustomView && ( @@ -13,7 +12,6 @@ function InventoryHeader({ isNotCustomView, children }: InventoryHeaderProps) { All Resources

)} -
{children}
); } diff --git a/dashboard/components/inventory/components/filter/InventoryFilter.tsx b/dashboard/components/inventory/components/filter/InventoryFilter.tsx deleted file mode 100644 index abfa3f0cd..000000000 --- a/dashboard/components/inventory/components/filter/InventoryFilter.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { NextRouter } from 'next/router'; -import Button from '../../../button/Button'; -import useFilterWizard from './hooks/useFilterWizard'; -import FilterIcon from '../../../icons/FilterIcon'; -import InventoryFilterDropdown from '../InventoryFilterDropdown'; - -type InventoryFilterProps = { - router: NextRouter; - setSkippedSearch: (number: number) => void; -}; - -function InventoryFilter({ router, setSkippedSearch }: InventoryFilterProps) { - const { toggle, isOpen } = useFilterWizard({ router, setSkippedSearch }); - - return ( -
- - - {/* Dropdown open */} - {isOpen && ( - - )} -
- ); -} - -export default InventoryFilter; diff --git a/dashboard/components/inventory/components/view/InventoryView.tsx b/dashboard/components/inventory/components/view/InventoryView.tsx index fe6f65043..1cc5f0ed7 100644 --- a/dashboard/components/inventory/components/view/InventoryView.tsx +++ b/dashboard/components/inventory/components/view/InventoryView.tsx @@ -5,7 +5,6 @@ import providers, { Provider } from '../../../../utils/providerHelper'; import Button from '../../../button/Button'; import Checkbox from '../../../checkbox/Checkbox'; import AlertIcon from '../../../icons/AlertIcon'; -import BookmarkIcon from '../../../icons/BookmarkIcon'; import Input from '../../../input/Input'; import Sidepanel from '../../../sidepanel/Sidepanel'; import SidepanelHeader from '../../../sidepanel/SidepanelHeader'; @@ -87,7 +86,7 @@ function InventoryView({ {/* Alerts button */} {router.query.view && ( -
+
+
openModal(filters)} + className="cursor-pointer font-sans text-[14px] font-semibold text-komiser-600" + > + Save as view +
)} {/* Sidepanel */} diff --git a/dashboard/components/inventory/components/view/InventoryViewHeader.tsx b/dashboard/components/inventory/components/view/InventoryViewHeader.tsx index e51b9bb6c..b51e18cdb 100644 --- a/dashboard/components/inventory/components/view/InventoryViewHeader.tsx +++ b/dashboard/components/inventory/components/view/InventoryViewHeader.tsx @@ -72,7 +72,7 @@ function InventoryViewHeader({ ); return ( -
+
{currentView && ( <>
diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index c128115ff..45d6a659f 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -17378,9 +17378,10 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.2", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -18177,9 +18178,10 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } diff --git a/dashboard/package.json b/dashboard/package.json index 6897ce92d..e484b8173 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "komiser-dashboard", - "version": "3.0.20", + "version": "3.1.0", "private": true, "scripts": { "dev": "next dev -p 3002", diff --git a/dashboard/pages/inventory.tsx b/dashboard/pages/inventory.tsx index 03a9c30ec..5203c7c6e 100644 --- a/dashboard/pages/inventory.tsx +++ b/dashboard/pages/inventory.tsx @@ -8,7 +8,6 @@ import InventoryLayout from '../components/inventory/components/InventoryLayout' import InventorySidePanel from '../components/inventory/components/InventorySidePanel'; import InventoryStatsCards from '../components/inventory/components/InventoryStatsCards'; import InventoryTable from '../components/inventory/components/InventoryTable'; -import InventoryFilter from '../components/inventory/components/filter/InventoryFilter'; import InventoryView from '../components/inventory/components/view/InventoryView'; import useInventory from '../components/inventory/hooks/useInventory/useInventory'; import SkeletonFilters from '../components/skeleton/SkeletonFilters'; @@ -87,45 +86,35 @@ export default function Inventory() { inventory={inventory} searchedInventory={searchedInventory} > - - {/* Custom view header and view management sidepanel */} - {hasFilterOrCustomView && ( - - )} - - {/* Filter component */} - {displayFilterIfIsNotCustomView && ( - - )} - - - + {/* Active filters skeleton */} {loadingFilters && } - {/* Active filters */} - - + {/* Filters bar containing active filters and view button */} +
+ + {hasFilterOrCustomView && ( + + )} + +
{/* Inventory stats skeleton */} {!error && statsLoading && ( diff --git a/providers/aws/aws.go b/providers/aws/aws.go index fd3698927..bab2af836 100644 --- a/providers/aws/aws.go +++ b/providers/aws/aws.go @@ -54,6 +54,7 @@ func listOfSupportedServices() []providers.FetchDataFunction { cloudfront.Distributions, dynamodb.Tables, ecs.Clusters, + ecs.TaskDefinitions, ecs.ContainerInstances, ecr.Repositories, sns.Topics, @@ -66,6 +67,7 @@ func listOfSupportedServices() []providers.FetchDataFunction { rds.Snapshots, rds.ClusterSnapshots, rds.ProxyEndpoints, + rds.AutoBackups, elb.LoadBalancers, efs.ElasticFileStorage, apigateway.Apis, diff --git a/providers/aws/ec2/instances.go b/providers/aws/ec2/instances.go index 88e0fbc19..17a95fab8 100644 --- a/providers/aws/ec2/instances.go +++ b/providers/aws/ec2/instances.go @@ -126,7 +126,7 @@ func Instances(ctx context.Context, client providers.ProviderClient) ([]models.R //log.Printf("Hourly cost EC2: %f", hourlyCost) - } + } monthlyCost = float64(hourlyUsage) * hourlyCost diff --git a/providers/aws/ecs/taskDefinitions.go b/providers/aws/ecs/taskDefinitions.go new file mode 100644 index 000000000..c04d58f75 --- /dev/null +++ b/providers/aws/ecs/taskDefinitions.go @@ -0,0 +1,66 @@ +package ecs + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ecs" + log "github.com/sirupsen/logrus" + . "github.com/tailwarden/komiser/models" + . "github.com/tailwarden/komiser/providers" +) + +func extractNameAndRevisionFromArn(input string) []string { + var nameAndRevision [2]string + + parts := strings.Split(input, ":") + if len(parts) >= 6 { + nameAndRevision[0] = strings.Split(parts[5], "/")[1] + nameAndRevision[1] = parts[6] + } + return nameAndRevision[:] +} + +func TaskDefinitions(ctx context.Context, client ProviderClient) ([]Resource, error) { + resources := make([]Resource, 0) + var config ecs.ListTaskDefinitionsInput + ecsClient := ecs.NewFromConfig(*client.AWSClient) + + for { + output, err := ecsClient.ListTaskDefinitions(context.Background(), &config) + if err != nil { + return resources, err + } + for _, taskdefinition := range output.TaskDefinitionArns { + ecsNameAndRevision := extractNameAndRevisionFromArn(taskdefinition) + resources = append(resources, Resource{ + Provider: "AWS", + Account: client.Name, + Service: "ECS Task Definition", + ResourceId: taskdefinition, + Region: client.AWSClient.Region, + Name: ecsNameAndRevision[0], + Cost: 0, + FetchedAt: time.Now(), + + Link: fmt.Sprintf("https://%s.console.aws.amazon.com/ecs/v2/task-definitions/%s/%s/containers?region=%s", client.AWSClient.Region, ecsNameAndRevision[0], ecsNameAndRevision[1], client.AWSClient.Region), + }) + } + + if aws.ToString(output.NextToken) == "" { + break + } + config.NextToken = output.NextToken + } + log.WithFields(log.Fields{ + "provider": "AWS", + "account": client.Name, + "region": client.AWSClient.Region, + "service": "ECS Task Definition", + "resources": len(resources), + }).Info("Fetched resources") + return resources, nil +} diff --git a/providers/aws/rds/auto_backups.go b/providers/aws/rds/auto_backups.go new file mode 100644 index 000000000..73c256622 --- /dev/null +++ b/providers/aws/rds/auto_backups.go @@ -0,0 +1,56 @@ +package rds + +import ( + "context" + "fmt" + "github.com/aws/aws-sdk-go-v2/aws" + "time" + + "github.com/aws/aws-sdk-go-v2/service/rds" + log "github.com/sirupsen/logrus" + "github.com/tailwarden/komiser/models" + "github.com/tailwarden/komiser/providers" +) + +func AutoBackups(ctx context.Context, client providers.ProviderClient) ([]models.Resource, error) { + var config rds.DescribeDBInstanceAutomatedBackupsInput + resources := make([]models.Resource, 0) + rdsClient := rds.NewFromConfig(*client.AWSClient) + + for { + output, err := rdsClient.DescribeDBInstanceAutomatedBackups(ctx, &config) + if err != nil { + return resources, err + } + + for _, backup := range output.DBInstanceAutomatedBackups { + + _backupName := *backup.DBInstanceIdentifier + + resources = append(resources, models.Resource{ + Provider: "AWS", + Account: client.Name, + Service: "RDS Backup", + Region: client.AWSClient.Region, + ResourceId: *backup.DBInstanceArn, + Name: _backupName, + FetchedAt: time.Now(), + Link: fmt.Sprintf("https:/%s.console.aws.amazon.com/rds/home?region=%s#dbinstance:id=%s", client.AWSClient.Region, client.AWSClient.Region, *backup.DBInstanceIdentifier), + }) + } + + if aws.ToString(output.Marker) == "" { + break + } + + config.Marker = output.Marker + } + log.WithFields(log.Fields{ + "provider": "AWS", + "account": client.Name, + "region": client.AWSClient.Region, + "service": "RDS Backup", + "resources": len(resources), + }).Info("Fetched resources") + return resources, nil +} diff --git a/providers/linode/linode.go b/providers/linode/linode.go index 0ccda90f2..2677d88c7 100644 --- a/providers/linode/linode.go +++ b/providers/linode/linode.go @@ -11,6 +11,7 @@ import ( "github.com/tailwarden/komiser/providers" "github.com/tailwarden/komiser/providers/linode/compute" "github.com/tailwarden/komiser/providers/linode/sql" + "github.com/tailwarden/komiser/providers/linode/postgres" "github.com/tailwarden/komiser/providers/linode/storage" "github.com/uptrace/bun" ) @@ -25,6 +26,7 @@ func listOfSupportedServices() []providers.FetchDataFunction { networking.NodeBalancers, networking.Firewalls, sql.Instances, + postgres.Instances, } } diff --git a/providers/linode/postgres/instances.go b/providers/linode/postgres/instances.go new file mode 100644 index 000000000..79dcc9b27 --- /dev/null +++ b/providers/linode/postgres/instances.go @@ -0,0 +1,133 @@ +package postgres + +import ( + "context" + "fmt" + "strings" + "time" + + // "github.com/linode/linodego" + log "github.com/sirupsen/logrus" + + "github.com/tailwarden/komiser/models" + "github.com/tailwarden/komiser/providers" +) + +// Cost data for Dedicated CPU instances +var dedicatedCPUCosts = map[string]float64{ + "Dedicated 4GB": 65.00, + "Dedicated 8GB": 130.00, + "Dedicated 16GB": 260.00, + "Dedicated 32GB": 520.00, + "Dedicated 64GB": 1040.00, + "Dedicated 96GB": 1560.00, + "Dedicated 128GB": 2080.00, + "Dedicated 256GB": 4160.00, + "Dedicated 512GB": 8320.00, +} + +// Cost data for Shared CPU instances +var sharedCPUCosts = map[string]float64{ + "Shared 1GB": 15.00, + "Shared 2GB": 30.00, + "Shared 4GB": 60.00, + "Shared 8GB": 120.00, + "Shared 16GB": 240.00, + "Shared 32GB": 480.00, + "Shared 64GB": 960.00, + "Shared 96GB": 1440.00, + "Shared 128GB": 1920.00, + "Shared 192GB": 2880.00, +} + +// PostgresInstances fetches PostgreSQL instances from the provider and returns them as resources. +func Instances(ctx context.Context, client providers.ProviderClient) ([]models.Resource, error) { + resources := make([]models.Resource, 0) + + // Fetch PostgreSQL databases from the Linode provider + databases, err := client.LinodeClient.ListPostgresDatabases(ctx, nil) + if err != nil { + return resources, err + } + + for _, database := range databases { + // Get the cluster size for the database + clusterSize, err := GetClusterSize(ctx, client, database.ID) + if err != nil { + log.Warnf("Failed to get cluster size for PostgreSQL database: %d, Error: %s", database.ID, err.Error()) + // Skip this database and continue with the next one + continue + } + + // Calculate the cost based on the database type and cluster size + cost, ok := InstancesCost(database.Type, clusterSize) + if !ok { + log.Warnf("Failed to calculate cost for PostgreSQL database: %d, Type: %s", database.ID, database.Type) + // Skip this database and continue with the next one + continue + } + + resources = append(resources, models.Resource{ + Provider: "Linode", + Account: client.Name, + Service: "PostgreSQL", + Region: database.Region, + ResourceId: fmt.Sprintf("%d", database.ID), + Cost: cost, + Name: database.Label, + FetchedAt: time.Now(), + CreatedAt: *database.Created, + Link: fmt.Sprintf("https://cloud.linode.com/databases/%d", database.ID), + }) + } + + log.WithFields(log.Fields{ + "provider": "Linode", + "account": client.Name, + "service": "PostgreSQL", + "resources": len(resources), + }).Info("Fetched resources") + return resources, nil +} + +// GetClusterSize retrieves the cluster size for a specific PostgreSQL instance. +func GetClusterSize(ctx context.Context, client providers.ProviderClient, instanceID int) (int, error) { + instance, err := client.LinodeClient.GetPostgresDatabase(ctx, instanceID) + if err != nil { + return 0, err + } + + return instance.ClusterSize, nil +} + +// InstancesCost calculates the cost for the given PostgreSQL instance type and cluster size. +func InstancesCost(instanceType string, clusterSize int) (float64, bool) { + // Calculate cost based on instance type + if strings.Contains(instanceType, "Dedicated") { + cost, ok := dedicatedCPUCosts[instanceType] + if !ok { + return 0, false + } + + // Adjust cost based on the cluster size + if clusterSize == 3 { + cost *= 3 + } + + return cost, true + } else if strings.Contains(instanceType, "Shared") { + cost, ok := sharedCPUCosts[instanceType] + if !ok { + return 0, false + } + + // Adjust cost for the cluster size + if clusterSize == 3 { + cost *= 2.333 + } + + return cost, true + } + + return 0, false +}