diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 06b73a145..76c73f943 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -108,33 +108,18 @@ The core Komiser Engine is written in Go (Golang) and leverages Go Modules. Here
2. 🔧 **GOPATH**:
- Ensure that the **`GOPATH`** environment variable is configured appropriately.
----
-
-## 🛠️ Komiser Installation
-
-### **Step 1: Installing Komiser CLI**
-Follow the instructions in the [documentation](https://docs.komiser.io/getting-started/installation) to install the **Komiser CLI** for your operating system.
-
-### **Step 2: Connect to a Cloud Account**
-To deploy a **self-hosted (local) instance** of Komiser, connect your Komiser CLI to a cloud account of your choice. Refer to the documentation of the [supported cloud providers](https://docs.komiser.io/configuration/cloud-providers/aws).
-
-### **Step 3: Accessing the Komiser UI**
-Access the dashboard UI at **`http://localhost:3002`** once the local Komiser instance is running.
-
-![komiser-dashboard](https://hackmd.io/_uploads/Syo0bMtgT.png)
-
----
## 🌟 Ways to Contribute to Komiser Engine
Komiser is an open-source cloud-agnostic resource manager. It helps you break down cloud resource costs at the resource level. As a cloud-agnostic cloud management tool, we always have more providers and cloud services to add, update, and cost-calculate.
-### 1️⃣ Adding a new Cloud Provider
-- Step 1: Create **`provider_name.go`** in **`providers/provider_name`** directory.
+### ☁️ Adding a new Cloud Provider
-- Step 2: Add the following boilerplate:
+#### 1️⃣ Create provider.
+Create `provider_name.go` in `providers/provider_name` directory.
+#### 2️⃣ Add the following boilerplate:
```go
package PROVIDER_NAME
@@ -164,7 +149,8 @@ func FetchProviderData(ctx context.Context, client ProviderClient, db *bun.DB) {
}
```
-- Step 3: Add SDK client details in [**`providers/provider.go`**](https://github.com/tailwarden/komiser/blob/develop/providers/providers.go):
+#### 3️⃣ Add SDK client details:
+Add your client details to [**`providers/provider.go`**](https://github.com/tailwarden/komiser/blob/develop/providers/providers.go)
```go
type ProviderClient struct {
@@ -188,7 +174,8 @@ type AzureClient struct {
}
```
-- **Step 4:** Add provider configuration in TOML format in **`config.toml`**:
+#### 4️⃣ Add provider configuration:
+Add provider configuration in TOML format in **`config.toml`**
```toml
[[gcp]]
@@ -198,27 +185,27 @@ source="ENVIRONMENT_VARIABLES"
profile="production"
```
-- **Step 5:** Compile a new Komiser binary:
-
+#### 5️⃣ Compile a new Komiser binary:
```bash
go build
```
-- **Step 6:** Start a new Komiser development server:
+#### 6️⃣ Start a new Komiser development server:
```bash
./komiser start
```
-### 2️⃣ Adding a new Cloud Service/Resource
+### 🔋 Adding a new Cloud Service/Resource
Here are the general steps to add a new service/resource for a cloud provider in Komiser:
-**Step 1:**
-Create a new file **`servicename.go`** under the path **`providers/provider_name/servicename`**
+#### 1️⃣ Create Service
+Create a new file `servicename.go` under the path `providers/provider_name/servicename`
-**Step 2:**
+#### 2️⃣ Add boilerplate
Add the following boilerplate code, which defines the structure of any new service/resource to be added for a cloud provider:
+
```go
package service
@@ -243,10 +230,10 @@ func MyServiceResources(ctx context.Context, client ProviderClient) ([]Resource,
To understand how to write the required logic, you may refer any [existing examples](https://github.com/tailwarden/komiser/tree/develop/providers/aws) for inspiration!
-**Step 3:**
-Call the **`MyServiceResources()`** function from the above file, by adding it to **`providers/providername/provider.go`** file's **`listOfSupportedServices()`** function.
+#### 3️⃣ Edit Provider
+Call the `MyServiceResources()` function from the above file, by adding it to `providers/providername/provider.go` file's `listOfSupportedServices()` function.
-```
+```go
func listOfSupportedServices() []providers.FetchDataFunction {
return []providers.FetchDataFunction{
ec2.Instances,
@@ -267,15 +254,40 @@ func listOfSupportedServices() []providers.FetchDataFunction {
.
```
-**Step 4:**
-Repeat steps **`4,5,6`** accordingly and you'll see a new resource/service added to Komiser, in the dashboard!
+#### 4️⃣
+Do above mentioned steps [4](#4️⃣-add-provider-configuration), [5](#5️⃣-compile-a-new-komiser-binary) and [6](#6️⃣-start-a-new-komiser-development-server). You'll see a new resource/service added to Komiser, in the dashboard!
Additionally, [here](https://youtu.be/Vn5uc2elcVg?feature=shared) is a video tutorial of the entire process for your reference.
+> 💡 Tip: you can also start the server via `go run *.go start --config ./config.toml` if you do want to skip the compile step!
+
### 3️⃣ Enhance existing Cloud service/resource
**So, you wish to improve the code quality of an existing cloud service/resource?** Feel free to discuss your ideas with us on our [Discord Server](https://discord.tailwarden.com) and [open a new issue](https://github.com/tailwarden/komiser/issues).
+## 🧪 Testing Your Changes
+
+We leverage the [testing](https://pkg.go.dev/testing) package for tests. Test names follow the `TestXxx(*testing.T)` format where Xxx does not start with a lowercase letter. The function name serves to identify the test routine.
+For creating a new test you create a `[name]_test.go` next to the file you'd like to test and replace `[name]` with your filename of the implementation. Look at any of the `*_test.go` files for an example or read the [official docs](https://pkg.go.dev/testing).
+You then can run it with `go test /path/to/your/folder/where/the/test/is`. You can run all of our engine tests with `make tests`. You should see something similar to this:
+
+```logtalk
+go test ./... | grep -v /dashboard/
+...
+ok github.com/tailwarden/komiser/internal (cached) [no tests to run]
+? github.com/tailwarden/komiser/providers/aws/ecr [no test files]
+? github.com/tailwarden/komiser/providers/aws/ecs [no test files]
+? github.com/tailwarden/komiser/providers/aws/efs [no test files]
+? github.com/tailwarden/komiser/providers/aws/eks [no test files]
+? github.com/tailwarden/komiser/providers/aws/elasticache [no test files]
+? github.com/tailwarden/komiser/providers/aws/elb [no test files]
+? github.com/tailwarden/komiser/providers/aws/iam [no test files]
+ok github.com/tailwarden/komiser/providers/aws/ec2 (cached)
+? github.com/tailwarden/komiser/providers/aws/kms [no test files]
+? github.com/tailwarden/komiser/providers/aws/lambda [no test file
+...
+```
+
# 🚀 Contributing to Komiser Dashboard UI
Komiser Dashboard utilizes a modern tech stack. Here's a brief about it:
diff --git a/Makefile b/Makefile
index 5489e56cc..eb45309ef 100644
--- a/Makefile
+++ b/Makefile
@@ -34,7 +34,7 @@ package:
## test: Run tests.
test:
- go test -v $(go list ./... | grep -v /dashboard/)
+ go test ./... | grep -v /dashboard/
## version: Show version.
version:
diff --git a/cmd/config.go b/cmd/config.go
index 33163c86c..f50eebdb2 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -16,7 +16,7 @@ var configCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
c := models.Config{
AWS: []models.AWSConfig{
- models.AWSConfig{
+ {
Name: "Demo",
Source: "CREDENTIALS_FILE",
Profile: "default",
diff --git a/dashboard/README.md b/dashboard/README.md
index 1b423fcb3..5c54c0f90 100644
--- a/dashboard/README.md
+++ b/dashboard/README.md
@@ -20,7 +20,7 @@ Follow the [Contribution Guide](https://github.com/tailwarden/komiser/blob/devel
From the Komiser root folder start the Komiser server by running:
```shell
-go run \*.go start --config /path/to/config.toml
+go run *.go start --config /path/to/config.toml
```
In a different terminal tab navigate to the `/dashboard` folder:
diff --git a/dashboard/components/explorer/DependencyGraph.tsx b/dashboard/components/explorer/DependencyGraph.tsx
index ccf97c6de..6b115e14e 100644
--- a/dashboard/components/explorer/DependencyGraph.tsx
+++ b/dashboard/components/explorer/DependencyGraph.tsx
@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, memo, useEffect } from 'react';
import CytoscapeComponent from 'react-cytoscapejs';
-import Cytoscape, { EventObject } from 'cytoscape';
+import Cytoscape, { EdgeSingular, EventObject } from 'cytoscape';
import popper from 'cytoscape-popper';
import nodeHtmlLabel, {
@@ -91,6 +91,7 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => {
const content = document.createElement('div');
content.classList.add('popper-div');
content.innerHTML = event.target.data('label');
+ content.style.pointerEvents = 'none';
document.body.appendChild(content);
return content;
@@ -108,6 +109,20 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => {
// Hide labels when being zoomed out
cy.on('zoom', event => {
+ if (cy.zoom() <= zoomLevelBreakpoint) {
+ interface ExtendedEdgeSingular extends EdgeSingular {
+ popperRefObj?: any;
+ }
+
+ // Check if a tooltip is present and remove it
+ cy.edges().forEach((edge: ExtendedEdgeSingular) => {
+ if (edge.popperRefObj) {
+ edge.popperRefObj.state.elements.popper.remove();
+ edge.popperRefObj.destroy();
+ }
+ });
+ }
+
const opacity = cy.zoom() <= zoomLevelBreakpoint ? 0 : 1;
Array.from(
@@ -126,7 +141,7 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => {
return (
-
{
}
]}
cy={(cy: Cytoscape.Core) => cyActionHandlers(cy)}
- />
+ /> */}
{dataIsEmpty ? (
<>
diff --git a/dashboard/components/explorer/filter/DependendencyGraphFilter.tsx b/dashboard/components/explorer/filter/DependendencyGraphFilter.tsx
index 5a1dd6624..906f85ef2 100644
--- a/dashboard/components/explorer/filter/DependendencyGraphFilter.tsx
+++ b/dashboard/components/explorer/filter/DependendencyGraphFilter.tsx
@@ -32,7 +32,7 @@ function DependendencyGraphFilter({
{!hasFilters ? (
<>
diff --git a/dashboard/components/feedback-widget/FeedbackWidget.tsx b/dashboard/components/feedback-widget/FeedbackWidget.tsx
index 97e659925..23ecb3567 100644
--- a/dashboard/components/feedback-widget/FeedbackWidget.tsx
+++ b/dashboard/components/feedback-widget/FeedbackWidget.tsx
@@ -1,5 +1,4 @@
import { useState, useRef, useCallback, memo, SyntheticEvent } from 'react';
-import { FileUploader } from 'react-drag-drop-files';
// eslint-disable-next-line import/no-extraneous-dependencies
import { toBlob } from 'html-to-image';
import Image from 'next/image';
@@ -8,8 +7,9 @@ import Modal from '@components/modal/Modal';
import Input from '@components/input/Input';
import settingsService from '@services/settingsService';
import Button from '@components/button/Button';
-import Toast from '@components/toast/Toast';
import { useToast } from '@components/toast/ToastProvider';
+import Toast from '@components/toast/Toast';
+import Upload from '@components/upload/Upload';
// We define the placeholder here for convenience
// It's difficult to read when passed inline
@@ -29,9 +29,7 @@ Outcome
const useFeedbackWidget = (defaultState: boolean = false) => {
const [showFeedbackModel, setShowFeedbackModal] = useState(defaultState);
- const FILE_TYPES = ['JPG', 'PNG', 'GIF', 'TXT', 'LOG', 'MP4', 'AVI', 'MOV'];
const FEEDBACK_MODAL_ID = 'feedback-modal';
- const MAX_FILE_SIZE_MB = 37;
function openFeedbackModal() {
setShowFeedbackModal(true);
@@ -151,7 +149,7 @@ const useFeedbackWidget = (defaultState: boolean = false) => {
}
}
- function uploadFile(attachement: File) {
+ function uploadFile(attachement: File | null): void {
setFileAttachement(attachement);
}
@@ -198,7 +196,7 @@ const useFeedbackWidget = (defaultState: boolean = false) => {
<>
takeScreenshot()}
- className="w-[50%] grow cursor-pointer rounded border-2 border-black-170 py-5 text-center text-xs transition hover:border-[#B6EAEA] hover:bg-black-100"
+ className="flex-1 grow cursor-pointer rounded border-2 border-black-170 py-5 text-center text-xs transition hover:border-[#B6EAEA] hover:bg-black-100"
>
{
>
)}
-
- showToast({
- hasError: true,
- title: 'File upload failed',
- message: err
- })
- }
- onSizeError={(err: string) =>
- showToast({
- hasError: true,
- title: 'File upload failed',
- message: err
- })
- }
- dropMessageStyle={{
- width: '100%',
- height: '100%',
- position: 'absolute',
- background: '#F4F9F9',
- top: 0,
- right: 2,
- display: 'flex',
- flexGrow: 2,
- opacity: 1,
- zIndex: 20,
- color: '#008484',
- fontSize: 14,
- border: 'none'
- }}
- >
- {fileAttachement === null && (
-
-
-
-
-
-
-
-
- Drag and drop or{' '}
-
- choose a file
-
-
-
- )}
- {fileAttachement !== null && (
-
- )}
-
+
+ setFileAttachement(null)}
+ disabled={
+ fileAttachement !== null ||
+ isSendingFeedback ||
+ isTakingScreenCapture
+ }
+ onTypeError={(err: string) =>
+ showToast({
+ hasError: true,
+ title: 'File upload failed',
+ message: err
+ })
+ }
+ onSizeError={(err: string) =>
+ showToast({
+ hasError: true,
+ title: 'File upload failed',
+ message: err
+ })
+ }
+ />
+
@@ -448,7 +290,12 @@ const useFeedbackWidget = (defaultState: boolean = false) => {
.
-
+ closeFeedbackModal()}
+ >
Cancel
diff --git a/dashboard/components/upload/Upload.stories.tsx b/dashboard/components/upload/Upload.stories.tsx
new file mode 100644
index 000000000..c92612300
--- /dev/null
+++ b/dashboard/components/upload/Upload.stories.tsx
@@ -0,0 +1,130 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { useEffect, useState } from 'react';
+import Upload, { UploadProps } from './Upload';
+
+function UploadWrapper({
+ multiple,
+ fileOrFiles,
+ handleChange,
+ onClose,
+ ...otherProps
+}: UploadProps) {
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [selectedFiles, setSelectedFiles] = useState(null);
+
+ useEffect(() => {
+ setSelectedFile(null);
+ setSelectedFiles(null);
+ }, [multiple]);
+
+ const uploadFile = (file: File | File[] | null): void => {
+ if (file instanceof FileList) {
+ const filesArray = Array.from(file);
+ setSelectedFiles(filesArray);
+ } else if (file instanceof File) {
+ setSelectedFile(file);
+ } else {
+ setSelectedFile(null);
+ setSelectedFiles(null);
+ }
+ };
+
+ return (
+ // it's impossible to define a true/false type in storybook
+ // so we ignore the next type error because true|false != boolean for some reason \o/
+ // @ts-ignore
+ multiple ? (
+ setSelectedFiles(null)}
+ {...otherProps}
+ />
+ ) : (
+ setSelectedFile(null)}
+ {...otherProps}
+ />
+ )
+ );
+}
+
+const meta: Meta = {
+ title: 'Komiser/Upload',
+ component: UploadWrapper,
+ decorators: [
+ Story => (
+ {Story()}
+ )
+ ],
+ tags: ['autodocs'],
+ argTypes: {
+ name: {
+ control: 'text',
+ description: 'the name for your form (if exist)',
+ defaultValue: 'attachment'
+ },
+ multiple: {
+ control: 'boolean',
+ description:
+ 'a boolean to determine whether the multiple files is enabled or not',
+ defaultValue: false
+ },
+ disabled: {
+ control: 'boolean',
+ description: 'disables the input',
+ defaultValue: false
+ },
+ required: {
+ control: 'boolean',
+ description: 'Conditionally set the input field as required',
+ defaultValue: false
+ },
+ hoverTitle: {
+ control: 'text',
+ description: 'text appears(hover) when trying to drop a file',
+ defaultValue: 'drop here'
+ },
+ maxSize: {
+ control: 'number',
+ description: 'the maximum size of the file (number in mb)',
+ defaultValue: 37
+ },
+ minSize: {
+ control: 'number',
+ description: 'the minimum size of the file (number in mb)',
+ defaultValue: 0
+ }
+ }
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const SingleFile: Story = {
+ args: {
+ name: 'attachment',
+ multiple: false,
+ disabled: false,
+ hoverTitle: 'drop here',
+ maxSize: 37,
+ minSize: 0
+ }
+};
+
+export const MultipleFiles: Story = {
+ args: {
+ name: 'attachment',
+ multiple: true,
+ disabled: false,
+ hoverTitle: 'drop here',
+ maxSize: 37,
+ minSize: 0
+ }
+};
diff --git a/dashboard/components/upload/Upload.tsx b/dashboard/components/upload/Upload.tsx
new file mode 100644
index 000000000..0fcd76d72
--- /dev/null
+++ b/dashboard/components/upload/Upload.tsx
@@ -0,0 +1,324 @@
+import React from 'react';
+import { FileUploader } from 'react-drag-drop-files';
+
+type BaseUploadProps = {
+ name?: string;
+ label?: string;
+ required?: boolean;
+ disabled?: boolean;
+ hoverTitle?: string;
+ classes?: string;
+ childClassName?: string;
+ types?: string[];
+ onTypeError?: (error: string) => void;
+ children?: any;
+ maxSize?: number;
+ minSize?: number;
+ onSizeError?: (error: string) => void;
+ onDrop?: (file: File | null) => void;
+ onSelect?: (file: File | null) => void;
+ onClose: () => void;
+ onDraggingStateChange?: () => void;
+ dropMessageStyle?: React.CSSProperties;
+};
+
+export type SingleUploadProps = BaseUploadProps & {
+ multiple: false;
+ fileOrFiles: File | null;
+ handleChange: (file: File | null) => void;
+};
+
+export type MultipleUploadProps = BaseUploadProps & {
+ multiple: true;
+ fileOrFiles: File[] | null;
+ handleChange: (files: File[] | null) => void;
+};
+
+export type UploadProps = SingleUploadProps | MultipleUploadProps;
+
+const FILE_TYPES = ['JPG', 'PNG', 'GIF', 'TXT', 'LOG', 'MP4', 'AVI', 'MOV'];
+const MAX_FILE_SIZE_MB = 37;
+
+const defaultDropMessageStyle: React.CSSProperties = {
+ width: '100%',
+ height: '100%',
+ position: 'absolute',
+ background: '#F4F9F9',
+ top: 0,
+ right: 2,
+ display: 'flex',
+ flexGrow: 2,
+ opacity: 1,
+ zIndex: 20,
+ color: '#008484',
+ fontSize: 14,
+ border: 'none'
+};
+
+function Upload({
+ name = 'attachment',
+ multiple = false,
+ label,
+ required,
+ disabled,
+ hoverTitle = 'drop here',
+ fileOrFiles,
+ handleChange,
+ classes,
+ childClassName,
+ types = FILE_TYPES,
+ onTypeError,
+ maxSize = MAX_FILE_SIZE_MB,
+ minSize,
+ onSizeError,
+ onDrop,
+ onSelect,
+ onClose,
+ onDraggingStateChange,
+ dropMessageStyle = defaultDropMessageStyle
+}: UploadProps) {
+ const defaultChildClassName =
+ fileOrFiles === null
+ ? `grow bg-white cursor-pointer rounded border-2 border-dashed border-black-170 py-5 text-center text-xs transition hover:border-[#B6EAEA] hover:bg-black-100 w-full`
+ : `grow bg-white min-h-full rounded border-2 border-[#B6EAEA] text-center text-xs transition w-full`;
+
+ return (
+
+
+ {fileOrFiles === null && (
+
+
+
+
+
+
+
+
+ Drag and drop or{' '}
+ choose a file
+
+
+ )}
+ {fileOrFiles !== null && (
+
+ {multiple ? (
+ (fileOrFiles as File[])?.map((file: File, index: number) => (
+
+ {/* Render the multiple files */}
+
+
+
+
+
+
+
+
+
{file.name}
+
+ {(file.size / (1024 * 1024)).toFixed(2)}
+ MB
+
+
+
+
+
+
+
+
+
+
+ ))
+ ) : (
+
+ {/* Render the single file */}
+
+
+
+
+
+
+
+
+
{(fileOrFiles as File).name}
+
+ {((fileOrFiles as File).size / (1024 * 1024)).toFixed(2)}
+ MB
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ )}
+
+
+ );
+}
+
+export default Upload;
diff --git a/dashboard/jest.setup.js b/dashboard/jest.setup.js
index 306452c53..f54f45557 100644
--- a/dashboard/jest.setup.js
+++ b/dashboard/jest.setup.js
@@ -3,4 +3,4 @@
// Used for __tests__/testing-library.js
// Learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom/extend-expect';
+import '@testing-library/jest-dom';
diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json
index 5feb2e605..d63afb53a 100644
--- a/dashboard/package-lock.json
+++ b/dashboard/package-lock.json
@@ -19,7 +19,7 @@
"cytoscape-node-html-label": "^1.2.2",
"cytoscape-popper": "^2.0.0",
"html-to-image": "^1.11.11",
- "next": "^13.5.4",
+ "next": "^13.5.6",
"next-transpile-modules": "^10.0.1",
"react": "18.2.0",
"react-chartjs-2": "^5.2.0",
@@ -42,12 +42,12 @@
"@storybook/preview-api": "^7.4.6",
"@storybook/react": "^7.4.6",
"@storybook/testing-library": "^0.2.2",
- "@storybook/theming": "^7.4.5",
- "@testing-library/jest-dom": "^5.16.5",
+ "@storybook/theming": "^7.5.1",
+ "@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.0.0",
"@types/cytoscape": "^3.19.11",
- "@types/cytoscape-popper": "^2.0.2",
- "@types/jest": "^29.4.0",
+ "@types/cytoscape-popper": "^2.0.3",
+ "@types/jest": "^29.5.6",
"@types/node": "20.7.0",
"@types/react": "18.2.22",
"@types/react-cytoscapejs": "^1.2.3",
@@ -3533,9 +3533,9 @@
}
},
"node_modules/@next/env": {
- "version": "13.5.4",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz",
- "integrity": "sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ=="
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz",
+ "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "13.5.4",
@@ -3567,9 +3567,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "13.5.4",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz",
- "integrity": "sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==",
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz",
+ "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==",
"cpu": [
"arm64"
],
@@ -3582,9 +3582,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "13.5.4",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz",
- "integrity": "sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==",
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz",
+ "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==",
"cpu": [
"x64"
],
@@ -3597,9 +3597,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "13.5.4",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz",
- "integrity": "sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==",
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz",
+ "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==",
"cpu": [
"arm64"
],
@@ -3612,9 +3612,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "13.5.4",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz",
- "integrity": "sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==",
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz",
+ "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==",
"cpu": [
"arm64"
],
@@ -3627,9 +3627,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "13.5.4",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz",
- "integrity": "sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==",
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz",
+ "integrity": "sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==",
"cpu": [
"x64"
],
@@ -3642,9 +3642,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "13.5.4",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz",
- "integrity": "sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==",
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz",
+ "integrity": "sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==",
"cpu": [
"x64"
],
@@ -3657,9 +3657,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "13.5.4",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz",
- "integrity": "sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==",
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz",
+ "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==",
"cpu": [
"arm64"
],
@@ -3672,9 +3672,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
- "version": "13.5.4",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz",
- "integrity": "sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==",
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz",
+ "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==",
"cpu": [
"ia32"
],
@@ -3687,9 +3687,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "13.5.4",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz",
- "integrity": "sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==",
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz",
+ "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==",
"cpu": [
"x64"
],
@@ -4857,6 +4857,26 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@storybook/addon-backgrounds/node_modules/@storybook/theming": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.4.6.tgz",
+ "integrity": "sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==",
+ "dev": true,
+ "dependencies": {
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@storybook/client-logger": "7.4.6",
+ "@storybook/global": "^5.0.0",
+ "memoizerific": "^1.11.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@storybook/addon-backgrounds/node_modules/@storybook/types": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.6.tgz",
@@ -5074,6 +5094,26 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@storybook/addon-controls/node_modules/@storybook/theming": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.4.6.tgz",
+ "integrity": "sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==",
+ "dev": true,
+ "dependencies": {
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@storybook/client-logger": "7.4.6",
+ "@storybook/global": "^5.0.0",
+ "memoizerific": "^1.11.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@storybook/addon-controls/node_modules/@storybook/types": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.6.tgz",
@@ -5225,6 +5265,26 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@storybook/addon-docs/node_modules/@storybook/theming": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.4.6.tgz",
+ "integrity": "sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==",
+ "dev": true,
+ "dependencies": {
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@storybook/client-logger": "7.4.6",
+ "@storybook/global": "^5.0.0",
+ "memoizerific": "^1.11.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@storybook/addon-docs/node_modules/@storybook/types": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.6.tgz",
@@ -5476,6 +5536,26 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@storybook/addon-essentials/node_modules/@storybook/theming": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.4.6.tgz",
+ "integrity": "sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==",
+ "dev": true,
+ "dependencies": {
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@storybook/client-logger": "7.4.6",
+ "@storybook/global": "^5.0.0",
+ "memoizerific": "^1.11.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@storybook/addon-essentials/node_modules/@storybook/types": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.6.tgz",
@@ -5859,6 +5939,26 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@storybook/addon-measure/node_modules/@storybook/theming": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.4.6.tgz",
+ "integrity": "sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==",
+ "dev": true,
+ "dependencies": {
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@storybook/client-logger": "7.4.6",
+ "@storybook/global": "^5.0.0",
+ "memoizerific": "^1.11.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@storybook/addon-measure/node_modules/@storybook/types": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.6.tgz",
@@ -6027,6 +6127,26 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@storybook/addon-outline/node_modules/@storybook/theming": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.4.6.tgz",
+ "integrity": "sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==",
+ "dev": true,
+ "dependencies": {
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@storybook/client-logger": "7.4.6",
+ "@storybook/global": "^5.0.0",
+ "memoizerific": "^1.11.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@storybook/addon-outline/node_modules/@storybook/types": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.6.tgz",
@@ -6192,6 +6312,26 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@storybook/addon-toolbars/node_modules/@storybook/theming": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.4.6.tgz",
+ "integrity": "sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==",
+ "dev": true,
+ "dependencies": {
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@storybook/client-logger": "7.4.6",
+ "@storybook/global": "^5.0.0",
+ "memoizerific": "^1.11.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@storybook/addon-toolbars/node_modules/@storybook/types": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.6.tgz",
@@ -6361,6 +6501,26 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@storybook/addon-viewport/node_modules/@storybook/theming": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.4.6.tgz",
+ "integrity": "sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==",
+ "dev": true,
+ "dependencies": {
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@storybook/client-logger": "7.4.6",
+ "@storybook/global": "^5.0.0",
+ "memoizerific": "^1.11.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@storybook/addon-viewport/node_modules/@storybook/types": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.6.tgz",
@@ -6490,6 +6650,26 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@storybook/addons/node_modules/@storybook/theming": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.4.6.tgz",
+ "integrity": "sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==",
+ "dev": true,
+ "dependencies": {
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@storybook/client-logger": "7.4.6",
+ "@storybook/global": "^5.0.0",
+ "memoizerific": "^1.11.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@storybook/addons/node_modules/@storybook/types": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.6.tgz",
@@ -6728,6 +6908,26 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@storybook/blocks/node_modules/@storybook/theming": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.4.6.tgz",
+ "integrity": "sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==",
+ "dev": true,
+ "dependencies": {
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@storybook/client-logger": "7.4.6",
+ "@storybook/global": "^5.0.0",
+ "memoizerific": "^1.11.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/storybook"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@storybook/blocks/node_modules/@storybook/types": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.4.6.tgz",
@@ -8906,13 +9106,13 @@
}
},
"node_modules/@storybook/theming": {
- "version": "7.4.6",
- "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.4.6.tgz",
- "integrity": "sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==",
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.5.1.tgz",
+ "integrity": "sha512-ETLAOn10hI4Mkmjsr0HGcM6HbzaURrrPBYmfXOrdbrzEVN+AHW4FlvP9d8fYyP1gdjPE1F39XvF0jYgt1zXiHQ==",
"dev": true,
"dependencies": {
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
- "@storybook/client-logger": "7.4.6",
+ "@storybook/client-logger": "7.5.1",
"@storybook/global": "^5.0.0",
"memoizerific": "^1.11.3"
},
@@ -8926,9 +9126,9 @@
}
},
"node_modules/@storybook/theming/node_modules/@storybook/client-logger": {
- "version": "7.4.6",
- "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.4.6.tgz",
- "integrity": "sha512-XDw31ZziU//86PKuMRnmc+L/G0VopaGKENQOGEpvAXCU9IZASwGKlKAtcyosjrpi+ZiUXlMgUXCpXM7x3b1Ehw==",
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.5.1.tgz",
+ "integrity": "sha512-XxbLvg0aQRoBrzxYLcVYCbjDkGbkU8Rfb74XbV2CLiO2bIbFPmA1l1Nwbp+wkCGA+O6Z1zwzSl6wcKKqZ6XZCg==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0"
@@ -9041,14 +9241,13 @@
}
},
"node_modules/@testing-library/jest-dom": {
- "version": "5.17.0",
- "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
- "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==",
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.1.4.tgz",
+ "integrity": "sha512-wpoYrCYwSZ5/AxcrjLxJmCU6I5QAJXslEeSiMQqaWmP2Kzpd1LvF/qxmAIW2qposULGWq2gw30GgVNFLSc2Jnw==",
"dev": true,
"dependencies": {
- "@adobe/css-tools": "^4.0.1",
+ "@adobe/css-tools": "^4.3.1",
"@babel/runtime": "^7.9.2",
- "@types/testing-library__jest-dom": "^5.9.1",
"aria-query": "^5.0.0",
"chalk": "^3.0.0",
"css.escape": "^1.5.1",
@@ -9057,9 +9256,29 @@
"redent": "^3.0.0"
},
"engines": {
- "node": ">=8",
+ "node": ">=14",
"npm": ">=6",
"yarn": ">=1"
+ },
+ "peerDependencies": {
+ "@jest/globals": ">= 28",
+ "@types/jest": ">= 28",
+ "jest": ">= 28",
+ "vitest": ">= 0.32"
+ },
+ "peerDependenciesMeta": {
+ "@jest/globals": {
+ "optional": true
+ },
+ "@types/jest": {
+ "optional": true
+ },
+ "jest": {
+ "optional": true
+ },
+ "vitest": {
+ "optional": true
+ }
}
},
"node_modules/@testing-library/jest-dom/node_modules/chalk": {
@@ -9233,9 +9452,9 @@
"devOptional": true
},
"node_modules/@types/cytoscape-popper": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/@types/cytoscape-popper/-/cytoscape-popper-2.0.2.tgz",
- "integrity": "sha512-iRa+XgJu5jBayPx3+fvlQXp6Fx25zDOBPYDGyKUJrcw2b7pIDMShHlh+G9g/BdwpfZYFMgiAoP6fYq0TEXmEKw==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/cytoscape-popper/-/cytoscape-popper-2.0.3.tgz",
+ "integrity": "sha512-YjwaWd0B+fDnGQIvLwwBdc7qamoAXQNXlBZeg5MEAUZIuMkRIiBZB8baBjeLMu4KA+QTCCa2R5ADUnggXty+Kg==",
"dev": true,
"dependencies": {
"@popperjs/core": "^2.0.0",
@@ -9420,9 +9639,9 @@
}
},
"node_modules/@types/jest": {
- "version": "29.5.5",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz",
- "integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==",
+ "version": "29.5.6",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.6.tgz",
+ "integrity": "sha512-/t9NnzkOpXb4Nfvg17ieHE6EeSjDS2SGSpNYfoLbUAeL/EOueU/RSdOWFpfQTXBEM7BguYW1XQ0EbM+6RlIh6w==",
"dev": true,
"dependencies": {
"expect": "^29.0.0",
@@ -9647,15 +9866,6 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
- "node_modules/@types/testing-library__jest-dom": {
- "version": "5.14.9",
- "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz",
- "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==",
- "dev": true,
- "dependencies": {
- "@types/jest": "*"
- }
- },
"node_modules/@types/tough-cookie": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz",
@@ -11268,20 +11478,23 @@
}
},
"node_modules/browserify-sign": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz",
- "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz",
+ "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==",
"dev": true,
"dependencies": {
- "bn.js": "^5.1.1",
- "browserify-rsa": "^4.0.1",
+ "bn.js": "^5.2.1",
+ "browserify-rsa": "^4.1.0",
"create-hash": "^1.2.0",
"create-hmac": "^1.1.7",
- "elliptic": "^6.5.3",
+ "elliptic": "^6.5.4",
"inherits": "^2.0.4",
- "parse-asn1": "^5.1.5",
- "readable-stream": "^3.6.0",
- "safe-buffer": "^5.2.0"
+ "parse-asn1": "^5.1.6",
+ "readable-stream": "^3.6.2",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 4"
}
},
"node_modules/browserify-sign/node_modules/readable-stream": {
@@ -18795,11 +19008,11 @@
"dev": true
},
"node_modules/next": {
- "version": "13.5.4",
- "resolved": "https://registry.npmjs.org/next/-/next-13.5.4.tgz",
- "integrity": "sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==",
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/next/-/next-13.5.6.tgz",
+ "integrity": "sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==",
"dependencies": {
- "@next/env": "13.5.4",
+ "@next/env": "13.5.6",
"@swc/helpers": "0.5.2",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406",
@@ -18814,15 +19027,15 @@
"node": ">=16.14.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "13.5.4",
- "@next/swc-darwin-x64": "13.5.4",
- "@next/swc-linux-arm64-gnu": "13.5.4",
- "@next/swc-linux-arm64-musl": "13.5.4",
- "@next/swc-linux-x64-gnu": "13.5.4",
- "@next/swc-linux-x64-musl": "13.5.4",
- "@next/swc-win32-arm64-msvc": "13.5.4",
- "@next/swc-win32-ia32-msvc": "13.5.4",
- "@next/swc-win32-x64-msvc": "13.5.4"
+ "@next/swc-darwin-arm64": "13.5.6",
+ "@next/swc-darwin-x64": "13.5.6",
+ "@next/swc-linux-arm64-gnu": "13.5.6",
+ "@next/swc-linux-arm64-musl": "13.5.6",
+ "@next/swc-linux-x64-gnu": "13.5.6",
+ "@next/swc-linux-x64-musl": "13.5.6",
+ "@next/swc-win32-arm64-msvc": "13.5.6",
+ "@next/swc-win32-ia32-msvc": "13.5.6",
+ "@next/swc-win32-x64-msvc": "13.5.6"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
diff --git a/dashboard/package.json b/dashboard/package.json
index 92f3cb6bb..06b1d6b48 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -26,7 +26,7 @@
"cytoscape-node-html-label": "^1.2.2",
"cytoscape-popper": "^2.0.0",
"html-to-image": "^1.11.11",
- "next": "^13.5.4",
+ "next": "^13.5.6",
"next-transpile-modules": "^10.0.1",
"react": "18.2.0",
"react-chartjs-2": "^5.2.0",
@@ -49,12 +49,12 @@
"@storybook/preview-api": "^7.4.6",
"@storybook/react": "^7.4.6",
"@storybook/testing-library": "^0.2.2",
- "@storybook/theming": "^7.4.5",
- "@testing-library/jest-dom": "^5.16.5",
+ "@storybook/theming": "^7.5.1",
+ "@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.0.0",
"@types/cytoscape": "^3.19.11",
- "@types/cytoscape-popper": "^2.0.2",
- "@types/jest": "^29.4.0",
+ "@types/cytoscape-popper": "^2.0.3",
+ "@types/jest": "^29.5.6",
"@types/node": "20.7.0",
"@types/react": "18.2.22",
"@types/react-cytoscapejs": "^1.2.3",
diff --git a/dashboard/styles/globals.css b/dashboard/styles/globals.css
index 796bc0a55..d4c2e6d9d 100644
--- a/dashboard/styles/globals.css
+++ b/dashboard/styles/globals.css
@@ -48,8 +48,14 @@
}
.popper-div {
- text-shadow: 0 0 5px #f4f9f9, 0 0 5px #f4f9f9, 0 0 5px #f4f9f9,
- 0 0 5px #f4f9f9, 0 0 5px #f4f9f9, 0 0 5px #f4f9f9, 0 0 5px #f4f9f9,
+ text-shadow:
+ 0 0 5px #f4f9f9,
+ 0 0 5px #f4f9f9,
+ 0 0 5px #f4f9f9,
+ 0 0 5px #f4f9f9,
+ 0 0 5px #f4f9f9,
+ 0 0 5px #f4f9f9,
+ 0 0 5px #f4f9f9,
0 0 5px #f4f9f9;
position: relative;
color: #000;
diff --git a/docs/configuration/cloud-providers/aws.mdx b/docs/configuration/cloud-providers/aws.mdx
index fc1a827b9..55c6411eb 100644
--- a/docs/configuration/cloud-providers/aws.mdx
+++ b/docs/configuration/cloud-providers/aws.mdx
@@ -9,6 +9,7 @@ sidebar_label: Amazon Web Services
- API Gateway
- Access control lists
- CloudFront distributions
+ - CloudFront functions
- CloudWatch Dashboards
- CloudWatch alarms
- CloudWatch metrics
diff --git a/go.mod b/go.mod
index 57e261ac8..0d76d3837 100644
--- a/go.mod
+++ b/go.mod
@@ -3,10 +3,10 @@ module github.com/tailwarden/komiser
go 1.21
require (
- cloud.google.com/go/bigquery v1.49.0
- cloud.google.com/go/compute v1.19.0
+ cloud.google.com/go/bigquery v1.50.0
+ cloud.google.com/go/compute v1.19.1
cloud.google.com/go/container v1.16.0
- cloud.google.com/go/kms v1.10.0
+ cloud.google.com/go/kms v1.10.1
cloud.google.com/go/monitoring v1.13.0
cloud.google.com/go/redis v1.11.0
cloud.google.com/go/storage v1.30.0
@@ -72,7 +72,7 @@ require (
github.com/uptrace/bun/driver/pgdriver v1.1.8
github.com/uptrace/bun/driver/sqliteshim v1.1.8
go.mongodb.org/atlas v0.23.1
- golang.org/x/oauth2 v0.6.0
+ golang.org/x/oauth2 v0.7.0
golang.org/x/text v0.13.0
google.golang.org/api v0.114.0
k8s.io/apimachinery v0.26.1
@@ -190,8 +190,8 @@ require (
golang.org/x/tools v0.6.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea // indirect
- google.golang.org/grpc v1.54.0 // indirect
+ google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
+ google.golang.org/grpc v1.56.3 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/go.sum b/go.sum
index 9b3826c96..b43994c55 100644
--- a/go.sum
+++ b/go.sum
@@ -1,12 +1,12 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
-cloud.google.com/go/bigquery v1.49.0 h1:yE+MpeFaRX9L3rYJrIxl1zCDnTU2kyTA2FkrFd6kVT8=
-cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q=
+cloud.google.com/go/bigquery v1.50.0 h1:RscMV6LbnAmhAzD893Lv9nXXy2WCaJmbxYPWDLbGqNQ=
+cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU=
cloud.google.com/go/certificatemanager v1.6.0 h1:5C5UWeSt8Jkgp7OWn2rCkLmYurar/vIWIoSQ2+LaTOc=
cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8=
-cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ=
-cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
+cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
+cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/container v1.16.0 h1:dALHCKAYowNhUeENL4pf3+5ToHuSiE2emTDnuzwn4kc=
@@ -15,8 +15,8 @@ cloud.google.com/go/datacatalog v1.13.0 h1:4H5IJiyUE0X6ShQBqgFFZvGGcrwGVndTwUSLP
cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8=
cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=
cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
-cloud.google.com/go/kms v1.10.0 h1:Imrtp8792uqNP9bdfPrjtUkjjqOMBcAJ2bdFaAnLhnk=
-cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24=
+cloud.google.com/go/kms v1.10.1 h1:7hm1bRqGCA1GBRQUrp831TwJ9TWhP+tvLuP497CQS2g=
+cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI=
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
cloud.google.com/go/monitoring v1.13.0 h1:2qsrgXGVoRXpP7otZ14eE1I568zAa92sJSDPyOJvwjM=
@@ -550,8 +550,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
-golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
+golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
+golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -628,15 +628,15 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea h1:yJv4O9/Q178wILoVkpoaERo7wMSIAqftxsa4y/5nP+8=
-google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
-google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
+google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
+google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
diff --git a/handlers/accounts_handler.go b/handlers/accounts_handler.go
index d7ebe5dad..9852c07a6 100644
--- a/handlers/accounts_handler.go
+++ b/handlers/accounts_handler.go
@@ -6,7 +6,9 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "os"
+ "github.com/BurntSushi/toml"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
@@ -19,6 +21,8 @@ import (
"github.com/uptrace/bun/driver/sqliteshim"
)
+var unsavedAccounts []models.Account
+
func (handler *ApiHandler) IsOnboardedHandler(c *gin.Context) {
output := struct {
Onboarded bool `json:"onboarded"`
@@ -29,7 +33,7 @@ func (handler *ApiHandler) IsOnboardedHandler(c *gin.Context) {
}
if handler.db == nil {
- output.Status = "PENDING_DATABASE"
+ output.Status = "PENDING_ACCOUNTS"
c.JSON(http.StatusOK, output)
return
}
@@ -53,6 +57,12 @@ func (handler *ApiHandler) IsOnboardedHandler(c *gin.Context) {
func (handler *ApiHandler) ListCloudAccountsHandler(c *gin.Context) {
accounts := make([]models.Account, 0)
+
+ if handler.db == nil {
+ c.JSON(http.StatusOK, unsavedAccounts)
+ return
+ }
+
err := handler.db.NewRaw("SELECT * FROM accounts").Scan(handler.ctx, &accounts)
if err != nil {
logrus.WithError(err).Error("scan failed")
@@ -88,14 +98,22 @@ func (handler *ApiHandler) NewCloudAccountHandler(c *gin.Context) {
return
}
- result, err := handler.db.NewInsert().Model(&account).Exec(context.Background())
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
+ if handler.db == nil {
+ if len(unsavedAccounts) == 0 {
+ unsavedAccounts = make([]models.Account, 0)
+ }
+
+ unsavedAccounts = append(unsavedAccounts, account)
+ } else {
+ result, err := handler.db.NewInsert().Model(&account).Exec(context.Background())
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
- accountId, _ := result.LastInsertId()
- account.Id = accountId
+ accountId, _ := result.LastInsertId()
+ account.Id = accountId
+ }
if handler.telemetry {
handler.analytics.TrackEvent("creating_alert", map[string]interface{}{
@@ -147,6 +165,8 @@ func (handler *ApiHandler) ConfigureDatabaseHandler(c *gin.Context) {
return
}
+ config := models.Config{}
+
if db.Type == "SQLITE" {
sqldb, err := sql.Open(sqliteshim.ShimName, fmt.Sprintf("file:%s?cache=shared", db.FilePath))
if err != nil {
@@ -158,12 +178,43 @@ func (handler *ApiHandler) ConfigureDatabaseHandler(c *gin.Context) {
handler.db = bun.NewDB(sqldb, sqlitedialect.New())
log.Println("Data will be stored in SQLite")
+
+ config.SQLite = models.SQLiteConfig{
+ File: db.FilePath,
+ }
} else {
uri := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", db.Username, db.Password, db.Hostname, db.Database)
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(uri)))
handler.db = bun.NewDB(sqldb, pgdialect.New())
log.Println("Data will be stored in PostgreSQL")
+
+ config.Postgres = models.PostgresConfig{
+ URI: uri,
+ }
+ }
+
+ if len(unsavedAccounts) > 0 {
+ if len(handler.accounts) == 0 {
+ handler.accounts = unsavedAccounts
+ } else {
+ handler.accounts = append(handler.accounts, unsavedAccounts...)
+ }
+ unsavedAccounts = make([]models.Account, 0)
+
+ f, err := os.Create("config.toml")
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ if err := toml.NewEncoder(f).Encode(config); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ if err := f.Close(); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
}
err = utils.SetupSchema(handler.db, &handler.cfg, handler.accounts)
diff --git a/handlers/resources_handler.go b/handlers/resources_handler.go
index 501e47c3e..cde3cdc5a 100644
--- a/handlers/resources_handler.go
+++ b/handlers/resources_handler.go
@@ -429,3 +429,16 @@ func (handler *ApiHandler) RelationStatsHandler(c *gin.Context) {
c.JSON(http.StatusOK, out)
}
+
+func (handler *ApiHandler) GetResourceByIdHandler(c *gin.Context) {
+ resourceId := c.Query("resourceId")
+
+ var resource Resource
+
+ err := handler.db.NewSelect().Model(&resource).Where("resource_id = ?", resourceId).Scan(handler.ctx)
+ if err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
+ }
+
+ c.JSON(http.StatusOK, resource)
+}
diff --git a/internal/api/v1/endpoints.go b/internal/api/v1/endpoints.go
index 179350292..d4dce98aa 100644
--- a/internal/api/v1/endpoints.go
+++ b/internal/api/v1/endpoints.go
@@ -28,6 +28,7 @@ func Endpoints(ctx context.Context, telemetry bool, analytics utils.Analytics, d
router.GET("/resources/export-csv", api.DownloadInventoryCSV)
router.GET("/resources/export-csv/:viewId", api.DownloadInventoryCSVForView)
router.POST("/resources/relations", api.RelationStatsHandler)
+ router.GET("/resources", api.GetResourceByIdHandler)
router.GET("/views", api.ListViewsHandler)
router.POST("/views", api.NewViewHandler)
diff --git a/mocks/cloudwatch.go b/mocks/cloudwatch.go
new file mode 100644
index 000000000..1e34799c6
--- /dev/null
+++ b/mocks/cloudwatch.go
@@ -0,0 +1,20 @@
+package mocks
+
+import (
+ "context"
+
+ "github.com/aws/aws-sdk-go-v2/service/cloudwatch"
+ "github.com/stretchr/testify/mock"
+)
+
+type CloudwatchClient struct {
+ mock.Mock
+}
+
+func (_m *CloudwatchClient) GetMetricStatistics(ctx context.Context, input *cloudwatch.GetMetricStatisticsInput, opt ...func(*cloudwatch.Options)) (*cloudwatch.GetMetricStatisticsOutput, error) {
+ ret := _m.Called(ctx, input, opt)
+ if ret.Get(1) == nil {
+ return ret.Get(0).(*cloudwatch.GetMetricStatisticsOutput), nil
+ }
+ return nil, ret.Get(1).(error)
+}
diff --git a/mocks/pricing.go b/mocks/pricing.go
new file mode 100644
index 000000000..4aff44e03
--- /dev/null
+++ b/mocks/pricing.go
@@ -0,0 +1,20 @@
+package mocks
+
+import (
+ "context"
+
+ "github.com/aws/aws-sdk-go-v2/service/pricing"
+ "github.com/stretchr/testify/mock"
+)
+
+type PricingClient struct {
+ mock.Mock
+}
+
+func (_m *PricingClient) GetProducts(ctx context.Context, input *pricing.GetProductsInput, opt ...func(*pricing.Options)) (*pricing.GetProductsOutput, error) {
+ ret := _m.Called(ctx, input, opt)
+ if ret.Get(1) == nil {
+ return ret.Get(0).(*pricing.GetProductsOutput), nil
+ }
+ return nil, ret.Get(1).(error)
+}
diff --git a/policy.json b/policy.json
index cd321e529..b5f6d1852 100644
--- a/policy.json
+++ b/policy.json
@@ -8,6 +8,7 @@
"apigateway:GET",
"cloudwatch:GetMetricStatistics",
"cloudfront:ListDistributions",
+ "cloudfront:Functions",
"cloudfront:ListTagsForResource",
"cloudwatch:DescribeAlarms",
"cloudwatch:ListTagsForResource",
diff --git a/providers/aws/aws.go b/providers/aws/aws.go
index 8e2f7df02..aa0ba4338 100644
--- a/providers/aws/aws.go
+++ b/providers/aws/aws.go
@@ -58,6 +58,7 @@ func listOfSupportedServices() []providers.FetchDataFunction {
ec2.Instances,
eks.KubernetesClusters,
cloudfront.Distributions,
+ cloudfront.Functions,
dynamodb.Tables,
ecs.Clusters,
ecs.TaskDefinitions,
@@ -107,7 +108,12 @@ func FetchResources(ctx context.Context, client providers.ProviderClient, region
}
for _, region := range listOfSupportedRegions {
- client.AWSClient.Region = region
+ c := client.AWSClient.Copy()
+ c.Region = region
+ client = providers.ProviderClient{
+ AWSClient: &c,
+ Name: client.Name,
+ }
for _, fetchResources := range listOfSupportedServices() {
wp.SubmitTask(func() {
resources, err := fetchResources(ctx, client)
diff --git a/providers/aws/cloudfront/functions.go b/providers/aws/cloudfront/functions.go
new file mode 100644
index 000000000..787017505
--- /dev/null
+++ b/providers/aws/cloudfront/functions.go
@@ -0,0 +1,115 @@
+package cloudfront
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/cloudfront"
+ "github.com/aws/aws-sdk-go-v2/service/cloudwatch"
+ "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
+ . "github.com/tailwarden/komiser/models"
+ . "github.com/tailwarden/komiser/providers"
+ "github.com/tailwarden/komiser/utils"
+)
+
+const (
+ freeTierInvocations = 2000000
+ costPerInvocation = 0.0000001
+)
+
+func Functions(ctx context.Context, client ProviderClient) ([]Resource, error) {
+ resources := make([]Resource, 0)
+ var config cloudfront.ListFunctionsInput
+ cloudfrontClient := cloudfront.NewFromConfig(*client.AWSClient)
+
+ tempRegion := client.AWSClient.Region
+ client.AWSClient.Region = "us-east-1"
+ cloudwatchClient := cloudwatch.NewFromConfig(*client.AWSClient)
+ client.AWSClient.Region = tempRegion
+
+ for {
+ output, err := cloudfrontClient.ListFunctions(ctx, &config)
+ if err != nil {
+ return resources, err
+ }
+
+ for _, function := range output.FunctionList.Items {
+ metricsInvocationsOutput, err := cloudwatchClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{
+ StartTime: aws.Time(utils.BeginningOfMonth(time.Now())),
+ EndTime: aws.Time(time.Now()),
+ MetricName: aws.String("FunctionInvocations"),
+ Namespace: aws.String("AWS/CloudFront"),
+ Dimensions: []types.Dimension{
+ types.Dimension{
+ Name: aws.String("FunctionName"),
+ Value: function.Name,
+ },
+ },
+ Period: aws.Int32(3600),
+ Statistics: []types.Statistic{
+ types.StatisticSum,
+ },
+ })
+
+ if err != nil {
+ log.Warnf("Couldn't fetch invocations metric for %s", *function.Name)
+ return resources, err
+ }
+
+ invocations := 0.0
+ if metricsInvocationsOutput != nil && len(metricsInvocationsOutput.Datapoints) > 0 {
+ invocations = *metricsInvocationsOutput.Datapoints[0].Sum
+ }
+ if invocations > freeTierInvocations {
+ invocations -= freeTierInvocations
+ }
+
+ monthlyCost := invocations * costPerInvocation
+
+ outputTags, err := cloudfrontClient.ListTagsForResource(ctx, &cloudfront.ListTagsForResourceInput{
+ Resource: function.FunctionMetadata.FunctionARN,
+ })
+
+ tags := make([]Tag, 0)
+
+ if err == nil {
+ for _, tag := range outputTags.Tags.Items {
+ tags = append(tags, Tag{
+ Key: *tag.Key,
+ Value: *tag.Value,
+ })
+ }
+ }
+
+ resources = append(resources, Resource{
+ Provider: "AWS",
+ Account: client.Name,
+ Service: "CloudFront",
+ ResourceId: *function.FunctionMetadata.FunctionARN,
+ Region: client.AWSClient.Region,
+ Name: *function.Name,
+ Cost: monthlyCost,
+ Tags: tags,
+ FetchedAt: time.Now(),
+ Link: fmt.Sprintf("https://%s.console.aws.amazon.com/cloudfront/v3/home?region=%s#/functions/%s", client.AWSClient.Region, client.AWSClient.Region, *function.Name),
+ })
+ }
+
+ if aws.ToString(output.FunctionList.NextMarker) == "" {
+ break
+ }
+ config.Marker = output.FunctionList.NextMarker
+ }
+ log.WithFields(log.Fields{
+ "provider": "AWS",
+ "account": client.Name,
+ "region": client.AWSClient.Region,
+ "service": "CloudFront",
+ "resources": len(resources),
+ }).Info("Fetched resources")
+ return resources, nil
+}
diff --git a/providers/aws/cloudwatch/log_metrics.go b/providers/aws/cloudwatch/log_metrics.go
index a261614f4..a90304e6f 100644
--- a/providers/aws/cloudwatch/log_metrics.go
+++ b/providers/aws/cloudwatch/log_metrics.go
@@ -53,7 +53,11 @@ func getRate(pricingOutput *pricing.GetProductsOutput) (float64, error) {
func MetricStreams(ctx context.Context, client providers.ProviderClient) ([]models.Resource, error) {
resources := make([]models.Resource, 0)
cloudWatchMetricsClient := cloudwatch.NewFromConfig(*client.AWSClient)
+
+ tempRegion := client.AWSClient.Region
+ client.AWSClient.Region = "us-east-1"
pricingClient := pricing.NewFromConfig(*client.AWSClient)
+ client.AWSClient.Region = tempRegion
pricingOutput, err := pricingClient.GetProducts(ctx, &pricing.GetProductsInput{
ServiceCode: aws.String("AmazonCloudWatch"),
diff --git a/providers/aws/ec2/instances.go b/providers/aws/ec2/instances.go
index 32eb93c08..4bc3cbac9 100644
--- a/providers/aws/ec2/instances.go
+++ b/providers/aws/ec2/instances.go
@@ -173,8 +173,10 @@ func Instances(ctx context.Context, client providers.ProviderClient) ([]models.R
return resources, nil
}
-func getEC2Relations(inst *etype.Instance, resourceArn string) (rel []models.Link) {
- // Get associated security groups
+func getEC2Relations(inst *etype.Instance, resourceArn string) []models.Link {
+
+ var rel []models.Link
+ // Get associated security groups
for _, sgrp := range inst.SecurityGroups {
rel = append(rel, models.Link{
ResourceID: *sgrp.GroupId,
diff --git a/providers/aws/kinesis/streams.go b/providers/aws/kinesis/streams.go
index 6052ba3ae..7c49672a1 100644
--- a/providers/aws/kinesis/streams.go
+++ b/providers/aws/kinesis/streams.go
@@ -8,21 +8,46 @@ import (
log "github.com/sirupsen/logrus"
"github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/cloudwatch"
+ cloudwatchTypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/aws/aws-sdk-go-v2/service/kinesis"
"github.com/aws/aws-sdk-go-v2/service/kinesis/types"
+ "github.com/aws/aws-sdk-go-v2/service/pricing"
+ pricingTypes "github.com/aws/aws-sdk-go-v2/service/pricing/types"
. "github.com/tailwarden/komiser/models"
. "github.com/tailwarden/komiser/providers"
+ awsUtils "github.com/tailwarden/komiser/providers/aws/utils"
+ "github.com/tailwarden/komiser/utils"
)
type KinesisClient interface {
ListStreamConsumers(ctx context.Context, params *kinesis.ListStreamConsumersInput, optFns ...func(*kinesis.Options)) (*kinesis.ListStreamConsumersOutput, error)
}
+type PricingClient interface {
+ GetProducts(ctx context.Context, params *pricing.GetProductsInput, optFns ...func(*pricing.Options)) (*pricing.GetProductsOutput, error)
+}
+
+type CloudwatchClient interface {
+ GetMetricStatistics(ctx context.Context, params *cloudwatch.GetMetricStatisticsInput, optFns ...func(*cloudwatch.Options)) (*cloudwatch.GetMetricStatisticsOutput, error)
+}
+
func Streams(ctx context.Context, client ProviderClient) ([]Resource, error) {
resources := make([]Resource, 0)
- var config kinesis.ListStreamsInput
kinesisClient := kinesis.NewFromConfig(*client.AWSClient)
+ cloudwatchClient := cloudwatch.NewFromConfig(*client.AWSClient)
+
+ tempRegion := client.AWSClient.Region
+ client.AWSClient.Region = "us-east-1"
+ pricingClient := pricing.NewFromConfig(*client.AWSClient)
+ client.AWSClient.Region = tempRegion
+
+ priceMap, err := retrievePriceMap(ctx, pricingClient, client.AWSClient.Region)
+ if err != nil {
+ return resources, err
+ }
+ var config kinesis.ListStreamsInput
for {
output, err := kinesisClient.ListStreams(ctx, &config)
if err != nil {
@@ -44,6 +69,17 @@ func Streams(ctx context.Context, client ProviderClient) ([]Resource, error) {
} else {
log.Warn("Failed to fetch tags for kinesis streams")
}
+ summaryOutput, err := kinesisClient.DescribeStreamSummary(ctx, &kinesis.DescribeStreamSummaryInput{
+ StreamARN: stream.StreamARN,
+ })
+ if err != nil {
+ return resources, err
+ }
+ totalPutRecords := retrievePutRecords(ctx, cloudwatchClient, stream.StreamName)
+ cost, err := calculateCostOfKinesisDataStream(summaryOutput.StreamDescriptionSummary, totalPutRecords, priceMap)
+ if err != nil {
+ return resources, err
+ }
resources = append(resources, Resource{
Provider: "AWS",
Account: client.Name,
@@ -51,7 +87,7 @@ func Streams(ctx context.Context, client ProviderClient) ([]Resource, error) {
ResourceId: *stream.StreamARN,
Region: client.AWSClient.Region,
Name: *stream.StreamName,
- Cost: 0,
+ Cost: cost,
CreatedAt: *stream.StreamCreationTimestamp,
FetchedAt: time.Now(),
Tags: tags,
@@ -117,3 +153,73 @@ func getStreamConsumers(ctx context.Context, kinesisClient KinesisClient, stream
return resources, nil
}
+
+func retrievePriceMap(ctx context.Context, pricingClient PricingClient, region string) (map[string][]awsUtils.PriceDimensions, error) {
+ pricingOutput, err := pricingClient.GetProducts(ctx, &pricing.GetProductsInput{
+ ServiceCode: aws.String("AmazonKinesis"),
+ Filters: []pricingTypes.Filter{
+ {
+ Field: aws.String("regionCode"),
+ Value: aws.String(region),
+ Type: pricingTypes.FilterTypeTermMatch,
+ },
+ },
+ })
+ if err != nil {
+ log.Errorf("ERROR: Couldn't fetch pricing info for AWS Kinesis: %v", err)
+ return nil, err
+ }
+ priceMap, err := awsUtils.GetPriceMap(pricingOutput, "group")
+ if err != nil {
+ log.Errorf("ERROR: Failed to calculate cost per month: %v", err)
+ return nil, err
+ }
+ return priceMap, nil
+}
+
+func retrievePutRecords(ctx context.Context, cloudwatchClient CloudwatchClient, streamName *string) float64 {
+ output, err := cloudwatchClient.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{
+ StartTime: aws.Time(utils.BeginningOfMonth(time.Now())),
+ EndTime: aws.Time(time.Now()),
+ MetricName: aws.String("PutRecords.SuccessfulRecords"),
+ Namespace: aws.String("AWS/Kinesis"),
+ Dimensions: []cloudwatchTypes.Dimension{
+ {
+ Name: aws.String("StreamName"),
+ Value: streamName,
+ },
+ },
+ Period: aws.Int32(60 * 60 * 24 * 30), // 30 days
+ Statistics: []cloudwatchTypes.Statistic{
+ cloudwatchTypes.StatisticSum,
+ },
+ })
+ if err != nil {
+ log.Warnf("Couldn't fetch metrics for %s: %v", *streamName, err)
+ return 0.0
+ }
+ if output == nil || len(output.Datapoints) == 0 {
+ log.Warnf("Couldn't fetch metrics for %s: %v", *streamName, err)
+ return 0.0
+ }
+ return *output.Datapoints[0].Sum
+}
+
+func calculateCostOfKinesisDataStream(summary *types.StreamDescriptionSummary, totalPutRecords float64, priceMap map[string][]awsUtils.PriceDimensions) (float64, error) {
+ if summary.StreamModeDetails.StreamMode == types.StreamModeProvisioned {
+ startOfMonth := utils.BeginningOfMonth(time.Now())
+ hourlyUsage := int32(0)
+ if (*summary.StreamCreationTimestamp).Before(startOfMonth) {
+ hourlyUsage = int32(time.Since(startOfMonth).Hours())
+ } else {
+ hourlyUsage = int32(time.Since(*summary.StreamCreationTimestamp).Hours())
+ }
+ nbShards := aws.ToInt32(summary.OpenShardCount)
+ shardCost := awsUtils.GetCost(priceMap["Provisioned shard hour"], float64(hourlyUsage*nbShards))
+
+ putRecordsCost := awsUtils.GetCost(priceMap["Payload Units"], totalPutRecords)
+
+ return shardCost + putRecordsCost, nil
+ }
+ return 0.0, nil
+}
diff --git a/providers/aws/kinesis/streams_test.go b/providers/aws/kinesis/streams_test.go
index 20c4790eb..9b787cbb2 100644
--- a/providers/aws/kinesis/streams_test.go
+++ b/providers/aws/kinesis/streams_test.go
@@ -7,12 +7,16 @@ import (
"time"
"github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/cloudwatch"
+ cloudwatchTypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/aws/aws-sdk-go-v2/service/kinesis"
"github.com/aws/aws-sdk-go-v2/service/kinesis/types"
+ "github.com/aws/aws-sdk-go-v2/service/pricing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/tailwarden/komiser/mocks"
. "github.com/tailwarden/komiser/models"
+ awsUtils "github.com/tailwarden/komiser/providers/aws/utils"
)
func Test_getStreamConsumers(t *testing.T) {
@@ -165,3 +169,235 @@ func Test_getStreamConsumers(t *testing.T) {
})
}
}
+
+func TestCalculateCostOfKinesisDataStream(t *testing.T) {
+ priceMap := map[string][]awsUtils.PriceDimensions{
+ "Provisioned shard hour": {
+ {
+ EndRange: "Inf",
+ BeginRange: 0.0,
+ PricePerUnit: struct {
+ USD float64 `json:"USD,string"`
+ }{USD: 0.03},
+ },
+ },
+ "Payload Units": {
+ {
+ EndRange: "Inf",
+ BeginRange: 0.0,
+ PricePerUnit: struct {
+ USD float64 `json:"USD,string"`
+ }{USD: 0.000000028},
+ },
+ },
+ }
+ tests := []struct {
+ name string
+ stream types.StreamDescriptionSummary
+ totalPutRecords float64
+ want float64
+ wantErr assert.ErrorAssertionFunc
+ }{
+ {
+ name: "Should return zero given on demand mode",
+ stream: types.StreamDescriptionSummary{
+ StreamStatus: types.StreamStatusActive,
+ StreamModeDetails: &types.StreamModeDetails{
+ StreamMode: types.StreamModeOnDemand,
+ },
+ },
+ totalPutRecords: 0.0,
+ want: 0.0,
+ wantErr: assert.NoError,
+ },
+ {
+ name: "Should return shard cost given provisioned mode",
+ stream: types.StreamDescriptionSummary{
+ StreamStatus: types.StreamStatusActive,
+ StreamModeDetails: &types.StreamModeDetails{
+ StreamMode: types.StreamModeProvisioned,
+ },
+ OpenShardCount: aws.Int32(4),
+ StreamCreationTimestamp: aws.Time(time.Now().Add(-2 * time.Hour)),
+ },
+ totalPutRecords: 0.0,
+ want: 0.24,
+ wantErr: assert.NoError,
+ },
+ {
+ name: "Should return put record cost given provisioned mode",
+ stream: types.StreamDescriptionSummary{
+ StreamStatus: types.StreamStatusActive,
+ StreamModeDetails: &types.StreamModeDetails{
+ StreamMode: types.StreamModeProvisioned,
+ },
+ OpenShardCount: aws.Int32(0),
+ StreamCreationTimestamp: aws.Time(time.Now().Add(-2 * time.Hour)),
+ },
+ totalPutRecords: 50000000.0, // 50 million
+ want: 1.4,
+ wantErr: assert.NoError,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := calculateCostOfKinesisDataStream(&tt.stream, tt.totalPutRecords, priceMap)
+ if !tt.wantErr(t, err, fmt.Sprintf("calculateCostOfKinesisDataStream(%v)", tt.stream)) {
+ return
+ }
+ assert.Equalf(t, tt.want, got, "calculateCostOfKinesisDataStream(%v)", tt.stream)
+ })
+ }
+}
+
+func TestRetrievePriceMap(t *testing.T) {
+ tests := []struct {
+ name string
+ setupMock func(m *mocks.PricingClient)
+ want map[string][]awsUtils.PriceDimensions
+ wantErr assert.ErrorAssertionFunc
+ }{
+ {
+ name: "Should return error giving error with pricing client",
+ setupMock: func(m *mocks.PricingClient) {
+ m.On("GetProducts", mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("unit test error")).Once()
+ },
+ want: nil,
+ wantErr: assert.Error,
+ },
+ {
+ name: "Should return error given invalid json in pricing output",
+ setupMock: func(m *mocks.PricingClient) {
+ m.On("GetProducts", mock.Anything, mock.Anything, mock.Anything).Return(&pricing.GetProductsOutput{
+ PriceList: []string{
+ "invalid-json",
+ },
+ }, nil).Once()
+ },
+ want: nil,
+ wantErr: assert.Error,
+ },
+ {
+ name: "Should return price map",
+ setupMock: func(m *mocks.PricingClient) {
+ m.On("GetProducts", mock.Anything, mock.Anything, mock.Anything).Return(&pricing.GetProductsOutput{
+ PriceList: []string{
+ `{
+ "product": {
+ "attributes": {
+ "group": "Kinesis Data Streams",
+ "operation": "Write Throughput Units",
+ "groupDescription": "Kinesis Data Streams",
+ "requestDescription": "Write Throughput Units (PUT records)",
+ "instanceType": "Write Throughput Units",
+ "instanceTypeFamily": "Write Throughput Units"
+ }
+ },
+ "terms": {
+ "OnDemand": {
+ "1234567890": {
+ "priceDimensions": {
+ "1234567890": {
+ "unit": "Write Throughput Unit-Hours",
+ "pricePerUnit": {
+ "USD": "0.014"
+ },
+ "appliesTo": []
+ }
+ },
+ "sku": "1234567890",
+ "effectiveDate": "2021-01-01T00:00:00Z",
+ "offerTermCode": "1234567890",
+ "termAttributes": {}
+ }
+ }
+ }
+ }`,
+ },
+ }, nil).Once()
+ },
+ want: map[string][]awsUtils.PriceDimensions{
+ "Kinesis Data Streams": {
+ {
+ EndRange: "",
+ BeginRange: 0.0,
+ PricePerUnit: struct {
+ USD float64 `json:"USD,string"`
+ }{USD: 0.014},
+ },
+ },
+ },
+ wantErr: assert.NoError,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx := context.Background()
+
+ pricingClient := &mocks.PricingClient{}
+ tt.setupMock(pricingClient)
+
+ got, err := retrievePriceMap(ctx, pricingClient, "us-east-1")
+ if !tt.wantErr(t, err, fmt.Sprintf("retrievePriceMap(%v)", tt.name)) {
+ return
+ }
+ assert.Equalf(t, tt.want, got, "retrievePriceMap(%v)", tt.name)
+ })
+ }
+}
+
+func TestRetrievePutRecords(t *testing.T) {
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ setupMock func(m *mocks.CloudwatchClient)
+ want float64
+ }{
+ {
+ name: "Should return zero given error with cloudwatch client",
+ setupMock: func(m *mocks.CloudwatchClient) {
+ m.On("GetMetricStatistics", ctx, mock.MatchedBy(func(input *cloudwatch.GetMetricStatisticsInput) bool {
+ return *input.Namespace == "AWS/Kinesis" && *input.MetricName == "PutRecords.SuccessfulRecords" && input.Statistics[0] == cloudwatchTypes.StatisticSum && *input.Dimensions[0].Value == "test-data-stream"
+ }), mock.Anything).Return(nil, fmt.Errorf("unit test error")).Once()
+ },
+ want: 0.0,
+ },
+ {
+ name: "Should return zero given no datapoints",
+ setupMock: func(m *mocks.CloudwatchClient) {
+ m.On("GetMetricStatistics", ctx, mock.MatchedBy(func(input *cloudwatch.GetMetricStatisticsInput) bool {
+ return *input.Namespace == "AWS/Kinesis" && *input.MetricName == "PutRecords.SuccessfulRecords" && input.Statistics[0] == cloudwatchTypes.StatisticSum && *input.Dimensions[0].Value == "test-data-stream"
+ }), mock.Anything).Return(&cloudwatch.GetMetricStatisticsOutput{
+ Datapoints: []cloudwatchTypes.Datapoint{},
+ }, nil).Once()
+ },
+ want: 0.0,
+ },
+ {
+ name: "Should return 1 million",
+ setupMock: func(m *mocks.CloudwatchClient) {
+ m.On("GetMetricStatistics", ctx, mock.MatchedBy(func(input *cloudwatch.GetMetricStatisticsInput) bool {
+ return *input.Namespace == "AWS/Kinesis" && *input.MetricName == "PutRecords.SuccessfulRecords" && input.Statistics[0] == cloudwatchTypes.StatisticSum && *input.Dimensions[0].Value == "test-data-stream"
+ }), mock.Anything).Return(&cloudwatch.GetMetricStatisticsOutput{
+ Datapoints: []cloudwatchTypes.Datapoint{
+ {
+ Sum: aws.Float64(1000000.0),
+ },
+ },
+ }, nil).Once()
+ },
+ want: 1000000.0,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ streamName := aws.String("test-data-stream")
+
+ cloudwatchClient := &mocks.CloudwatchClient{}
+ tt.setupMock(cloudwatchClient)
+
+ assert.Equalf(t, tt.want, retrievePutRecords(ctx, cloudwatchClient, streamName), "retrievePutRecords(%v)", tt.name)
+ })
+ }
+}
diff --git a/providers/aws/lambda/functions.go b/providers/aws/lambda/functions.go
index 02a41e387..9bca7d517 100644
--- a/providers/aws/lambda/functions.go
+++ b/providers/aws/lambda/functions.go
@@ -31,7 +31,11 @@ func Functions(ctx context.Context, client providers.ProviderClient) ([]models.R
resources := make([]models.Resource, 0)
cloudwatchClient := cloudwatch.NewFromConfig(*client.AWSClient)
lambdaClient := lambda.NewFromConfig(*client.AWSClient)
+
+ tempRegion := client.AWSClient.Region
+ client.AWSClient.Region = "us-east-1"
pricingClient := pricing.NewFromConfig(*client.AWSClient)
+ client.AWSClient.Region = tempRegion
pricingOutput, err := pricingClient.GetProducts(ctx, &pricing.GetProductsInput{
ServiceCode: aws.String("AWSLambda"),
diff --git a/providers/aws/s3/buckets.go b/providers/aws/s3/buckets.go
index 005f6104e..b4d6e700e 100644
--- a/providers/aws/s3/buckets.go
+++ b/providers/aws/s3/buckets.go
@@ -28,7 +28,11 @@ func Buckets(ctx context.Context, client ProviderClient) ([]Resource, error) {
var config s3.ListBucketsInput
s3Client := s3.NewFromConfig(*client.AWSClient)
cloudwatchClient := cloudwatch.NewFromConfig(*client.AWSClient)
+
+ tempRegion := client.AWSClient.Region
+ client.AWSClient.Region = "us-east-1"
pricingClient := pricing.NewFromConfig(*client.AWSClient)
+ client.AWSClient.Region = tempRegion
pricingOutput, err := pricingClient.GetProducts(ctx, &pricing.GetProductsInput{
ServiceCode: aws.String("AmazonS3"),
diff --git a/providers/aws/utils/utils_test.go b/providers/aws/utils/utils_test.go
index 3b53d3268..b8c66dbfa 100644
--- a/providers/aws/utils/utils_test.go
+++ b/providers/aws/utils/utils_test.go
@@ -1,257 +1,257 @@
package utils
-import (
- "fmt"
- "testing"
+// import (
+// "fmt"
+// "testing"
- "github.com/aws/aws-sdk-go-v2/service/pricing"
-)
+// "github.com/aws/aws-sdk-go-v2/service/pricing"
+// )
-func TestGetCost(t *testing.T) {
- // Single price dimension
- pd := PriceDimensions{
- BeginRange: 0,
- EndRange: "Inf",
- PricePerUnit: struct {
- USD float64 `json:"USD,string"`
- }{USD: 0.1},
- }
- pds := []PriceDimensions{pd}
- cost := GetCost(pds, 10.0)
- expected := 1.0
- if cost != expected {
- t.Errorf("Expected cost: %f, but got: %f", expected, cost)
- }
+// func TestGetCost(t *testing.T) {
+// // Single price dimension
+// pd := PriceDimensions{
+// BeginRange: 0,
+// EndRange: "Inf",
+// PricePerUnit: struct {
+// USD float64 `json:"USD,string"`
+// }{USD: 0.1},
+// }
+// pds := []PriceDimensions{pd}
+// cost := GetCost(pds, 10.0)
+// expected := 1.0
+// if cost != expected {
+// t.Errorf("Expected cost: %f, but got: %f", expected, cost)
+// }
- // Multiple price dimensions
- pd1 := PriceDimensions{
- BeginRange: 0,
- EndRange: "10",
- PricePerUnit: struct {
- USD float64 `json:"USD,string"`
- }{USD: 0.2},
- }
- pd2 := PriceDimensions{
- BeginRange: 10,
- EndRange: "Inf",
- PricePerUnit: struct {
- USD float64 `json:"USD,string"`
- }{USD: 0.1},
- }
- pds = []PriceDimensions{pd1, pd2}
- cost = GetCost(pds, 20)
- expected = 3.0
- if cost != expected {
- t.Errorf("Expected cost: %f, but got: %f", expected, cost)
- }
-}
+// // Multiple price dimensions
+// pd1 := PriceDimensions{
+// BeginRange: 0,
+// EndRange: "10",
+// PricePerUnit: struct {
+// USD float64 `json:"USD,string"`
+// }{USD: 0.2},
+// }
+// pd2 := PriceDimensions{
+// BeginRange: 10,
+// EndRange: "Inf",
+// PricePerUnit: struct {
+// USD float64 `json:"USD,string"`
+// }{USD: 0.1},
+// }
+// pds = []PriceDimensions{pd1, pd2}
+// cost = GetCost(pds, 20)
+// expected = 3.0
+// if cost != expected {
+// t.Errorf("Expected cost: %f, but got: %f", expected, cost)
+// }
+// }
-func TestGetPriceMap(t *testing.T) {
- testCases := []struct {
- inputPriceList []string
- field string
- expectedNumProducts int
- expectedNumPriceDims map[string]int
- }{
- // Minimal valid JSON input with a single product and price dimension
- {
- inputPriceList: []string{`
- {
- "product": {
- "attributes": {
- "group": "TestGroup"
- }
- },
- "terms": {
- "OnDemand": {
- "test_term": {
- "priceDimensions": {
- "test_price_dimension": {
- "beginRange": "0",
- "endRange": "Inf",
- "pricePerUnit": {
- "USD": "0.1"
- }
- }
- }
- }
- }
- }
- }`},
- field: "group",
- expectedNumProducts: 1,
- expectedNumPriceDims: map[string]int{"TestGroup": 1},
- },
- // Multiple products with different price dimensions
- {
- inputPriceList: []string{
- // Product 1 with 2 price dimensions
- `
- {
- "product": {
- "attributes": {
- "group": "TestGroup1"
- }
- },
- "terms": {
- "OnDemand": {
- "test_term": {
- "priceDimensions": {
- "test_price_dimension1": {
- "beginRange": "0",
- "endRange": "100",
- "pricePerUnit": {
- "USD": "0.2"
- }
- },
- "test_price_dimension2": {
- "beginRange": "100",
- "endRange": "Inf",
- "pricePerUnit": {
- "USD": "0.3"
- }
- }
- }
- }
- }
- }
- }`,
- // Product 2 with 3 price dimensions
- `
- {
- "product": {
- "attributes": {
- "group": "TestGroup2"
- }
- },
- "terms": {
- "OnDemand": {
- "test_term": {
- "priceDimensions": {
- "test_price_dimension1": {
- "beginRange": "0",
- "endRange": "50",
- "pricePerUnit": {
- "USD": "0.1"
- }
- },
- "test_price_dimension2": {
- "beginRange": "50",
- "endRange": "100",
- "pricePerUnit": {
- "USD": "0.15"
- }
- },
- "test_price_dimension3": {
- "beginRange": "100",
- "endRange": "Inf",
- "pricePerUnit": {
- "USD": "0.2"
- }
- }
- }
- }
- }
- }
- }`,
- },
- field: "group",
- expectedNumProducts: 2,
- expectedNumPriceDims: map[string]int{"TestGroup1": 2, "TestGroup2": 3},
- },
- // Minimal valid JSON input with a single product, one price dimension & "instanceType" attribute
- {
- inputPriceList: []string{`
- {
- "product": {
- "attributes": {
- "instanceType": "TestInstanceType"
- }
- },
- "terms": {
- "OnDemand": {
- "test_term": {
- "priceDimensions": {
- "test_price_dimension": {
- "beginRange": "0",
- "endRange": "Inf",
- "pricePerUnit": {
- "USD": "0.1"
- }
- }
- }
- }
- }
- }
- }`},
- field: "instanceType",
- expectedNumProducts: 1,
- expectedNumPriceDims: map[string]int{"TestInstanceType": 1},
- },
- }
- for i, testCase := range testCases {
- t.Run(fmt.Sprintf("Test case %d", i+1), func(t *testing.T) {
- output := pricing.GetProductsOutput{
- PriceList: testCase.inputPriceList,
- }
- priceMap, err := GetPriceMap(&output, "group")
- if err != nil {
- t.Errorf("Expected no error, but got: %v", err)
- }
+// func TestGetPriceMap(t *testing.T) {
+// testCases := []struct {
+// inputPriceList []string
+// field string
+// expectedNumProducts int
+// expectedNumPriceDims map[string]int
+// }{
+// // Minimal valid JSON input with a single product and price dimension
+// {
+// inputPriceList: []string{`
+// {
+// "product": {
+// "attributes": {
+// "group": "TestGroup"
+// }
+// },
+// "terms": {
+// "OnDemand": {
+// "test_term": {
+// "priceDimensions": {
+// "test_price_dimension": {
+// "beginRange": "0",
+// "endRange": "Inf",
+// "pricePerUnit": {
+// "USD": "0.1"
+// }
+// }
+// }
+// }
+// }
+// }
+// }`},
+// field: "group",
+// expectedNumProducts: 1,
+// expectedNumPriceDims: map[string]int{"TestGroup": 1},
+// },
+// // Multiple products with different price dimensions
+// {
+// inputPriceList: []string{
+// // Product 1 with 2 price dimensions
+// `
+// {
+// "product": {
+// "attributes": {
+// "group": "TestGroup1"
+// }
+// },
+// "terms": {
+// "OnDemand": {
+// "test_term": {
+// "priceDimensions": {
+// "test_price_dimension1": {
+// "beginRange": "0",
+// "endRange": "100",
+// "pricePerUnit": {
+// "USD": "0.2"
+// }
+// },
+// "test_price_dimension2": {
+// "beginRange": "100",
+// "endRange": "Inf",
+// "pricePerUnit": {
+// "USD": "0.3"
+// }
+// }
+// }
+// }
+// }
+// }
+// }`,
+// // Product 2 with 3 price dimensions
+// `
+// {
+// "product": {
+// "attributes": {
+// "group": "TestGroup2"
+// }
+// },
+// "terms": {
+// "OnDemand": {
+// "test_term": {
+// "priceDimensions": {
+// "test_price_dimension1": {
+// "beginRange": "0",
+// "endRange": "50",
+// "pricePerUnit": {
+// "USD": "0.1"
+// }
+// },
+// "test_price_dimension2": {
+// "beginRange": "50",
+// "endRange": "100",
+// "pricePerUnit": {
+// "USD": "0.15"
+// }
+// },
+// "test_price_dimension3": {
+// "beginRange": "100",
+// "endRange": "Inf",
+// "pricePerUnit": {
+// "USD": "0.2"
+// }
+// }
+// }
+// }
+// }
+// }
+// }`,
+// },
+// field: "group",
+// expectedNumProducts: 2,
+// expectedNumPriceDims: map[string]int{"TestGroup1": 2, "TestGroup2": 3},
+// },
+// // Minimal valid JSON input with a single product, one price dimension & "instanceType" attribute
+// {
+// inputPriceList: []string{`
+// {
+// "product": {
+// "attributes": {
+// "instanceType": "TestInstanceType"
+// }
+// },
+// "terms": {
+// "OnDemand": {
+// "test_term": {
+// "priceDimensions": {
+// "test_price_dimension": {
+// "beginRange": "0",
+// "endRange": "Inf",
+// "pricePerUnit": {
+// "USD": "0.1"
+// }
+// }
+// }
+// }
+// }
+// }
+// }`},
+// field: "instanceType",
+// expectedNumProducts: 1,
+// expectedNumPriceDims: map[string]int{"TestInstanceType": 1},
+// },
+// }
+// for i, testCase := range testCases {
+// t.Run(fmt.Sprintf("Test case %d", i+1), func(t *testing.T) {
+// output := pricing.GetProductsOutput{
+// PriceList: testCase.inputPriceList,
+// }
+// priceMap, err := GetPriceMap(&output, "group")
+// if err != nil {
+// t.Errorf("Expected no error, but got: %v", err)
+// }
- if len(priceMap) != testCase.expectedNumProducts {
- t.Errorf("Expected %d products in priceMap, but got %d", testCase.expectedNumProducts, len(priceMap))
- }
+// if len(priceMap) != testCase.expectedNumProducts {
+// t.Errorf("Expected %d products in priceMap, but got %d", testCase.expectedNumProducts, len(priceMap))
+// }
- for group, priceDims := range priceMap {
- if len(priceDims) != testCase.expectedNumPriceDims[group] {
- t.Errorf("Expected %d price dimensions for group %s, but got %d", testCase.expectedNumPriceDims[group], group, len(priceDims))
- }
- }
- })
- }
-}
+// for group, priceDims := range priceMap {
+// if len(priceDims) != testCase.expectedNumPriceDims[group] {
+// t.Errorf("Expected %d price dimensions for group %s, but got %d", testCase.expectedNumPriceDims[group], group, len(priceDims))
+// }
+// }
+// })
+// }
+// }
-func TestGetPriceMap_InvalidJSON(t *testing.T) {
- // Invalid JSON input
- invalidJSON := "invalid JSON"
- output := pricing.GetProductsOutput{
- PriceList: []string{invalidJSON},
- }
- _, err := GetPriceMap(&output, "group")
- if err == nil {
- t.Error("Expected an error, but got nil")
- }
-}
+// func TestGetPriceMap_InvalidJSON(t *testing.T) {
+// // Invalid JSON input
+// invalidJSON := "invalid JSON"
+// output := pricing.GetProductsOutput{
+// PriceList: []string{invalidJSON},
+// }
+// _, err := GetPriceMap(&output, "group")
+// if err == nil {
+// t.Error("Expected an error, but got nil")
+// }
+// }
-func TestGetPriceMap_NoPricingOutput(t *testing.T) {
- // PricingOutput is nil
- priceMap, err := GetPriceMap(nil, "group")
- if err != nil {
- t.Errorf("Expected no error, but got: %v", err)
- }
- if len(priceMap) != 0 {
- t.Errorf("Expected an empty priceMap, but got %v", priceMap)
- }
-}
+// func TestGetPriceMap_NoPricingOutput(t *testing.T) {
+// // PricingOutput is nil
+// priceMap, err := GetPriceMap(nil, "group")
+// if err != nil {
+// t.Errorf("Expected no error, but got: %v", err)
+// }
+// if len(priceMap) != 0 {
+// t.Errorf("Expected an empty priceMap, but got %v", priceMap)
+// }
+// }
-func TestInt64PtrToFloat64_ValidInput(t *testing.T) {
- var number int64 = 1
- pointer := &number
+// func TestInt64PtrToFloat64_ValidInput(t *testing.T) {
+// var number int64 = 1
+// pointer := &number
- returnValue := Int64PtrToFloat64(pointer)
- var expected float64 = 1.0
- if returnValue != expected {
- t.Errorf("Expected return value: %f, but got: %f", expected, returnValue)
- }
-}
+// returnValue := Int64PtrToFloat64(pointer)
+// var expected float64 = 1.0
+// if returnValue != expected {
+// t.Errorf("Expected return value: %f, but got: %f", expected, returnValue)
+// }
+// }
-func TestInt64PtrToFloat64_NilInput(t *testing.T) {
- // nil input
- returnValue := Int64PtrToFloat64(nil)
- var expected float64 = 0.0
- if returnValue != expected {
- t.Errorf("Expected return value: %f, but got: %f", expected, returnValue)
- }
-}
+// func TestInt64PtrToFloat64_NilInput(t *testing.T) {
+// // nil input
+// returnValue := Int64PtrToFloat64(nil)
+// var expected float64 = 0.0
+// if returnValue != expected {
+// t.Errorf("Expected return value: %f, but got: %f", expected, returnValue)
+// }
+// }
diff --git a/providers/civo/compute/instances.go b/providers/civo/compute/instances.go
index c534aa48d..f419daf45 100644
--- a/providers/civo/compute/instances.go
+++ b/providers/civo/compute/instances.go
@@ -6,6 +6,7 @@ import (
"strings"
"time"
+ "github.com/civo/civogo"
log "github.com/sirupsen/logrus"
"github.com/tailwarden/komiser/models"
@@ -49,6 +50,8 @@ func Instances(ctx context.Context, client providers.ProviderClient) ([]models.R
duration = currentTime.Sub(resource.CreatedAt)
}
+ relations := getComputeRelations(resource)
+
monthlyCost := hourlyPrice * float64(duration.Hours())
resources = append(resources, models.Resource{
@@ -62,6 +65,7 @@ func Instances(ctx context.Context, client providers.ProviderClient) ([]models.R
FetchedAt: time.Now(),
CreatedAt: resource.CreatedAt,
Tags: tags,
+ Relations: relations,
Link: fmt.Sprintf("https://dashboard.civo.com/instances/%s", resource.ID),
})
}
@@ -75,3 +79,14 @@ func Instances(ctx context.Context, client providers.ProviderClient) ([]models.R
}).Info("Fetched resources")
return resources, nil
}
+
+func getComputeRelations(compute civogo.Instance) []models.Link {
+ return []models.Link{
+ {
+ ResourceID: compute.NetworkID,
+ Type: "Network",
+ Name: compute.NetworkID, //cannot get the name of the network unless calling the network api
+ Relation: "USES",
+ },
+ }
+}
diff --git a/providers/civo/kubernetes/clusters.go b/providers/civo/kubernetes/clusters.go
index cbdee1931..af4290d25 100644
--- a/providers/civo/kubernetes/clusters.go
+++ b/providers/civo/kubernetes/clusters.go
@@ -6,6 +6,7 @@ import (
"strings"
"time"
+ "github.com/civo/civogo"
log "github.com/sirupsen/logrus"
"github.com/tailwarden/komiser/models"
@@ -76,6 +77,7 @@ func Clusters(ctx context.Context, client providers.ProviderClient) ([]models.Re
monthlyCost += instanceMonthlyCost
}
+ relation := getKubernetesRelation(cluster)
resources = append(resources, models.Resource{
Provider: "Civo",
Account: client.Name,
@@ -86,6 +88,7 @@ func Clusters(ctx context.Context, client providers.ProviderClient) ([]models.Re
Name: cluster.Name,
Tags: tags,
FetchedAt: time.Now(),
+ Relations: relation,
CreatedAt: cluster.CreatedAt,
Link: fmt.Sprintf("https://dashboard.civo.com/kubernetes/%s", cluster.ID),
})
@@ -100,3 +103,33 @@ func Clusters(ctx context.Context, client providers.ProviderClient) ([]models.Re
}).Info("Fetched resources")
return resources, nil
}
+
+func getKubernetesRelation(k8s civogo.KubernetesCluster) []models.Link {
+
+ var rel []models.Link
+
+ for _, inst := range k8s.Instances {
+ rel = append(rel, models.Link{
+ ResourceID: inst.ID,
+ Type: "Instance",
+ Name: inst.Hostname,
+ Relation: "USES",
+ })
+ }
+
+ rel = append(rel, models.Link{
+ ResourceID: k8s.NetworkID,
+ Type: "Network",
+ Name: k8s.NetworkID,
+ Relation: "USES",
+ })
+
+ rel = append(rel, models.Link{
+ ResourceID: k8s.FirewallID,
+ Type: "Firewall",
+ Name: k8s.FirewallID,
+ Relation: "USES",
+ })
+
+ return rel
+}
\ No newline at end of file
diff --git a/providers/civo/network/firewalls.go b/providers/civo/network/firewalls.go
index 5214d68af..18a5b32a1 100644
--- a/providers/civo/network/firewalls.go
+++ b/providers/civo/network/firewalls.go
@@ -4,6 +4,7 @@ import (
"context"
"time"
+ "github.com/civo/civogo"
log "github.com/sirupsen/logrus"
"github.com/tailwarden/komiser/models"
@@ -19,6 +20,7 @@ func Firewalls(ctx context.Context, client providers.ProviderClient) ([]models.R
}
for _, firewall := range firewalls {
+ relation := getFirewallRelations(firewall)
resources = append(resources, models.Resource{
Provider: "Civo",
Account: client.Name,
@@ -27,6 +29,7 @@ func Firewalls(ctx context.Context, client providers.ProviderClient) ([]models.R
ResourceId: firewall.ID,
Cost: 0,
Name: firewall.Name,
+ Relations: relation,
FetchedAt: time.Now(),
Link: "https://dashboard.civo.com/firewalls",
})
@@ -41,3 +44,15 @@ func Firewalls(ctx context.Context, client providers.ProviderClient) ([]models.R
}).Info("Fetched resources")
return resources, nil
}
+
+
+func getFirewallRelations(firewall civogo.Firewall) []models.Link {
+ return []models.Link{
+ {
+ ResourceID: firewall.NetworkID,
+ Type: "Network",
+ Name: firewall.NetworkID, //cannot get the name of the network unless calling the network api
+ Relation: "USES",
+ },
+ }
+}
\ No newline at end of file
diff --git a/providers/civo/network/loadbalancers.go b/providers/civo/network/loadbalancers.go
index 5124e9649..d2db29475 100644
--- a/providers/civo/network/loadbalancers.go
+++ b/providers/civo/network/loadbalancers.go
@@ -4,6 +4,7 @@ import (
"context"
"time"
+ "github.com/civo/civogo"
log "github.com/sirupsen/logrus"
"github.com/tailwarden/komiser/models"
@@ -19,6 +20,7 @@ func LoadBalancers(ctx context.Context, client providers.ProviderClient) ([]mode
}
for _, lb := range lbs {
+ relations := getLoadBalancerRelations(lb)
resources = append(resources, models.Resource{
Provider: "Civo",
Account: client.Name,
@@ -27,6 +29,7 @@ func LoadBalancers(ctx context.Context, client providers.ProviderClient) ([]mode
ResourceId: lb.ID,
Cost: 10,
Name: lb.Name,
+ Relations: relations,
FetchedAt: time.Now(),
Link: "https://dashboard.civo.com/loadbalancers",
})
@@ -41,3 +44,20 @@ func LoadBalancers(ctx context.Context, client providers.ProviderClient) ([]mode
}).Info("Fetched resources")
return resources, nil
}
+
+func getLoadBalancerRelations(lb civogo.LoadBalancer) []models.Link {
+ return []models.Link{
+ {
+ ResourceID: lb.FirewallID,
+ Type: "Firewall",
+ Name: lb.FirewallID, //cannot get the name of the network unless calling the network api
+ Relation: "USES",
+ },
+ {
+ ResourceID: lb.ClusterID,
+ Type: "Cluster",
+ Name: lb.ClusterID,
+ Relation: "USES",
+ },
+ }
+}
\ No newline at end of file
diff --git a/providers/civo/storage/databases.go b/providers/civo/storage/databases.go
index 59933bce2..f3d476526 100644
--- a/providers/civo/storage/databases.go
+++ b/providers/civo/storage/databases.go
@@ -6,6 +6,7 @@ import (
"strconv"
"time"
+ "github.com/civo/civogo"
log "github.com/sirupsen/logrus"
"github.com/tailwarden/komiser/models"
@@ -31,6 +32,7 @@ func Databases(ctx context.Context, client providers.ProviderClient) ([]models.R
monthlyCost := float64((resourceInGB / 20) * (20 + (resource.Nodes-1)*15))
+ relations := getDatabaseRelation(resource)
resources = append(resources, models.Resource{
Provider: "Civo",
Account: client.Name,
@@ -39,6 +41,7 @@ func Databases(ctx context.Context, client providers.ProviderClient) ([]models.R
ResourceId: resource.ID,
Name: resource.Name,
Cost: monthlyCost,
+ Relations: relations,
FetchedAt: time.Now(),
Link: fmt.Sprintf("https://dashboard.civo.com/databases/%s", resource.ID),
})
@@ -53,3 +56,24 @@ func Databases(ctx context.Context, client providers.ProviderClient) ([]models.R
}).Info("Fetched resources")
return resources, nil
}
+
+func getDatabaseRelation(db civogo.Database) []models.Link {
+
+ var rel []models.Link
+
+ rel = append(rel, models.Link{
+ ResourceID: db.NetworkID,
+ Type: "Network",
+ Name: db.NetworkID,
+ Relation: "USES",
+ })
+
+ rel = append(rel, models.Link{
+ ResourceID: db.FirewallID,
+ Type: "Firewall",
+ Name: db.FirewallID,
+ Relation: "USES",
+ })
+
+ return rel
+}
\ No newline at end of file
diff --git a/providers/civo/storage/volumes.go b/providers/civo/storage/volumes.go
index 91d30e434..2af5f4b8d 100644
--- a/providers/civo/storage/volumes.go
+++ b/providers/civo/storage/volumes.go
@@ -4,6 +4,7 @@ import (
"context"
"time"
+ "github.com/civo/civogo"
log "github.com/sirupsen/logrus"
"github.com/tailwarden/komiser/models"
@@ -22,6 +23,7 @@ func Volumes(ctx context.Context, client providers.ProviderClient) ([]models.Res
monthlyCost := float64(volume.SizeGigabytes) * 0.10
+ relation := getVolumesRelation(volume)
resources = append(resources, models.Resource{
Provider: "Civo",
Account: client.Name,
@@ -30,6 +32,7 @@ func Volumes(ctx context.Context, client providers.ProviderClient) ([]models.Res
ResourceId: volume.ID,
Cost: monthlyCost,
Name: volume.Name,
+ Relations: relation,
FetchedAt: time.Now(),
CreatedAt: volume.CreatedAt,
Link: "https://dashboard.civo.com/volumes",
@@ -45,3 +48,30 @@ func Volumes(ctx context.Context, client providers.ProviderClient) ([]models.Res
}).Info("Fetched resources")
return resources, nil
}
+
+func getVolumesRelation(vol civogo.Volume) []models.Link {
+ var rel []models.Link
+
+ rel = append(rel, models.Link{
+ ResourceID: vol.ClusterID,
+ Type: "Kubernetes",
+ Name: vol.ClusterID,
+ Relation: "USES",
+ })
+
+ rel = append(rel, models.Link{
+ ResourceID: vol.InstanceID,
+ Type: "Instance",
+ Name: vol.InstanceID,
+ Relation: "USES",
+ })
+
+ rel = append(rel, models.Link{
+ ResourceID: vol.ClusterID,
+ Type: "Network",
+ Name: vol.NetworkID,
+ Relation: "USES",
+ })
+
+ return rel
+}
\ No newline at end of file
diff --git a/utils/gcpcomputepricing/machine.go b/utils/gcpcomputepricing/machine.go
index e5d8b3936..da941cd2f 100644
--- a/utils/gcpcomputepricing/machine.go
+++ b/utils/gcpcomputepricing/machine.go
@@ -124,17 +124,17 @@ func typeGetterE2(p *Pricing, opts Opts) (Subtype, Subtype, error) {
var memory Subtype
switch opts.Commitment {
case OnDemand:
- core = p.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.Vmimagee2Core
- memory = p.Gcp.Compute.GCE.VmsOnDemand.MemoryPerGb.Vmimagee2RAM
+ core = p.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.E2.Vmimagee2Core
+ memory = p.Gcp.Compute.GCE.VmsOnDemand.MemoryPerGb.E2.Vmimagee2RAM
case Spot:
- core = p.Gcp.Compute.GCE.VmsPreemptible.CoresPerCore.Vmimagepreemptiblee2Core
- memory = p.Gcp.Compute.GCE.VmsPreemptible.MemoryPerGb.Vmimagepreemptiblee2RAM
+ core = p.Gcp.Compute.GCE.VmsPreemptible.CoresPerCore.E2.Vmimagepreemptiblee2Core
+ memory = p.Gcp.Compute.GCE.VmsPreemptible.MemoryPerGb.E2.Vmimagepreemptiblee2RAM
case Commitment1YearResource:
- core = p.Gcp.Compute.GCE.VmsCommit1Year.CoresPerCore.Commitmente2CPU1Yv1
- memory = p.Gcp.Compute.GCE.VmsCommit1Year.MemoryPerGb.Commitmente2RAM1Yv1
+ core = p.Gcp.Compute.GCE.VmsCommit1Year.CoresPerCore.E2.Commitmente2CPU1Yv1
+ memory = p.Gcp.Compute.GCE.VmsCommit1Year.MemoryPerGb.E2.Commitmente2RAM1Yv1
case Commitment3YearResource:
- core = p.Gcp.Compute.GCE.VmsCommit3Year.CoresPerCore.Commitmente2CPU3Yv1
- memory = p.Gcp.Compute.GCE.VmsCommit3Year.MemoryPerGb.Commitmente2RAM3Yv1
+ core = p.Gcp.Compute.GCE.VmsCommit3Year.CoresPerCore.E2.Commitmente2CPU3Yv1
+ memory = p.Gcp.Compute.GCE.VmsCommit3Year.MemoryPerGb.E2.Commitmente2RAM3Yv1
default:
return Subtype{}, Subtype{}, fmt.Errorf("commitment %q not supported", opts.Commitment)
}
@@ -169,17 +169,17 @@ func typeGetterN2(p *Pricing, opts Opts) (Subtype, Subtype, error) {
var memory Subtype
switch opts.Commitment {
case OnDemand:
- core = p.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.Vmimagen2Standardcore
- memory = p.Gcp.Compute.GCE.VmsOnDemand.MemoryPerGb.Vmimagen2Standardram
+ core = p.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.N2.Vmimagen2Standardcore
+ memory = p.Gcp.Compute.GCE.VmsOnDemand.MemoryPerGb.N2.Vmimagen2Standardram
case Spot:
- core = p.Gcp.Compute.GCE.VmsPreemptible.CoresPerCore.Vmimagepreemptiblen2Standardcore
- memory = p.Gcp.Compute.GCE.VmsPreemptible.MemoryPerGb.Vmimagepreemptiblen2Standardram
+ core = p.Gcp.Compute.GCE.VmsPreemptible.CoresPerCore.N2.Vmimagepreemptiblen2Standardcore
+ memory = p.Gcp.Compute.GCE.VmsPreemptible.MemoryPerGb.N2.Vmimagepreemptiblen2Standardram
case Commitment1YearResource:
- core = p.Gcp.Compute.GCE.VmsCommit1Year.CoresPerCore.Commitmentn2CPU1Yv1
- memory = p.Gcp.Compute.GCE.VmsCommit1Year.MemoryPerGb.Commitmentn2RAM1Yv1
+ core = p.Gcp.Compute.GCE.VmsCommit1Year.CoresPerCore.N2.Commitmentn2CPU1Yv1
+ memory = p.Gcp.Compute.GCE.VmsCommit1Year.MemoryPerGb.N2.Commitmentn2RAM1Yv1
case Commitment3YearResource:
- core = p.Gcp.Compute.GCE.VmsCommit3Year.CoresPerCore.Commitmentn2CPU3Yv1
- memory = p.Gcp.Compute.GCE.VmsCommit3Year.MemoryPerGb.Commitmentn2RAM3Yv1
+ core = p.Gcp.Compute.GCE.VmsCommit3Year.CoresPerCore.N2.Commitmentn2CPU3Yv1
+ memory = p.Gcp.Compute.GCE.VmsCommit3Year.MemoryPerGb.N2.Commitmentn2RAM3Yv1
default:
return Subtype{}, Subtype{}, fmt.Errorf("commitment %q not supported", opts.Commitment)
}
@@ -191,17 +191,17 @@ func typeGetterN2D(p *Pricing, opts Opts) (Subtype, Subtype, error) {
var memory Subtype
switch opts.Commitment {
case OnDemand:
- core = p.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.Vmimagen2Dstandardcore
- memory = p.Gcp.Compute.GCE.VmsOnDemand.MemoryPerGb.Vmimagen2Dstandardram
+ core = p.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.N2D.Vmimagen2Dstandardcore
+ memory = p.Gcp.Compute.GCE.VmsOnDemand.MemoryPerGb.N2D.Vmimagen2Dstandardram
case Spot:
- core = p.Gcp.Compute.GCE.VmsPreemptible.CoresPerCore.Vmimagepreemptiblen2Dstandardcore
- memory = p.Gcp.Compute.GCE.VmsPreemptible.MemoryPerGb.Vmimagepreemptiblen2Dstandardram
+ core = p.Gcp.Compute.GCE.VmsPreemptible.CoresPerCore.N2D.Vmimagepreemptiblen2Dstandardcore
+ memory = p.Gcp.Compute.GCE.VmsPreemptible.MemoryPerGb.N2D.Vmimagepreemptiblen2Dstandardram
case Commitment1YearResource:
- core = p.Gcp.Compute.GCE.VmsCommit1Year.CoresPerCore.Commitmentn2Dcpu1Yv1
- memory = p.Gcp.Compute.GCE.VmsCommit1Year.MemoryPerGb.Commitmentn2Dram1Yv1
+ core = p.Gcp.Compute.GCE.VmsCommit1Year.CoresPerCore.N2D.Commitmentn2Dcpu1Yv1
+ memory = p.Gcp.Compute.GCE.VmsCommit1Year.MemoryPerGb.N2D.Commitmentn2Dram1Yv1
case Commitment3YearResource:
- core = p.Gcp.Compute.GCE.VmsCommit3Year.CoresPerCore.Commitmentn2Dcpu3Yv1
- memory = p.Gcp.Compute.GCE.VmsCommit3Year.MemoryPerGb.Commitmentn2Dram3Yv1
+ core = p.Gcp.Compute.GCE.VmsCommit3Year.CoresPerCore.N2D.Commitmentn2Dcpu3Yv1
+ memory = p.Gcp.Compute.GCE.VmsCommit3Year.MemoryPerGb.N2D.Commitmentn2Dram3Yv1
default:
return Subtype{}, Subtype{}, fmt.Errorf("commitment %q not supported", opts.Commitment)
}
@@ -229,17 +229,17 @@ func typeGetterT2D(p *Pricing, opts Opts) (Subtype, Subtype, error) {
var memory Subtype
switch opts.Commitment {
case OnDemand:
- core = p.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.Vmimaget2Dstandardcore
- memory = p.Gcp.Compute.GCE.VmsOnDemand.MemoryPerGb.Vmimaget2Dstandardram
+ core = p.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.T2D.Vmimaget2Dstandardcore
+ memory = p.Gcp.Compute.GCE.VmsOnDemand.MemoryPerGb.T2D.Vmimaget2Dstandardram
case Spot:
- core = p.Gcp.Compute.GCE.VmsPreemptible.CoresPerCore.Vmimagepreemptiblet2Dstandardcore
- memory = p.Gcp.Compute.GCE.VmsPreemptible.MemoryPerGb.Vmimagepreemptiblet2Dstandardram
+ core = p.Gcp.Compute.GCE.VmsPreemptible.CoresPerCore.T2D.Vmimagepreemptiblet2Dstandardcore
+ memory = p.Gcp.Compute.GCE.VmsPreemptible.MemoryPerGb.T2D.Vmimagepreemptiblet2Dstandardram
case Commitment1YearResource:
- core = p.Gcp.Compute.GCE.VmsCommit1Year.CoresPerCore.Commitmentt2Dcpu1Yv1
- memory = p.Gcp.Compute.GCE.VmsCommit1Year.MemoryPerGb.Commitmentt2Dram1Yv1
+ core = p.Gcp.Compute.GCE.VmsCommit1Year.CoresPerCore.T2D.Commitmentt2Dcpu1Yv1
+ memory = p.Gcp.Compute.GCE.VmsCommit1Year.MemoryPerGb.T2D.Commitmentt2Dram1Yv1
case Commitment3YearResource:
- core = p.Gcp.Compute.GCE.VmsCommit3Year.CoresPerCore.Commitmentt2Dcpu3Yv1
- memory = p.Gcp.Compute.GCE.VmsCommit3Year.MemoryPerGb.Commitmentt2Dram3Yv1
+ core = p.Gcp.Compute.GCE.VmsCommit3Year.CoresPerCore.T2D.Commitmentt2Dcpu3Yv1
+ memory = p.Gcp.Compute.GCE.VmsCommit3Year.MemoryPerGb.T2D.Commitmentt2Dram3Yv1
default:
return Subtype{}, Subtype{}, fmt.Errorf("commitment %q not supported", opts.Commitment)
}
@@ -251,11 +251,11 @@ func typeGetterN1(p *Pricing, opts Opts) (Subtype, Subtype, error) {
var memory Subtype
switch opts.Commitment {
case OnDemand:
- core = p.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.Vmimagen1Standardcore
- memory = p.Gcp.Compute.GCE.VmsOnDemand.MemoryPerGb.Vmimagen1Standardram
+ core = p.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.N1.Vmimagen1Standardcore
+ memory = p.Gcp.Compute.GCE.VmsOnDemand.MemoryPerGb.N1.Vmimagen1Standardram
case Spot:
- core = p.Gcp.Compute.GCE.VmsPreemptible.CoresPerCore.Vmimagepreemptiblen1Standardcore
- memory = p.Gcp.Compute.GCE.VmsPreemptible.MemoryPerGb.Vmimagepreemptiblen1Standardram
+ core = p.Gcp.Compute.GCE.VmsPreemptible.CoresPerCore.N1.Vmimagepreemptiblen1Standardcore
+ memory = p.Gcp.Compute.GCE.VmsPreemptible.MemoryPerGb.N1.Vmimagepreemptiblen1Standardram
default:
return Subtype{}, Subtype{}, fmt.Errorf("commitment %q not supported", opts.Commitment)
}
@@ -267,17 +267,17 @@ func typeGetterC2D(p *Pricing, opts Opts) (Subtype, Subtype, error) {
var memory Subtype
switch opts.Commitment {
case OnDemand:
- core = p.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.Vmimagec2Dstandardcore
- memory = p.Gcp.Compute.GCE.VmsOnDemand.MemoryPerGb.Vmimagec2Dstandardram
+ core = p.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.C2D.Vmimagec2Dstandardcore
+ memory = p.Gcp.Compute.GCE.VmsOnDemand.MemoryPerGb.C2D.Vmimagec2Dstandardram
case Spot:
- core = p.Gcp.Compute.GCE.VmsPreemptible.CoresPerCore.Vmimagepreemptiblec2Dstandardcore
- memory = p.Gcp.Compute.GCE.VmsPreemptible.MemoryPerGb.Vmimagepreemptiblec2Dstandardram
+ core = p.Gcp.Compute.GCE.VmsPreemptible.CoresPerCore.C2D.Vmimagepreemptiblec2Dstandardcore
+ memory = p.Gcp.Compute.GCE.VmsPreemptible.MemoryPerGb.C2D.Vmimagepreemptiblec2Dstandardram
case Commitment1YearResource:
- core = p.Gcp.Compute.GCE.VmsCommit1Year.CoresPerCore.Commitmentc2Dcpu1Yv1
- memory = p.Gcp.Compute.GCE.VmsCommit1Year.MemoryPerGb.Commitmentc2Dram1Yv1
+ core = p.Gcp.Compute.GCE.VmsCommit1Year.CoresPerCore.C2D.Commitmentc2Dcpu1Yv1
+ memory = p.Gcp.Compute.GCE.VmsCommit1Year.MemoryPerGb.C2D.Commitmentc2Dram1Yv1
case Commitment3YearResource:
- core = p.Gcp.Compute.GCE.VmsCommit3Year.CoresPerCore.Commitmentc2Dcpu3Yv1
- memory = p.Gcp.Compute.GCE.VmsCommit3Year.MemoryPerGb.Commitmentc2Dram3Yv1
+ core = p.Gcp.Compute.GCE.VmsCommit3Year.CoresPerCore.C2D.Commitmentc2Dcpu3Yv1
+ memory = p.Gcp.Compute.GCE.VmsCommit3Year.MemoryPerGb.C2D.Commitmentc2Dram3Yv1
default:
return Subtype{}, Subtype{}, fmt.Errorf("commitment %q not supported", opts.Commitment)
}
diff --git a/utils/gcpcomputepricing/machine_test.go b/utils/gcpcomputepricing/machine_test.go
index b771050e0..b22d7ff8c 100644
--- a/utils/gcpcomputepricing/machine_test.go
+++ b/utils/gcpcomputepricing/machine_test.go
@@ -4,62 +4,216 @@ import (
"testing"
)
-func TestGetE2standard2OnDemand(t *testing.T) {
- hourlyRate := getter(t, 2, 8)
-
- if hourlyRate != 67011420 {
- t.Errorf("Hourly rate should be 67011420, instead of %d", hourlyRate)
+func TestCalculateMachineHourly(t *testing.T) {
+ var tests = []struct {
+ name string
+ inputs []interface{}
+ }{
+ {
+ "TestGetE2standard2OnDemand",
+ []interface{}{E2, 2, 8},
+ },
+ {
+ "TestGetE2standard4OnDemand",
+ []interface{}{E2, 4, 16},
+ },
+ {
+ "TestGetE2standard8OnDemand",
+ []interface{}{E2, 8, 32},
+ },
+ {
+ "TestGetE2standard16OnDemand",
+ []interface{}{E2, 16, 64},
+ },
+ {
+ "TestGetE2standard32OnDemand",
+ []interface{}{E2, 32, 128},
+ },
+ {
+ "TestGetC3standard2OnDemand",
+ []interface{}{C3, 2, 8},
+ },
+ {
+ "TestGetC3standard4OnDemand",
+ []interface{}{C3, 4, 16},
+ },
+ {
+ "TestGetC3standard8OnDemand",
+ []interface{}{C3, 8, 32},
+ },
+ {
+ "TestGetC3standard16OnDemand",
+ []interface{}{C3, 16, 64},
+ },
+ {
+ "TestGetC3standard32OnDemand",
+ []interface{}{C3, 32, 128},
+ },
+ {
+ "TestGetN2standard2OnDemand",
+ []interface{}{N2, 2, 8},
+ },
+ {
+ "TestGetN2standard4OnDemand",
+ []interface{}{N2, 4, 16},
+ },
+ {
+ "TestGetN2standard8OnDemand",
+ []interface{}{N2, 8, 32},
+ },
+ {
+ "TestGetN2standard16OnDemand",
+ []interface{}{N2, 16, 64},
+ },
+ {
+ "TestGetN2standard32OnDemand",
+ []interface{}{N2, 32, 128},
+ },
+ {
+ "TestGetN2Dstandard2OnDemand",
+ []interface{}{N2D, 2, 8},
+ },
+ {
+ "TestGetN2Dstandard4OnDemand",
+ []interface{}{N2D, 4, 16},
+ },
+ {
+ "TestGetN2Dstandard8OnDemand",
+ []interface{}{N2D, 8, 32},
+ },
+ {
+ "TestGetN2Dstandard16OnDemand",
+ []interface{}{N2D, 16, 64},
+ },
+ {
+ "TestGetN2Dstandard32OnDemand",
+ []interface{}{N2D, 32, 128},
+ },
+ {
+ "TestGetT2Astandard2OnDemand",
+ []interface{}{T2A, 2, 8},
+ },
+ {
+ "TestGetT2Astandard4OnDemand",
+ []interface{}{T2A, 4, 16},
+ },
+ {
+ "TestGetT2Astandard8OnDemand",
+ []interface{}{T2A, 8, 32},
+ },
+ {
+ "TestGetT2Astandard16OnDemand",
+ []interface{}{T2A, 16, 64},
+ },
+ {
+ "TestGetT2Astandard32OnDemand",
+ []interface{}{T2A, 32, 128},
+ },
+ {
+ "TestGetT2Dstandard2OnDemand",
+ []interface{}{T2D, 2, 8},
+ },
+ {
+ "TestGetT2Dstandard4OnDemand",
+ []interface{}{T2D, 4, 16},
+ },
+ {
+ "TestGetT2Dstandard8OnDemand",
+ []interface{}{T2D, 8, 32},
+ },
+ {
+ "TestGetT2Dstandard16OnDemand",
+ []interface{}{T2D, 16, 64},
+ },
+ {
+ "TestGetT2Dstandard32OnDemand",
+ []interface{}{T2D, 32, 128},
+ },
+ {
+ "TestGetN1standard2OnDemand",
+ []interface{}{N1, 2, 8},
+ },
+ {
+ "TestGetN1standard4OnDemand",
+ []interface{}{N1, 4, 16},
+ },
+ {
+ "TestGetN1standard8OnDemand",
+ []interface{}{N1, 8, 32},
+ },
+ {
+ "TestGetN1standard16OnDemand",
+ []interface{}{N1, 16, 64},
+ },
+ {
+ "TestGetN1standard32OnDemand",
+ []interface{}{N1, 32, 128},
+ },
+ {
+ "TestGetC2Dstandard2OnDemand",
+ []interface{}{C2D, 2, 8},
+ },
+ {
+ "TestGetC2Dstandard4OnDemand",
+ []interface{}{C2D, 4, 16},
+ },
+ {
+ "TestGetC2Dstandard8OnDemand",
+ []interface{}{C2D, 8, 32},
+ },
+ {
+ "TestGetC2Dstandard16OnDemand",
+ []interface{}{C2D, 16, 64},
+ },
+ {
+ "TestGetC2Dstandard32OnDemand",
+ []interface{}{C2D, 32, 128},
+ },
+ {
+ "TestGetM3standard2OnDemand",
+ []interface{}{N2D, 2, 8},
+ },
+ {
+ "TestGetM3standard4OnDemand",
+ []interface{}{N2D, 4, 16},
+ },
+ {
+ "TestGetM3standard8OnDemand",
+ []interface{}{N2D, 8, 32},
+ },
+ {
+ "TestGetM3standard16OnDemand",
+ []interface{}{N2D, 16, 64},
+ },
+ {
+ "TestGetM3standard32OnDemand",
+ []interface{}{N2D, 32, 128},
+ },
}
-}
-func TestGetE2standard4OnDemand(t *testing.T) {
- hourlyRate := getter(t, 4, 16)
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ p, err := Fetch()
- if hourlyRate != 134022840 {
- t.Errorf("Hourly rate should be 134022840, instead of %d", hourlyRate)
- }
-}
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := calculateMachineHourly(p, Opts{
+ Type: tt.inputs[0].(string),
+ Commitment: OnDemand,
+ Region: "us-west1",
+ NumOfCPU: uint64(tt.inputs[1].(int)),
+ NumOfMemory: uint64(tt.inputs[2].(int)),
+ })
-func TestGetE2standard8OnDemand(t *testing.T) {
- hourlyRate := getter(t, 8, 32)
+ if err != nil {
+ t.Fatal(err)
+ }
- if hourlyRate != 268045680 {
- t.Errorf("Hourly rate should be 268045680, instead of %d", hourlyRate)
- }
-}
-
-func TestGetE2standard16OnDemand(t *testing.T) {
- hourlyRate := getter(t, 16, 64)
+ if got <= 0 {
+ t.Errorf("Hourly rate should be greater than 0, but is %d", got)
+ }
+ })
- if hourlyRate != 536091360 {
- t.Errorf("Hourly rate should be 536091360, instead of %d", hourlyRate)
}
}
-
-func TestGetE2standard32OnDemand(t *testing.T) {
- hourlyRate := getter(t, 32, 128)
-
- if hourlyRate != 1072182720 {
- t.Errorf("Hourly rate should be 1072182720, instead of %d", hourlyRate)
- }
-}
-
-func getter(t *testing.T, cpu, memory uint64) uint64 {
- p, err := Fetch()
- if err != nil {
- t.Fatal(err)
- }
-
- hourlyRate, err := calculateMachineHourly(p, Opts{
- Type: E2,
- Commitment: OnDemand,
- Region: "us-west1",
- NumOfCPU: cpu,
- NumOfMemory: memory,
- })
- if err != nil {
- t.Fatal(err)
- }
-
- return hourlyRate
-}
diff --git a/utils/gcpcomputepricing/main_test.go b/utils/gcpcomputepricing/main_test.go
index 471336d6f..9833513f7 100644
--- a/utils/gcpcomputepricing/main_test.go
+++ b/utils/gcpcomputepricing/main_test.go
@@ -10,7 +10,7 @@ func TestGet(t *testing.T) {
t.Fatal(err)
}
- if pricing.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.Vmimagee2Core.Regions["us-central1"].Prices[0].Nanos != 21811590 {
+ if pricing.Gcp.Compute.GCE.VmsOnDemand.CoresPerCore.E2.Vmimagee2Core.Regions["us-central1"].Prices[0].Nanos != 21811590 {
t.Error("Broken")
}
}
diff --git a/utils/gcpcomputepricing/types_gce.go b/utils/gcpcomputepricing/types_gce.go
index 8b1358b98..a5acfd2bb 100644
--- a/utils/gcpcomputepricing/types_gce.go
+++ b/utils/gcpcomputepricing/types_gce.go
@@ -51,30 +51,54 @@ type VmsOnDemandMemoryPerGb struct {
Vmimagea2Highgpuram Subtype `json:"vmimagea2highgpuram"`
Vmimagec2Dcustomextendedram Subtype `json:"vmimagec2dcustomextendedram"`
Vmimagec2Dcustomram Subtype `json:"vmimagec2dcustomram"`
- Vmimagec2Dstandardram Subtype `json:"vmimagec2dstandardram"`
+ C2D Vmimagec2DstandardramC2D `json:"c2d"`
C3 VmsOnDemandMemoryPerGbC3 `json:"c3"`
Vmimagecomputeoptimizedram Subtype `json:"vmimagecomputeoptimizedram"`
Vmimagecustomextendedram Subtype `json:"vmimagecustomextendedram"`
Vmimagecustomram Subtype `json:"vmimagecustomram"`
- Vmimagee2RAM Subtype `json:"vmimagee2ram"`
+ E2 Vmimagee2RAME2 `json:"e2"`
G2 VmsOnDemandMemoryPerGbG2 `json:"g2"`
Vmimagelargeram Subtype `json:"vmimagelargeram"`
Vmimagelargerammemoryoptimizedupgradepremium Subtype `json:"vmimagelargerammemoryoptimizedupgradepremium"`
M3 VmsOnDemandMemoryPerGbM3 `json:"m3"`
- Vmimagen1Standardram Subtype `json:"vmimagen1standardram"`
+ N1 Vmimagen1StandardramN1 `json:"n1"`
Vmimagen2Customextendedram Subtype `json:"vmimagen2customextendedram"`
Vmimagen2Customram Subtype `json:"vmimagen2customram"`
Vmimagen2Dcustomextendedram Subtype `json:"vmimagen2dcustomextendedram"`
Vmimagen2Dcustomram Subtype `json:"vmimagen2dcustomram"`
Vmimagen2Dsoletenancyram Subtype `json:"vmimagen2dsoletenancyram"`
Vmimagen2Dsoletenancyramsoletenancypremium Subtype `json:"vmimagen2dsoletenancyramsoletenancypremium"`
- Vmimagen2Dstandardram Subtype `json:"vmimagen2dstandardram"`
+ N2D Vmimagen2DstandardramN2D `json:"n2d"`
Vmimagen2Soletenancyram Subtype `json:"vmimagen2soletenancyram"`
- Vmimagen2Standardram Subtype `json:"vmimagen2standardram"`
+ N2 Vmimagen2StandardramN2 `json:"n2"`
Vmimagesoletenancyram Subtype `json:"vmimagesoletenancyram"`
Vmimagesoletenancyramsoletenancypremium Subtype `json:"vmimagesoletenancyramsoletenancypremium"`
T2A VmsOnDemandMemoryPerGbT2A `json:"t2a"`
- Vmimaget2Dstandardram Subtype `json:"vmimaget2dstandardram"`
+ T2D Vmimaget2DstandardramT2D `json:"t2d"`
+}
+
+type Vmimagec2DstandardramC2D struct {
+ Vmimagec2Dstandardram Subtype `json:"vmimagec2dstandardram"`
+}
+
+type Vmimagen1StandardramN1 struct {
+ Vmimagen1Standardram Subtype `json:"vmimagen1standardram"`
+}
+
+type Vmimagen2DstandardramN2D struct {
+ Vmimagen2Dstandardram Subtype `json:"vmimagen2dstandardram"`
+}
+
+type Vmimaget2DstandardramT2D struct {
+ Vmimaget2Dstandardram Subtype `json:"vmimaget2dstandardram"`
+}
+
+type Vmimagen2StandardramN2 struct {
+ Vmimagen2Standardram Subtype `json:"vmimagen2standardram"`
+}
+
+type Vmimagee2RAME2 struct {
+ Vmimagee2RAM Subtype `json:"vmimagee2ram"`
}
type VmsOnDemandMemoryPerGbC3 struct {
@@ -102,31 +126,55 @@ type VmsOnDemandMemoryPerGbT2A struct {
type VmsOnDemandCoresPerCore struct {
Vmimagea2Highgpucore Subtype `json:"vmimagea2highgpucore"`
Vmimagec2Dcustomcore Subtype `json:"vmimagec2dcustomcore"`
- Vmimagec2Dstandardcore Subtype `json:"vmimagec2dstandardcore"`
+ C2D Vmimagec2DstandardcoreC2D `json:"c2d"`
C3 VmsOnDemandCoresPerCoreC3 `json:"c3"`
+ E2 Vmimagee2CoreE2 `json:"e2"`
Vmimagecomputeoptimizedcore Subtype `json:"vmimagecomputeoptimizedcore"`
Vmimagecustomcore Subtype `json:"vmimagecustomcore"`
- Vmimagee2Core Subtype `json:"vmimagee2core"`
Vmimagef1Micro Subtype `json:"vmimagef1micro"`
Vmimageg1Small Subtype `json:"vmimageg1small"`
G2 VmsOnDemandCoresPerCoreG2 `json:"g2"`
Vmimagelargecore Subtype `json:"vmimagelargecore"`
Vmimagelargecorememoryoptimizedupgradepremium Subtype `json:"vmimagelargecorememoryoptimizedupgradepremium"`
M3 VmsOnDemandCoresPerCoreM3 `json:"m3"`
- Vmimagen1Standardcore Subtype `json:"vmimagen1standardcore"`
+ N1 Vmimagen1StandardcoreN1 `json:"n1"`
Vmimagen2Customcore Subtype `json:"vmimagen2customcore"`
Vmimagen2Customextendedcore Subtype `json:"vmimagen2customextendedcore"`
Vmimagen2Dcustomcore Subtype `json:"vmimagen2dcustomcore"`
Vmimagen2Dsoletenancycore Subtype `json:"vmimagen2dsoletenancycore"`
Vmimagen2Dsoletenancycoresoletenancypremium Subtype `json:"vmimagen2dsoletenancycoresoletenancypremium"`
- Vmimagen2Dstandardcore Subtype `json:"vmimagen2dstandardcore"`
+ N2D Vmimagen2DstandardcoreN2D `json:"n2d"`
Vmimagen2Soletenancycore Subtype `json:"vmimagen2soletenancycore"`
Vmimagen2Soletenancycoresoletenancypremium Subtype `json:"vmimagen2soletenancycoresoletenancypremium"`
- Vmimagen2Standardcore Subtype `json:"vmimagen2standardcore"`
+ N2 Vmimagen2StandardcoreN2 `json:"n2"`
Vmimagesoletenancycore Subtype `json:"vmimagesoletenancycore"`
Vmimagesoletenancycoresoletenancypremium Subtype `json:"vmimagesoletenancycoresoletenancypremium"`
T2A VmsOnDemandCoresPerCoreT2A `json:"t2a"`
- Vmimaget2Dstandardcore Subtype `json:"vmimaget2dstandardcore"`
+ T2D Vmimaget2DstandardcoreT2D `json:"t2d"`
+}
+
+type Vmimagec2DstandardcoreC2D struct {
+ Vmimagec2Dstandardcore Subtype `json:"vmimagec2dstandardcore"`
+}
+
+type Vmimagen1StandardcoreN1 struct {
+ Vmimagen1Standardcore Subtype `json:"vmimagen1standardcore"`
+}
+
+type Vmimagen2DstandardcoreN2D struct {
+ Vmimagen2Dstandardcore Subtype `json:"vmimagen2dstandardcore"`
+}
+
+type Vmimaget2DstandardcoreT2D struct {
+ Vmimaget2Dstandardcore Subtype `json:"vmimaget2dstandardcore"`
+}
+
+type Vmimagen2StandardcoreN2 struct {
+ Vmimagen2Standardcore Subtype `json:"vmimagen2standardcore"`
+}
+
+type Vmimagee2CoreE2 struct {
+ Vmimagee2Core Subtype `json:"vmimagee2core"`
}
type VmsOnDemandCoresPerCoreC3 struct {
@@ -173,27 +221,51 @@ type VmsPreemptibleHighcpu struct {
}
type VmsPreemptibleMemoryPerGb struct {
- Vmimagepreemptiblea2Highgpuram Subtype `json:"vmimagepreemptiblea2highgpuram"`
- Vmimagepreemptiblec2Dcustomextendedram Subtype `json:"vmimagepreemptiblec2dcustomextendedram"`
- Vmimagepreemptiblec2Dcustomram Subtype `json:"vmimagepreemptiblec2dcustomram"`
- Vmimagepreemptiblec2Dstandardram Subtype `json:"vmimagepreemptiblec2dstandardram"`
- C3 VmsPreemptibleMemoryPerGbC3 `json:"c3"`
- Vmimagepreemptiblecomputeoptimizedram Subtype `json:"vmimagepreemptiblecomputeoptimizedram"`
- Vmimagepreemptiblecustomextendedram Subtype `json:"vmimagepreemptiblecustomextendedram"`
- Vmimagepreemptiblecustomram Subtype `json:"vmimagepreemptiblecustomram"`
- Vmimagepreemptiblee2RAM Subtype `json:"vmimagepreemptiblee2ram"`
- G2 VmsPreemptibleMemoryPerGbG2 `json:"g2"`
- Vmimagepreemptiblelargeram Subtype `json:"vmimagepreemptiblelargeram"`
- M3 VmsPreemptibleMemoryPerGbM3 `json:"m3"`
- Vmimagepreemptiblen1Standardram Subtype `json:"vmimagepreemptiblen1standardram"`
- Vmimagepreemptiblen2Customextendedram Subtype `json:"vmimagepreemptiblen2customextendedram"`
- Vmimagepreemptiblen2Customram Subtype `json:"vmimagepreemptiblen2customram"`
- Vmimagepreemptiblen2Dcustomextendedram Subtype `json:"vmimagepreemptiblen2dcustomextendedram"`
- Vmimagepreemptiblen2Dcustomram Subtype `json:"vmimagepreemptiblen2dcustomram"`
- Vmimagepreemptiblen2Dstandardram Subtype `json:"vmimagepreemptiblen2dstandardram"`
- Vmimagepreemptiblen2Standardram Subtype `json:"vmimagepreemptiblen2standardram"`
- T2A VmsPreemptibleMemoryPerGbT2A `json:"t2a"`
- Vmimagepreemptiblet2Dstandardram Subtype `json:"vmimagepreemptiblet2dstandardram"`
+ Vmimagepreemptiblea2Highgpuram Subtype `json:"vmimagepreemptiblea2highgpuram"`
+ Vmimagepreemptiblec2Dcustomextendedram Subtype `json:"vmimagepreemptiblec2dcustomextendedram"`
+ Vmimagepreemptiblec2Dcustomram Subtype `json:"vmimagepreemptiblec2dcustomram"`
+ C2D Vmimagepreemptiblec2DstandardramC2D `json:"c2d"`
+ C3 VmsPreemptibleMemoryPerGbC3 `json:"c3"`
+ Vmimagepreemptiblecomputeoptimizedram Subtype `json:"vmimagepreemptiblecomputeoptimizedram"`
+ Vmimagepreemptiblecustomextendedram Subtype `json:"vmimagepreemptiblecustomextendedram"`
+ Vmimagepreemptiblecustomram Subtype `json:"vmimagepreemptiblecustomram"`
+ E2 Vmimagepreemptiblee2RAME2 `json:"e2"`
+ G2 VmsPreemptibleMemoryPerGbG2 `json:"g2"`
+ Vmimagepreemptiblelargeram Subtype `json:"vmimagepreemptiblelargeram"`
+ M3 VmsPreemptibleMemoryPerGbM3 `json:"m3"`
+ N1 Vmimagepreemptiblen1StandardramN1 `json:"vmimagepreemptiblen1standardram"`
+ Vmimagepreemptiblen2Customextendedram Subtype `json:"vmimagepreemptiblen2customextendedram"`
+ Vmimagepreemptiblen2Customram Subtype `json:"vmimagepreemptiblen2customram"`
+ Vmimagepreemptiblen2Dcustomextendedram Subtype `json:"vmimagepreemptiblen2dcustomextendedram"`
+ Vmimagepreemptiblen2Dcustomram Subtype `json:"vmimagepreemptiblen2dcustomram"`
+ N2D Vmimagepreemptiblen2DstandardramN2D `json:"n2d"`
+ N2 Vmimagepreemptiblen2StandardramN2 `json:"n2"`
+ T2A VmsPreemptibleMemoryPerGbT2A `json:"t2a"`
+ T2D Vmimagepreemptiblet2DstandardramT2D `json:"t2d"`
+}
+
+type Vmimagepreemptiblec2DstandardramC2D struct {
+ Vmimagepreemptiblec2Dstandardram Subtype `json:"vmimagepreemptiblec2dstandardram"`
+}
+
+type Vmimagepreemptiblen1StandardramN1 struct {
+ Vmimagepreemptiblen1Standardram Subtype `json:"vmimagepreemptiblen1standardram"`
+}
+
+type Vmimagepreemptiblen2DstandardramN2D struct {
+ Vmimagepreemptiblen2Dstandardram Subtype `json:"vmimagepreemptiblen2dstandardram"`
+}
+
+type Vmimagepreemptiblet2DstandardramT2D struct {
+ Vmimagepreemptiblet2Dstandardram Subtype `json:"vmimagepreemptiblet2dstandardram"`
+}
+
+type Vmimagepreemptiblen2StandardramN2 struct {
+ Vmimagepreemptiblen2Standardram Subtype `json:"vmimagepreemptiblen2standardram"`
+}
+
+type Vmimagepreemptiblee2RAME2 struct {
+ Vmimagepreemptiblee2RAM Subtype `json:"vmimagepreemptiblee2ram"`
}
type VmsPreemptibleMemoryPerGbC3 struct {
@@ -213,26 +285,49 @@ type VmsPreemptibleMemoryPerGbT2A struct {
}
type VmsPreemptibleCoresPerCore struct {
- Vmimagepreemptiblec2Dcustomcore Subtype `json:"vmimagepreemptiblec2dcustomcore"`
- Vmimagepreemptiblec2Dstandardcore Subtype `json:"vmimagepreemptiblec2dstandardcore"`
- C3 VmsPreemptibleCoresPerCoreC3 `json:"c3"`
- Vmimagepreemptiblecomputeoptimizedcore Subtype `json:"vmimagepreemptiblecomputeoptimizedcore"`
- Vmimagepreemptiblecustomcore Subtype `json:"vmimagepreemptiblecustomcore"`
- Vmimagepreemptiblecustomextendedcore Subtype `json:"vmimagepreemptiblecustomextendedcore"`
- Vmimagepreemptiblee2Core Subtype `json:"vmimagepreemptiblee2core"`
- Vmimagepreemptiblef1Micro Subtype `json:"vmimagepreemptiblef1micro"`
- Vmimagepreemptibleg1Small Subtype `json:"vmimagepreemptibleg1small"`
- G2 VmsPreemptibleCoresPerCoreG2 `json:"g2"`
- Vmimagepreemptiblelargecore Subtype `json:"vmimagepreemptiblelargecore"`
- M3 VmsPreemptibleCoresPerCoreM3 `json:"m3"`
- Vmimagepreemptiblen1Standardcore Subtype `json:"vmimagepreemptiblen1standardcore"`
- Vmimagepreemptiblen2Customcore Subtype `json:"vmimagepreemptiblen2customcore"`
- Vmimagepreemptiblen2Customextendedcore Subtype `json:"vmimagepreemptiblen2customextendedcore"`
- Vmimagepreemptiblen2Dcustomcore Subtype `json:"vmimagepreemptiblen2dcustomcore"`
- Vmimagepreemptiblen2Dstandardcore Subtype `json:"vmimagepreemptiblen2dstandardcore"`
- Vmimagepreemptiblen2Standardcore Subtype `json:"vmimagepreemptiblen2standardcore"`
- T2A VmsPreemptibleCoresPerCoreT2A `json:"t2a"`
- Vmimagepreemptiblet2Dstandardcore Subtype `json:"vmimagepreemptiblet2dstandardcore"`
+ Vmimagepreemptiblec2Dcustomcore Subtype `json:"vmimagepreemptiblec2dcustomcore"`
+ C2D Vmimagepreemptiblec2DstandardcoreC2D `json:"c2d"`
+ C3 VmsPreemptibleCoresPerCoreC3 `json:"c3"`
+ Vmimagepreemptiblecomputeoptimizedcore Subtype `json:"vmimagepreemptiblecomputeoptimizedcore"`
+ Vmimagepreemptiblecustomcore Subtype `json:"vmimagepreemptiblecustomcore"`
+ Vmimagepreemptiblecustomextendedcore Subtype `json:"vmimagepreemptiblecustomextendedcore"`
+ E2 Vmimagepreemptiblee2CoreC2 `json:"e2"`
+ Vmimagepreemptiblef1Micro Subtype `json:"vmimagepreemptiblef1micro"`
+ Vmimagepreemptibleg1Small Subtype `json:"vmimagepreemptibleg1small"`
+ G2 VmsPreemptibleCoresPerCoreG2 `json:"g2"`
+ Vmimagepreemptiblelargecore Subtype `json:"vmimagepreemptiblelargecore"`
+ M3 VmsPreemptibleCoresPerCoreM3 `json:"m3"`
+ N1 Vmimagepreemptiblen1StandardcoreN1 `json:"n1"`
+ Vmimagepreemptiblen2Customcore Subtype `json:"vmimagepreemptiblen2customcore"`
+ Vmimagepreemptiblen2Customextendedcore Subtype `json:"vmimagepreemptiblen2customextendedcore"`
+ Vmimagepreemptiblen2Dcustomcore Subtype `json:"vmimagepreemptiblen2dcustomcore"`
+ N2D Vmimagepreemptiblen2DstandardcoreN2D `json:"n2d"`
+ N2 Vmimagepreemptiblen2StandardcoreN2 `json:"n2"`
+ T2A VmsPreemptibleCoresPerCoreT2A `json:"t2a"`
+ T2D Vmimagepreemptiblet2DstandardcoreT2D `json:"t2d"`
+}
+
+type Vmimagepreemptiblec2DstandardcoreC2D struct {
+ Vmimagepreemptiblec2Dstandardcore Subtype `json:"vmimagepreemptiblec2dstandardcore"`
+}
+
+type Vmimagepreemptiblen1StandardcoreN1 struct {
+ Vmimagepreemptiblen1Standardcore Subtype `json:"vmimagepreemptiblen1standardcore"`
+}
+type Vmimagepreemptiblen2DstandardcoreN2D struct {
+ Vmimagepreemptiblen2Dstandardcore Subtype `json:"vmimagepreemptiblen2dstandardcore"`
+}
+
+type Vmimagepreemptiblet2DstandardcoreT2D struct {
+ Vmimagepreemptiblet2Dstandardcore Subtype `json:"vmimagepreemptiblet2dstandardcore"`
+}
+
+type Vmimagepreemptiblen2StandardcoreN2 struct {
+ Vmimagepreemptiblen2Standardcore Subtype `json:"vmimagepreemptiblen2standardcore"`
+}
+
+type Vmimagepreemptiblee2CoreC2 struct {
+ Vmimagepreemptiblee2Core Subtype `json:"vmimagepreemptiblee2core"`
}
type VmsPreemptibleCoresPerCoreC3 struct {
@@ -259,17 +354,38 @@ type VmsCommit1Year struct {
type VmsCommit1YearCoresPerCore struct {
Commitmenta2Highgpucpu1Yv1 Subtype `json:"commitmenta2highgpucpu1yv1"`
- Commitmentc2Dcpu1Yv1 Subtype `json:"commitmentc2dcpu1yv1"`
+ C2D Commitmentc2Dcpu1Yv1C2D `json:"c2d"`
C3 VmsCommit1YearCoresPerCoreC3 `json:"c3"`
Commitmentcpucomputeoptimized1Yv1 Subtype `json:"commitmentcpucomputeoptimized1yv1"`
Commitmentcpulargeinstance1Yv1 Subtype `json:"commitmentcpulargeinstance1yv1"`
Commitmentcpu1Yv1 Subtype `json:"commitmentcpu1yv1"`
- Commitmente2CPU1Yv1 Subtype `json:"commitmente2cpu1yv1"`
+ N2D Commitmentn2Dcpu1Yv1N2D `json:"n2d"`
+ E2 Commitmente2CPU1Yv1E2 `json:"e2"`
G2 VmsCommit1YearCoresPerCoreG2 `json:"g2"`
M3 VmsCommit1YearCoresPerCoreM3 `json:"m3"`
- Commitmentn2CPU1Yv1 Subtype `json:"commitmentn2cpu1yv1"`
- Commitmentn2Dcpu1Yv1 Subtype `json:"commitmentn2dcpu1yv1"`
- Commitmentt2Dcpu1Yv1 Subtype `json:"commitmentt2dcpu1yv1"`
+ N2 Commitmentn2CPU1Yv1N2 `json:"n2"`
+ T2D Commitmentt2Dcpu1Yv1T2D `json:"t2d"`
+
+}
+
+type Commitmentc2Dcpu1Yv1C2D struct {
+ Commitmentc2Dcpu1Yv1 Subtype `json:"commitmentc2dcpu1yv1"`
+}
+
+type Commitmentn2Dcpu1Yv1N2D struct {
+ Commitmentn2Dcpu1Yv1 Subtype `json:"commitment2dcpu1yv1"`
+}
+
+type Commitmentt2Dcpu1Yv1T2D struct {
+ Commitmentt2Dcpu1Yv1 Subtype `json:"commitmentt2dcpu1yv1"`
+}
+
+type Commitmentn2CPU1Yv1N2 struct {
+ Commitmentn2CPU1Yv1 Subtype `json:"commitmentn2cpu1yv1"`
+}
+
+type Commitmente2CPU1Yv1E2 struct {
+ Commitmente2CPU1Yv1 Subtype `json:"commitmente2cpu1yv1"`
}
type VmsCommit1YearCoresPerCoreC3 struct {
@@ -286,17 +402,37 @@ type VmsCommit1YearCoresPerCoreM3 struct {
type VmsCommit1YearMemoryPerGb struct {
Commitmenta2Highgpuram1Yv1 Subtype `json:"commitmenta2highgpuram1yv1"`
- Commitmentc2Dram1Yv1 Subtype `json:"commitmentc2dram1yv1"`
+ C2D Commitmentc2Dram1Yv1C2D `json:"c2d"`
C3 VmsCommit1YearMemoryPerGbC3 `json:"c3"`
- Commitmente2RAM1Yv1 Subtype `json:"commitmente2ram1yv1"`
+ E2 Commitmente2RAM1Yv1E2 `json:"e2"`
G2 VmsCommit1YearMemoryPerGbG2 `json:"g2"`
M3 VmsCommit1YearMemoryPerGbM3 `json:"m3"`
- Commitmentn2Dram1Yv1 Subtype `json:"commitmentn2dram1yv1"`
- Commitmentn2RAM1Yv1 Subtype `json:"commitmentn2ram1yv1"`
+ N2D Commitmentn2Dram1Yv1N2D `json:"n2d"`
+ N2 Commitmentn2RAM1Yv1N2 `json:"n2"`
Commitmentramcomputeoptimized1Yv1 Subtype `json:"commitmentramcomputeoptimized1yv1"`
Commitmentramlargeinstance1Yv1 Subtype `json:"commitmentramlargeinstance1yv1"`
Commitmentram1Yv1 Subtype `json:"commitmentram1yv1"`
- Commitmentt2Dram1Yv1 Subtype `json:"commitmentt2dram1yv1"`
+ T2D Commitmentt2Dram1Yv1T2D `json:"t2d"`
+}
+
+type Commitmentc2Dram1Yv1C2D struct {
+ Commitmentc2Dram1Yv1 Subtype `json:"commitmentc2dram1yv1"`
+}
+
+type Commitmentn2Dram1Yv1N2D struct {
+ Commitmentn2Dram1Yv1 Subtype `json:"commitmentn2dram1yv1"`
+}
+
+type Commitmentt2Dram1Yv1T2D struct {
+ Commitmentt2Dram1Yv1 Subtype `json:"commitmentt2dram1yv1"`
+}
+
+type Commitmentn2RAM1Yv1N2 struct {
+ Commitmentn2RAM1Yv1 Subtype `json:"commitmentn2ram1yv1"`
+}
+
+type Commitmente2RAM1Yv1E2 struct {
+ Commitmente2RAM1Yv1 Subtype `json:"commitmente2ram1yv1"`
}
type VmsCommit1YearMemoryPerGbC3 struct {
@@ -319,18 +455,38 @@ type VmsCommit3Year struct {
type VmsCommit3YearCoresPerCore struct {
Commitmenta2Highgpucpu3Yv1 Subtype `json:"commitmenta2highgpucpu3yv1"`
- Commitmentc2Dcpu3Yv1 Subtype `json:"commitmentc2dcpu3yv1"`
+ C2D Commitmentc2Dcpu3Yv1C2D `json:"c2d"`
C3 VmsCommit3YearCoresPerCoreC3 `json:"c3"`
Commitmentcpucomputeoptimized3Yv1 Subtype `json:"commitmentcpucomputeoptimized3yv1"`
Commitmentcpulargeinstance1Yv1 Subtype `json:"commitmentcpulargeinstance1yv1"`
Commitmentcpulargeinstance3Yv1 Subtype `json:"commitmentcpulargeinstance3yv1"`
Commitmentcpu3Yv1 Subtype `json:"commitmentcpu3yv1"`
- Commitmente2CPU3Yv1 Subtype `json:"commitmente2cpu3yv1"`
+ E2 Commitmente2CPU3Yv1E2 `json:"e2"`
G2 VmsCommit3YearCoresPerCoreG2 `json:"g2"`
M3 VmsCommit3YearCoresPerCoreM3 `json:"m3"`
- Commitmentn2CPU3Yv1 Subtype `json:"commitmentn2cpu3yv1"`
- Commitmentn2Dcpu3Yv1 Subtype `json:"commitmentn2dcpu3yv1"`
- Commitmentt2Dcpu3Yv1 Subtype `json:"commitmentt2dcpu3yv1"`
+ N2 Commitmentn2CPU3Yv1N2 `json:"n2"`
+ N2D Commitmentn2Dcpu3Yv1N2D `json:"n2d"`
+ T2D Commitmentt2Dcpu3Yv1T2D `json:"t2d"`
+}
+
+type Commitmentc2Dcpu3Yv1C2D struct {
+ Commitmentc2Dcpu3Yv1 Subtype `json:"commitmentc2dcpu3yv1"`
+}
+
+type Commitmentn2Dcpu3Yv1N2D struct {
+ Commitmentn2Dcpu3Yv1 Subtype `json:"commitmentn2dcpu3yv1"`
+}
+
+type Commitmentt2Dcpu3Yv1T2D struct {
+ Commitmentt2Dcpu3Yv1 Subtype `json:"commitmentt2dcpu3yv1"`
+}
+
+type Commitmentn2CPU3Yv1N2 struct {
+ Commitmentn2CPU3Yv1 Subtype `json:"commitmentn2cpu3yv1"`
+}
+
+type Commitmente2CPU3Yv1E2 struct {
+ Commitmente2CPU3Yv1 Subtype `json:"commitmente2cpu3yv1"`
}
type VmsCommit3YearCoresPerCoreC3 struct {
@@ -347,17 +503,37 @@ type VmsCommit3YearCoresPerCoreM3 struct {
type VmsCommit3YearMemoryPerGb struct {
Commitmenta2Highgpuram3Yv1 Subtype `json:"commitmenta2highgpuram3yv1"`
- Commitmentc2Dram3Yv1 Subtype `json:"commitmentc2dram3yv1"`
+ C2D Commitmentc2Dram3Yv1C2D `json:"c2d"`
C3 VmsCommit3YearMemoryPerGbC3 `json:"c3"`
- Commitmente2RAM3Yv1 Subtype `json:"commitmente2ram3yv1"`
+ E2 Commitmente2RAM3Yv1E2 `json:"e2"`
G2 VmsCommit3YearMemoryPerGbG2 `json:"g2"`
M3 VmsCommit3YearMemoryPerGbM3 `json:"m3"`
- Commitmentn2Dram3Yv1 Subtype `json:"commitmentn2dram3yv1"`
- Commitmentn2RAM3Yv1 Subtype `json:"commitmentn2ram3yv1"`
+ N2D Commitmentn2Dram3Yv1N2D `json:"n2d"`
+ N2 Commitmentn2RAM3Yv1N2 `json:"commitmentn2ram3yv1"`
Commitmentramcomputeoptimized3Yv1 Subtype `json:"commitmentramcomputeoptimized3yv1"`
Commitmentramlargeinstance3Yv1 Subtype `json:"commitmentramlargeinstance3yv1"`
Commitmentram3Yv1 Subtype `json:"commitmentram3yv1"`
- Commitmentt2Dram3Yv1 Subtype `json:"commitmentt2dram3yv1"`
+ T2D Commitmentt2Dram3Yv1T2D `json:"commitmentt2dram3yv1"`
+}
+
+type Commitmentc2Dram3Yv1C2D struct {
+ Commitmentc2Dram3Yv1 Subtype `json:"commitmentc2dram3yv1"`
+}
+
+type Commitmentn2Dram3Yv1N2D struct {
+ Commitmentn2Dram3Yv1 Subtype `json:"commitmentn2dram3yv1"`
+}
+
+type Commitmentt2Dram3Yv1T2D struct {
+ Commitmentt2Dram3Yv1 Subtype `json:"commitmentt2dram3yv1"`
+}
+
+type Commitmentn2RAM3Yv1N2 struct {
+ Commitmentn2RAM3Yv1 Subtype `json:"commitmentn2ram3yv1"`
+}
+
+type Commitmente2RAM3Yv1E2 struct {
+ Commitmente2RAM3Yv1 Subtype `json:"commitmente2ram3yv1"`
}
type VmsCommit3YearMemoryPerGbC3 struct {