From 70c82be02be20a9f97e96283d104be15ae4f65c3 Mon Sep 17 00:00:00 2001 From: Axel Koehler Date: Wed, 1 Dec 2021 16:43:43 +0100 Subject: [PATCH 1/7] [azure] Simple endpoint to list actual costs for all resource groups in the past seven days for a single subscription. --- .gitignore | 3 + go.mod | 5 +- go.sum | 6 +- plugins/azure/azure.go | 10 ++- plugins/azure/costmanagement.go | 31 ++++++++ .../instance/costmanagement/costmanagement.go | 76 +++++++++++++++++++ plugins/azure/pkg/instance/instance.go | 4 + 7 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 plugins/azure/costmanagement.go create mode 100644 plugins/azure/pkg/instance/costmanagement/costmanagement.go diff --git a/.gitignore b/.gitignore index a14d7610f..fab2035c7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ node_modules .env.development.local .env.test.local .env.production.local +*.iml +.idea +config.yaml npm-debug.log* yarn-debug.log* diff --git a/go.mod b/go.mod index 5e126d052..112b5a20b 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,13 @@ module github.com/kobsio/kobs go 1.17 require ( - github.com/Azure/azure-sdk-for-go v59.3.0+incompatible + github.com/Azure/azure-sdk-for-go v59.4.0+incompatible github.com/Azure/azure-sdk-for-go/sdk/azcore v0.20.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.12.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v0.2.0 github.com/Azure/go-autorest/autorest v0.11.19 github.com/Azure/go-autorest/autorest/azure/auth v0.5.9 + github.com/Azure/go-autorest/autorest/date v0.3.0 github.com/ClickHouse/clickhouse-go v1.5.1 github.com/fluxcd/helm-controller/api v0.13.0 github.com/fluxcd/kustomize-controller/api v0.18.0 @@ -41,7 +42,6 @@ require ( github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/adal v0.9.14 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect @@ -96,6 +96,7 @@ require ( github.com/prometheus/procfs v0.6.0 // indirect github.com/rs/xid v1.2.1 // indirect github.com/rs/zerolog v1.20.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect github.com/stretchr/objx v0.2.0 // indirect github.com/vjeantet/grok v1.0.0 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect diff --git a/go.sum b/go.sum index 691a62f71..0dc73b73f 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go v59.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v59.3.0+incompatible h1:dPIm0BO4jsMXFcCI/sLTPkBtE7mk8WMuRHA0JeWhlcQ= -github.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v59.4.0+incompatible h1:gDA8odnngdNd3KYHL2NoK1j9vpWBgEnFSjKKLpkC8Aw= +github.com/Azure/azure-sdk-for-go v59.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.20.0 h1:KQgdWmEOmaJKxaUUZwHAYh12t+b+ZJf8q3friycK1kA= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.20.0/go.mod h1:ZPW/Z0kLCTdDZaDbYTetxc9Cxl/2lNqxYHYNOF2bti0= @@ -622,6 +622,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= diff --git a/plugins/azure/azure.go b/plugins/azure/azure.go index 26823d6a5..5a0b7537c 100644 --- a/plugins/azure/azure.go +++ b/plugins/azure/azure.go @@ -43,12 +43,12 @@ func Register(clusters *clusters.Clusters, plugins *plugin.Plugins, config Confi var instances []*instance.Instance for _, cfg := range config { - instance, err := instance.New(cfg) + inst, err := instance.New(cfg) if err != nil { - log.WithError(err).WithFields(logrus.Fields{"name": cfg.Name}).Fatalf("Could not create Azure instance") + log.WithError(err).WithFields(logrus.Fields{"name": cfg.Name}).Fatalf("Could not create Azure inst") } - instances = append(instances, instance) + instances = append(instances, inst) plugins.Append(plugin.Plugin{ Name: cfg.Name, @@ -74,6 +74,10 @@ func Register(clusters *clusters.Clusters, plugins *plugin.Plugins, config Confi containerInstancesRouter.Get("/containergroup/logs", router.getContainerLogs) containerInstancesRouter.Put("/containergroup/restart", router.restartContainerGroup) }) + + r.Route("/costmanagment", func(costManagementRouter chi.Router) { + costManagementRouter.Get("/actualCost", router.getActualCost) + }) }) return router diff --git a/plugins/azure/costmanagement.go b/plugins/azure/costmanagement.go new file mode 100644 index 000000000..046e33a5d --- /dev/null +++ b/plugins/azure/costmanagement.go @@ -0,0 +1,31 @@ +package azure + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/kobsio/kobs/pkg/api/middleware/errresponse" + "github.com/sirupsen/logrus" +) + +func (router *Router) getActualCost(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + + log.WithFields(logrus.Fields{"name": name}).Tracef("getActualCost") + + i := router.getInstance(name) + if i == nil { + errresponse.Render(w, r, nil, http.StatusBadRequest, "Could not find instance name") + return + } + + costUsage, err := i.CostManagement.GetActualCost(r.Context()) + if err != nil { + errresponse.Render(w, r, err, http.StatusInternalServerError, "Could not query cost usage") + return + } + + render.JSON(w, r, costUsage) + +} diff --git a/plugins/azure/pkg/instance/costmanagement/costmanagement.go b/plugins/azure/pkg/instance/costmanagement/costmanagement.go new file mode 100644 index 000000000..5169dd3b8 --- /dev/null +++ b/plugins/azure/pkg/instance/costmanagement/costmanagement.go @@ -0,0 +1,76 @@ +package costmanagement + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/services/costmanagement/mgmt/2019-11-01/costmanagement" + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/date" +) + +// Client is the client to interact with the container instance API. +type Client struct { + subscriptionID string + queryClient *costmanagement.QueryClient +} + +// GetActualCost query the actual costs +func (c *Client) GetActualCost(ctx context.Context) (costmanagement.QueryResult, error) { + scope := fmt.Sprintf("subscriptions/%s", c.subscriptionID) + res, err := c.queryClient.Usage(ctx, scope, buildQueryParams()) + if err != nil { + return costmanagement.QueryResult{}, err + } + + return res, nil +} + +func buildQueryParams() costmanagement.QueryDefinition { + agg := make(map[string]*costmanagement.QueryAggregation) + tc := costmanagement.QueryAggregation{ + Name: to.StringPtr("Cost"), + Function: costmanagement.FunctionTypeSum, + } + agg["totalCost"] = &tc + + grouping := []costmanagement.QueryGrouping{ + { + Type: costmanagement.QueryColumnTypeDimension, + Name: to.StringPtr("resourceGroup"), + }, + } + + ds := costmanagement.QueryDataset{ + Granularity: "None", + Configuration: nil, + Aggregation: agg, + Grouping: &grouping, + Filter: nil, + } + now := date.Time{Time: time.Now()} + from := date.Time{Time: now.AddDate(0, 0, -7)} + tp := costmanagement.QueryTimePeriod{ + From: &from, + To: &now, + } + return costmanagement.QueryDefinition{ + Type: costmanagement.ExportTypeActualCost, + Timeframe: costmanagement.TimeframeTypeCustom, + TimePeriod: &tp, + Dataset: &ds, + } +} + +// New returns a new client to interact with the cost management API. +func New(subscriptionID string, authorizer autorest.Authorizer) *Client { + client := costmanagement.NewQueryClient(subscriptionID) + client.Authorizer = authorizer + + return &Client{ + subscriptionID: subscriptionID, + queryClient: &client, + } +} diff --git a/plugins/azure/pkg/instance/instance.go b/plugins/azure/pkg/instance/instance.go index a468acb5b..096f91657 100644 --- a/plugins/azure/pkg/instance/instance.go +++ b/plugins/azure/pkg/instance/instance.go @@ -4,6 +4,7 @@ import ( "os" "github.com/kobsio/kobs/plugins/azure/pkg/instance/containerinstances" + "github.com/kobsio/kobs/plugins/azure/pkg/instance/costmanagement" "github.com/kobsio/kobs/plugins/azure/pkg/instance/resourcegroups" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" @@ -29,6 +30,7 @@ type Instance struct { PermissionsEnabled bool ResourceGroups *resourcegroups.Client ContainerInstances *containerinstances.Client + CostManagement *costmanagement.Client } // New returns a new Elasticsearch instance for the given configuration. @@ -47,11 +49,13 @@ func New(config Config) (*Instance, error) { containerInstances := containerinstances.New(subscriptionID, authorizer) resourceGroups := resourcegroups.New(subscriptionID, credentials) + costManagement := costmanagement.New(subscriptionID, authorizer) return &Instance{ Name: config.Name, PermissionsEnabled: config.PermissionsEnabled, ResourceGroups: resourceGroups, ContainerInstances: containerInstances, + CostManagement: costManagement, }, nil } From 2d49e691d93b97858e018c60250e899fc291a082 Mon Sep 17 00:00:00 2001 From: Axel Koehler Date: Thu, 2 Dec 2021 16:10:39 +0100 Subject: [PATCH 2/7] [azure] Pie chart which shows costs for subscription grouped by resourceGroups for a given timeframe. --- CHANGELOG.md | 3 +- plugins/azure/azure.go | 2 +- plugins/azure/costmanagement.go | 13 ++- plugins/azure/package.json | 2 + .../instance/costmanagement/costmanagement.go | 10 +-- .../src/assets/services/cost-management.svg | 1 + .../containerinstances/ContainerGroups.tsx | 2 +- .../components/costmanagement/ActualCosts.tsx | 82 +++++++++++++++++++ .../costmanagement/CostManagementToolbar.tsx | 34 ++++++++ .../CostManagementToolbarItemTimeframe.tsx | 37 +++++++++ .../costmanagement/CostPieChart.tsx | 46 +++++++++++ .../src/components/costmanagement/Page.tsx | 37 +++++++++ .../components/costmanagement/interfaces.ts | 19 +++++ plugins/azure/src/components/page/Page.tsx | 4 + plugins/azure/src/utils/helpers.ts | 16 ++++ plugins/azure/src/utils/services.ts | 6 ++ 16 files changed, 303 insertions(+), 11 deletions(-) create mode 100644 plugins/azure/src/assets/services/cost-management.svg create mode 100644 plugins/azure/src/components/costmanagement/ActualCosts.tsx create mode 100644 plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx create mode 100644 plugins/azure/src/components/costmanagement/CostManagementToolbarItemTimeframe.tsx create mode 100644 plugins/azure/src/components/costmanagement/CostPieChart.tsx create mode 100644 plugins/azure/src/components/costmanagement/Page.tsx create mode 100644 plugins/azure/src/components/costmanagement/interfaces.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e8a406a..5ed831918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan - [#213](https://github.com/kobsio/kobs/pull/213): [techdocs] Add TechDocs plugin, to access the documentation for your services within kobs. - [#215](https://github.com/kobsio/kobs/pull/215): [azure] Add Azure plugin, to monitor your Azure resources. - [#219](https://github.com/kobsio/kobs/pull/219): [azure] Add permissions for Azure plugin, so that access to resources and actions can be restricted based on resource groups. -- [#220](https://github.com/kobsio/kobs/pull/220): [azure] Add auto formatting for the returned metrics of an container instance and fix the tooltip positioning in the metrics chart. +- [#220](https://github.com/kobsio/kobs/pull/220): [azure] Add auto formatting for the returned metrics of a container instance and fix the tooltip positioning in the metrics chart. +- [#222](https://github.com/kobsio/kobs/pull/222): [azure] Simple cost management on subscription level ### Fixed diff --git a/plugins/azure/azure.go b/plugins/azure/azure.go index 5a0b7537c..385747d12 100644 --- a/plugins/azure/azure.go +++ b/plugins/azure/azure.go @@ -75,7 +75,7 @@ func Register(clusters *clusters.Clusters, plugins *plugin.Plugins, config Confi containerInstancesRouter.Put("/containergroup/restart", router.restartContainerGroup) }) - r.Route("/costmanagment", func(costManagementRouter chi.Router) { + r.Route("/costmanagement", func(costManagementRouter chi.Router) { costManagementRouter.Get("/actualCost", router.getActualCost) }) }) diff --git a/plugins/azure/costmanagement.go b/plugins/azure/costmanagement.go index 046e33a5d..ce65e7ba7 100644 --- a/plugins/azure/costmanagement.go +++ b/plugins/azure/costmanagement.go @@ -2,25 +2,32 @@ package azure import ( "net/http" + "strconv" + + "github.com/kobsio/kobs/pkg/api/middleware/errresponse" "github.com/go-chi/chi/v5" "github.com/go-chi/render" - "github.com/kobsio/kobs/pkg/api/middleware/errresponse" "github.com/sirupsen/logrus" ) func (router *Router) getActualCost(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") - log.WithFields(logrus.Fields{"name": name}).Tracef("getActualCost") + timeframe, err := strconv.Atoi(r.URL.Query().Get("timeframe")) + if err != nil { + errresponse.Render(w, r, nil, http.StatusBadRequest, "Invalid timeframe parameter") + return + } + i := router.getInstance(name) if i == nil { errresponse.Render(w, r, nil, http.StatusBadRequest, "Could not find instance name") return } - costUsage, err := i.CostManagement.GetActualCost(r.Context()) + costUsage, err := i.CostManagement.GetActualCost(r.Context(), timeframe) if err != nil { errresponse.Render(w, r, err, http.StatusInternalServerError, "Could not query cost usage") return diff --git a/plugins/azure/package.json b/plugins/azure/package.json index 264b1ec40..f7c34c418 100644 --- a/plugins/azure/package.json +++ b/plugins/azure/package.json @@ -12,7 +12,9 @@ "dependencies": { "@azure/arm-containerinstance": "^7.1.0", "@kobsio/plugin-core": "*", + "@nivo/core": "^0.74.0", "@nivo/line": "^0.74.0", + "@nivo/pie": "^0.74.0", "@patternfly/react-core": "^4.128.2", "@patternfly/react-log-viewer": "^4.20.4", "@types/react": "^17.0.0", diff --git a/plugins/azure/pkg/instance/costmanagement/costmanagement.go b/plugins/azure/pkg/instance/costmanagement/costmanagement.go index 5169dd3b8..1089312d4 100644 --- a/plugins/azure/pkg/instance/costmanagement/costmanagement.go +++ b/plugins/azure/pkg/instance/costmanagement/costmanagement.go @@ -17,10 +17,10 @@ type Client struct { queryClient *costmanagement.QueryClient } -// GetActualCost query the actual costs -func (c *Client) GetActualCost(ctx context.Context) (costmanagement.QueryResult, error) { +// GetActualCost query the actual costs for the configured subscription and given timeframe grouped by resourceGroup +func (c *Client) GetActualCost(ctx context.Context, timeframe int) (costmanagement.QueryResult, error) { scope := fmt.Sprintf("subscriptions/%s", c.subscriptionID) - res, err := c.queryClient.Usage(ctx, scope, buildQueryParams()) + res, err := c.queryClient.Usage(ctx, scope, buildQueryParams(timeframe)) if err != nil { return costmanagement.QueryResult{}, err } @@ -28,7 +28,7 @@ func (c *Client) GetActualCost(ctx context.Context) (costmanagement.QueryResult, return res, nil } -func buildQueryParams() costmanagement.QueryDefinition { +func buildQueryParams(timeframe int) costmanagement.QueryDefinition { agg := make(map[string]*costmanagement.QueryAggregation) tc := costmanagement.QueryAggregation{ Name: to.StringPtr("Cost"), @@ -51,7 +51,7 @@ func buildQueryParams() costmanagement.QueryDefinition { Filter: nil, } now := date.Time{Time: time.Now()} - from := date.Time{Time: now.AddDate(0, 0, -7)} + from := date.Time{Time: now.AddDate(0, 0, timeframe*-1)} tp := costmanagement.QueryTimePeriod{ From: &from, To: &now, diff --git a/plugins/azure/src/assets/services/cost-management.svg b/plugins/azure/src/assets/services/cost-management.svg new file mode 100644 index 000000000..38d056097 --- /dev/null +++ b/plugins/azure/src/assets/services/cost-management.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/azure/src/components/containerinstances/ContainerGroups.tsx b/plugins/azure/src/components/containerinstances/ContainerGroups.tsx index 288a58037..62116577e 100644 --- a/plugins/azure/src/components/containerinstances/ContainerGroups.tsx +++ b/plugins/azure/src/components/containerinstances/ContainerGroups.tsx @@ -36,7 +36,7 @@ const ContainerGroups: React.FunctionComponent = ({ if (json.error) { throw new Error(json.error); } else { - throw new Error('An unknown error occured'); + throw new Error('An unknown error occurred'); } } } catch (err) { diff --git a/plugins/azure/src/components/costmanagement/ActualCosts.tsx b/plugins/azure/src/components/costmanagement/ActualCosts.tsx new file mode 100644 index 000000000..79b938448 --- /dev/null +++ b/plugins/azure/src/components/costmanagement/ActualCosts.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import {Alert, AlertActionLink, AlertVariant, Spinner} from "@patternfly/react-core"; +import {QueryObserverResult, useQuery} from "react-query"; +import {IQueryResult} from "./interfaces"; +import {CostPieChart} from "./CostPieChart"; + +interface IActualCostsProps { + name: string; + timeframe: number +} + +const ActualCosts: React.FunctionComponent = ({ + name, + timeframe, + }: IActualCostsProps) => { + const {isError, isLoading, error, data, refetch} = useQuery( + ['azure/costmanagement/actualCost', name, timeframe], + async () => { + try { + const timeframeParam = `timeframe=${timeframe}`; + const response = await fetch( + `/api/plugins/azure/${name}/costmanagement/actualCost?${timeframeParam}`, + { + method: 'get', + }, + ); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occurred'); + } + } + } catch (err) { + throw err; + } + }, + ); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( + + > => refetch()}> + Retry + + + } + > +

{error?.message}

+
+ ); + } + + console.log(data) + if (!data) { + return null; + } + + return ( +
+ +
+ ); +}; + +export default ActualCosts; diff --git a/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx b/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx new file mode 100644 index 000000000..44ea26d2d --- /dev/null +++ b/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import {Toolbar, ToolbarContent, ToolbarItem, ToolbarToggleGroup} from "@patternfly/react-core"; +import {FilterIcon} from "@patternfly/react-icons"; +import CostManagementToolbarItemTimeframe from "./CostManagementToolbarItemTimeframe"; + +export interface ICostManagementToolbarProps { + timeframe: number; + setTimeframe: (timeframe: number) => void; +} + + +const CostManagementToolbar: React.FunctionComponent = ({ + timeframe, + setTimeframe, + }: ICostManagementToolbarProps) => { + + return ( + + + } breakpoint="lg"> + Timeframe + + + + + + + ); +}; + +export default CostManagementToolbar; diff --git a/plugins/azure/src/components/costmanagement/CostManagementToolbarItemTimeframe.tsx b/plugins/azure/src/components/costmanagement/CostManagementToolbarItemTimeframe.tsx new file mode 100644 index 000000000..3f1704071 --- /dev/null +++ b/plugins/azure/src/components/costmanagement/CostManagementToolbarItemTimeframe.tsx @@ -0,0 +1,37 @@ +import React, {useState} from 'react'; +import {Select, SelectOption, SelectVariant} from '@patternfly/react-core'; + +export interface ICostManagementToolbarItemTimeframeProps { + timeframe: number; + setTimeframe: (timeframe: number) => void; +} + +// CostManagementToolbarItemTimeframe lets the user select the timeframe +const CostManagementToolbarItemTimeframe: React.FunctionComponent = ({ + timeframe, + setTimeframe, + }: ICostManagementToolbarItemTimeframeProps) => { + const [showSelect, setShowSelect] = useState(false); + const options = [ + {value: '7'}, + {value: '30'} + ]; + + return ( + + ); +}; + +export default CostManagementToolbarItemTimeframe; diff --git a/plugins/azure/src/components/costmanagement/CostPieChart.tsx b/plugins/azure/src/components/costmanagement/CostPieChart.tsx new file mode 100644 index 000000000..c5d29ce47 --- /dev/null +++ b/plugins/azure/src/components/costmanagement/CostPieChart.tsx @@ -0,0 +1,46 @@ +import {ResponsivePieCanvas} from '@nivo/pie' +import {IQueryResult} from "./interfaces"; +import React from "react"; +import {convertQueryResult} from "../../utils/helpers"; + +interface ICostPieChartProps { + data: IQueryResult; +} + +export const CostPieChart: React.FunctionComponent = ({ + data, + }: ICostPieChartProps) => { + return ( + ); +}; diff --git a/plugins/azure/src/components/costmanagement/Page.tsx b/plugins/azure/src/components/costmanagement/Page.tsx new file mode 100644 index 000000000..0c0bb624f --- /dev/null +++ b/plugins/azure/src/components/costmanagement/Page.tsx @@ -0,0 +1,37 @@ +import {PageSection, PageSectionVariants} from '@patternfly/react-core'; +import React, {useState} from 'react'; + +import {Title} from '@kobsio/plugin-core'; +import {services} from '../../utils/services'; +import ActualCosts from "./ActualCosts"; +import CostManagementToolbar from "./CostManagementToolbar"; + +const service = 'costmanagement'; + +interface ICostManagementPageProps { + name: string; + displayName: string; +} + +const CostManagementPage: React.FunctionComponent = ({ + name, + displayName, + }: ICostManagementPageProps) => { + const [timeframe, setTimeframe] = useState(7); + + return ( + + + + <p>{services[service].description}</p> + <CostManagementToolbar timeframe={timeframe} setTimeframe={setTimeframe}/> + </PageSection> + + <PageSection style={{minHeight: '100%'}} variant={PageSectionVariants.default}> + <ActualCosts name={name} timeframe={timeframe}/> + </PageSection> + </React.Fragment> + ); +}; + +export default CostManagementPage; diff --git a/plugins/azure/src/components/costmanagement/interfaces.ts b/plugins/azure/src/components/costmanagement/interfaces.ts new file mode 100644 index 000000000..39ee60a32 --- /dev/null +++ b/plugins/azure/src/components/costmanagement/interfaces.ts @@ -0,0 +1,19 @@ +export interface IQueryResult { + properties: IQueryProperties; +} + +export interface IQueryProperties { + columns: IQueryColumn[]; + rows: any[][]; +} + +export interface IQueryColumn { + name: string; + type: string; +} + +export interface PieDatum { + id: string; + label: string; + value: number; +} diff --git a/plugins/azure/src/components/page/Page.tsx b/plugins/azure/src/components/page/Page.tsx index 54949d534..1f0f1a8f3 100644 --- a/plugins/azure/src/components/page/Page.tsx +++ b/plugins/azure/src/components/page/Page.tsx @@ -6,6 +6,7 @@ import React from 'react'; import ContainerInstancesPage from '../containerinstances/Page'; import { IPluginPageProps } from '@kobsio/plugin-core'; import OverviewPage from './OverviewPage'; +import CostManagementPage from "../costmanagement/Page"; // IResourceGroup is the interface for a resource group returned by the Azure API. This interface is only required for // the returned data, because we are only passing the name of the resource group to the other components. @@ -75,6 +76,9 @@ const Page: React.FunctionComponent<IPluginPageProps> = ({ name, displayName, de <Route exact={true} path={`/${name}/containerinstances`}> <ContainerInstancesPage name={name} displayName={displayName} resourceGroups={data} /> </Route> + <Route exact={true} path={`/${name}/costmanagement`}> + <CostManagementPage name={name} displayName={displayName} /> + </Route> </Switch> ); }; diff --git a/plugins/azure/src/utils/helpers.ts b/plugins/azure/src/utils/helpers.ts index 6bd768de0..b3f41ee67 100644 --- a/plugins/azure/src/utils/helpers.ts +++ b/plugins/azure/src/utils/helpers.ts @@ -2,6 +2,7 @@ import { Serie } from '@nivo/line'; import { IMetric } from './interfaces'; import { formatTime as formatTimeCore } from '@kobsio/plugin-core'; +import {IQueryResult, PieDatum} from "../components/costmanagement/interfaces"; // getResourceGroupFromID returns the resource group name from a given Azure ID. For that we are splitting the ID by "/" // and looking for the resourceGroups parameter. Then the next parameter must be the name of the resource group. If the @@ -55,6 +56,21 @@ export const convertMetric = (metric: IMetric): Serie[] => { return series; }; +// convertQueryResult returns the cost management query result in the format required for nivo pie canvas. +export const convertQueryResult = (data: IQueryResult): PieDatum[] => { + const pieData: PieDatum[] = []; + + for (let i = 0; i < data.properties.rows.length; i++) { + pieData.push({ + id: data.properties.rows[i][1], + label: data.properties.rows[i][1], + value: data.properties.rows[i][0], + }); + } + + return pieData; +}; + // formatMetric is used to auto format the values of a metric. When the unit of a metric is "Bytes" we auto format the // values to KB, MB, GB, etc. export const formatMetric = (metric: IMetric): IMetric => { diff --git a/plugins/azure/src/utils/services.ts b/plugins/azure/src/utils/services.ts index 9b8ed72be..bf9630083 100644 --- a/plugins/azure/src/utils/services.ts +++ b/plugins/azure/src/utils/services.ts @@ -1,4 +1,5 @@ import containerInstancesIcon from '../assets/services/container-instances.svg'; +import costManagementIcon from '../assets/services/cost-management.svg'; export interface IServices { [key: string]: IService; @@ -17,4 +18,9 @@ export const services: IServices = { icon: containerInstancesIcon, name: 'Container Instances', }, + costmanagement: { + description: 'Cost Management helps you understand your Azure invoice', + icon: costManagementIcon, + name: 'Cost Management', + }, }; From 71df9b9b91bc9a13376364971f57ae09e8665323 Mon Sep 17 00:00:00 2001 From: Axel Koehler <axel@staffbase.com> Date: Thu, 2 Dec 2021 16:50:30 +0100 Subject: [PATCH 3/7] Minor format changes --- plugins/azure/azure.go | 2 +- plugins/azure/pkg/instance/costmanagement/costmanagement.go | 2 ++ plugins/azure/pkg/instance/instance.go | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/azure/azure.go b/plugins/azure/azure.go index b19e5c4bd..4020e24da 100644 --- a/plugins/azure/azure.go +++ b/plugins/azure/azure.go @@ -45,7 +45,7 @@ func Register(clusters *clusters.Clusters, plugins *plugin.Plugins, config Confi for _, cfg := range config { inst, err := instance.New(cfg) if err != nil { - log.WithError(err).WithFields(logrus.Fields{"name": cfg.Name}).Fatalf("Could not create Azure inst") + log.WithError(err).WithFields(logrus.Fields{"name": cfg.Name}).Fatalf("Could not create Azure instance") } instances = append(instances, inst) diff --git a/plugins/azure/pkg/instance/costmanagement/costmanagement.go b/plugins/azure/pkg/instance/costmanagement/costmanagement.go index c1a2628fe..a415b96d4 100644 --- a/plugins/azure/pkg/instance/costmanagement/costmanagement.go +++ b/plugins/azure/pkg/instance/costmanagement/costmanagement.go @@ -51,12 +51,14 @@ func buildQueryParams(timeframe int) costmanagement.QueryDefinition { Grouping: &grouping, Filter: nil, } + now := date.Time{Time: time.Now()} from := date.Time{Time: now.AddDate(0, 0, timeframe*-1)} tp := costmanagement.QueryTimePeriod{ From: &from, To: &now, } + return costmanagement.QueryDefinition{ Type: costmanagement.ExportTypeActualCost, Timeframe: costmanagement.TimeframeTypeCustom, diff --git a/plugins/azure/pkg/instance/instance.go b/plugins/azure/pkg/instance/instance.go index 65b592464..33fd9ba6c 100644 --- a/plugins/azure/pkg/instance/instance.go +++ b/plugins/azure/pkg/instance/instance.go @@ -2,7 +2,7 @@ package instance import ( "github.com/kobsio/kobs/plugins/azure/pkg/instance/containerinstances" - "github.com/kobsio/kobs/plugins/azure/pkg/instance/costmanagement" + "github.com/kobsio/kobs/plugins/azure/pkg/instance/costmanagement" "github.com/kobsio/kobs/plugins/azure/pkg/instance/kubernetesservices" "github.com/kobsio/kobs/plugins/azure/pkg/instance/monitor" "github.com/kobsio/kobs/plugins/azure/pkg/instance/resourcegroups" From a313005443a008cbbbe2141810907b635fd45d3d Mon Sep 17 00:00:00 2001 From: Axel Koehler <axel@staffbase.com> Date: Fri, 3 Dec 2021 11:18:13 +0100 Subject: [PATCH 4/7] [azure] Fix linter errors --- .../components/costmanagement/ActualCosts.tsx | 39 +++++++++---------- .../costmanagement/CostManagementToolbar.tsx | 27 ++++++------- .../CostManagementToolbarItemTimeframe.tsx | 17 ++++---- .../costmanagement/CostPieChart.tsx | 34 ++++++++-------- .../src/components/costmanagement/Page.tsx | 26 ++++++------- .../components/costmanagement/interfaces.ts | 3 +- plugins/azure/src/components/page/Page.tsx | 2 +- plugins/azure/src/utils/helpers.ts | 6 +-- plugins/azure/src/utils/services.ts | 4 +- 9 files changed, 75 insertions(+), 83 deletions(-) diff --git a/plugins/azure/src/components/costmanagement/ActualCosts.tsx b/plugins/azure/src/components/costmanagement/ActualCosts.tsx index 79b938448..de55bac0d 100644 --- a/plugins/azure/src/components/costmanagement/ActualCosts.tsx +++ b/plugins/azure/src/components/costmanagement/ActualCosts.tsx @@ -1,29 +1,26 @@ -import React from "react"; -import {Alert, AlertActionLink, AlertVariant, Spinner} from "@patternfly/react-core"; -import {QueryObserverResult, useQuery} from "react-query"; -import {IQueryResult} from "./interfaces"; -import {CostPieChart} from "./CostPieChart"; +import { Alert, AlertActionLink, AlertVariant, Spinner } from '@patternfly/react-core'; +import { QueryObserverResult, useQuery } from 'react-query'; +import React from 'react'; + +import { CostPieChart } from './CostPieChart'; +import { IQueryResult } from './interfaces'; interface IActualCostsProps { name: string; - timeframe: number + timeframe: number; } -const ActualCosts: React.FunctionComponent<IActualCostsProps> = ({ - name, - timeframe, - }: IActualCostsProps) => { - const {isError, isLoading, error, data, refetch} = useQuery<IQueryResult, Error>( +const ActualCosts: React.FunctionComponent<IActualCostsProps> = ({ name, timeframe }: IActualCostsProps) => { + const { isError, isLoading, error, data, refetch } = useQuery<IQueryResult, Error>( ['azure/costmanagement/actualCost', name, timeframe], async () => { try { const timeframeParam = `timeframe=${timeframe}`; - const response = await fetch( - `/api/plugins/azure/${name}/costmanagement/actualCost?${timeframeParam}`, - { - method: 'get', - }, - ); + + const response = await fetch(`/api/plugins/azure/${name}/costmanagement/actualCost?${timeframeParam}`, { + method: 'get', + }); + const json = await response.json(); if (response.status >= 200 && response.status < 300) { @@ -44,7 +41,7 @@ const ActualCosts: React.FunctionComponent<IActualCostsProps> = ({ if (isLoading) { return ( <div className="pf-u-text-align-center"> - <Spinner/> + <Spinner /> </div> ); } @@ -67,14 +64,14 @@ const ActualCosts: React.FunctionComponent<IActualCostsProps> = ({ ); } - console.log(data) + console.log(data); if (!data) { return null; } return ( - <div style={{height: '500px'}}> - <CostPieChart data={data}/> + <div style={{ height: '500px' }}> + <CostPieChart data={data} /> </div> ); }; diff --git a/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx b/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx index 44ea26d2d..7e5ad31d5 100644 --- a/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx +++ b/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx @@ -1,29 +1,24 @@ -import React from "react"; -import {Toolbar, ToolbarContent, ToolbarItem, ToolbarToggleGroup} from "@patternfly/react-core"; -import {FilterIcon} from "@patternfly/react-icons"; -import CostManagementToolbarItemTimeframe from "./CostManagementToolbarItemTimeframe"; +import { Toolbar, ToolbarContent, ToolbarItem, ToolbarToggleGroup } from '@patternfly/react-core'; +import CostManagementToolbarItemTimeframe from './CostManagementToolbarItemTimeframe'; +import { FilterIcon } from '@patternfly/react-icons'; +import React from 'react'; export interface ICostManagementToolbarProps { timeframe: number; setTimeframe: (timeframe: number) => void; } - const CostManagementToolbar: React.FunctionComponent<ICostManagementToolbarProps> = ({ - timeframe, - setTimeframe, - }: ICostManagementToolbarProps) => { - + timeframe, + setTimeframe, +}: ICostManagementToolbarProps) => { return ( - <Toolbar id="cost-management-toolbar" style={{paddingBottom: '0px', zIndex: 300}}> - <ToolbarContent style={{padding: '0px'}}> - <ToolbarToggleGroup toggleIcon={<FilterIcon/>} breakpoint="lg"> + <Toolbar id="cost-management-toolbar" style={{ paddingBottom: '0px', zIndex: 300 }}> + <ToolbarContent style={{ padding: '0px' }}> + <ToolbarToggleGroup toggleIcon={<FilterIcon />} breakpoint="lg"> <ToolbarItem variant="label">Timeframe</ToolbarItem> <ToolbarItem> - <CostManagementToolbarItemTimeframe - timeframe={timeframe} - setTimeframe={setTimeframe} - /> + <CostManagementToolbarItemTimeframe timeframe={timeframe} setTimeframe={setTimeframe} /> </ToolbarItem> </ToolbarToggleGroup> </ToolbarContent> diff --git a/plugins/azure/src/components/costmanagement/CostManagementToolbarItemTimeframe.tsx b/plugins/azure/src/components/costmanagement/CostManagementToolbarItemTimeframe.tsx index 3f1704071..a6af38ee5 100644 --- a/plugins/azure/src/components/costmanagement/CostManagementToolbarItemTimeframe.tsx +++ b/plugins/azure/src/components/costmanagement/CostManagementToolbarItemTimeframe.tsx @@ -1,5 +1,5 @@ -import React, {useState} from 'react'; -import {Select, SelectOption, SelectVariant} from '@patternfly/react-core'; +import React, { useState } from 'react'; +import { Select, SelectOption, SelectVariant } from '@patternfly/react-core'; export interface ICostManagementToolbarItemTimeframeProps { timeframe: number; @@ -8,14 +8,11 @@ export interface ICostManagementToolbarItemTimeframeProps { // CostManagementToolbarItemTimeframe lets the user select the timeframe const CostManagementToolbarItemTimeframe: React.FunctionComponent<ICostManagementToolbarItemTimeframeProps> = ({ - timeframe, - setTimeframe, - }: ICostManagementToolbarItemTimeframeProps) => { + timeframe, + setTimeframe, +}: ICostManagementToolbarItemTimeframeProps) => { const [showSelect, setShowSelect] = useState<boolean>(false); - const options = [ - {value: '7'}, - {value: '30'} - ]; + const options = [{ value: '7' }, { value: '30' }]; return ( <Select @@ -28,7 +25,7 @@ const CostManagementToolbarItemTimeframe: React.FunctionComponent<ICostManagemen isOpen={showSelect} > {options.map((tf, index) => ( - <SelectOption key={index} value={tf.value} description="days"/> + <SelectOption key={index} value={tf.value} description="days" /> ))} </Select> ); diff --git a/plugins/azure/src/components/costmanagement/CostPieChart.tsx b/plugins/azure/src/components/costmanagement/CostPieChart.tsx index c5d29ce47..6b84afee7 100644 --- a/plugins/azure/src/components/costmanagement/CostPieChart.tsx +++ b/plugins/azure/src/components/costmanagement/CostPieChart.tsx @@ -1,18 +1,18 @@ -import {ResponsivePieCanvas} from '@nivo/pie' -import {IQueryResult} from "./interfaces"; -import React from "react"; -import {convertQueryResult} from "../../utils/helpers"; +import React from 'react'; +import { ResponsivePieCanvas } from '@nivo/pie'; + +import { IQueryResult } from './interfaces'; +import { convertQueryResult } from '../../utils/helpers'; interface ICostPieChartProps { data: IQueryResult; } -export const CostPieChart: React.FunctionComponent<ICostPieChartProps> = ({ - data, - }: ICostPieChartProps) => { - return (<ResponsivePieCanvas +export const CostPieChart: React.FunctionComponent<ICostPieChartProps> = ({ data }: ICostPieChartProps) => { + return ( + <ResponsivePieCanvas data={convertQueryResult(data)} - margin={{ top: 40, right: 80, bottom: 80, left: 80 }} + margin={{ bottom: 80, left: 80, right: 80, top: 40 }} valueFormat=" >-.2f" innerRadius={0.5} padAngle={0.7} @@ -20,26 +20,26 @@ export const CostPieChart: React.FunctionComponent<ICostPieChartProps> = ({ activeOuterRadiusOffset={8} colors={{ scheme: 'paired' }} borderWidth={2} - borderColor={{ from: 'color', modifiers: [ [ 'darker', 0.3 ] ] }} + borderColor={{ from: 'color', modifiers: [['darker', 0.3]] }} arcLinkLabelsSkipAngle={5} arcLinkLabelsTextColor="#333333" arcLinkLabelsThickness={2} arcLinkLabelsColor={{ from: 'color' }} arcLabelsSkipAngle={10} - arcLabelsTextColor={{ from: 'color', modifiers: [ [ 'darker', 2 ] ] }} + arcLabelsTextColor={{ from: 'color', modifiers: [['darker', 2]] }} legends={[ { anchor: 'right', direction: 'column', - justify: false, - translateX: 0, - translateY: 0, - itemWidth: 100, + itemDirection: 'left-to-right', itemHeight: 20, + itemWidth: 100, itemsSpacing: 5, + justify: false, symbolSize: 20, - itemDirection: 'left-to-right' - } + translateX: 0, + translateY: 0, + }, ]} /> ); diff --git a/plugins/azure/src/components/costmanagement/Page.tsx b/plugins/azure/src/components/costmanagement/Page.tsx index 0c0bb624f..2a1f13324 100644 --- a/plugins/azure/src/components/costmanagement/Page.tsx +++ b/plugins/azure/src/components/costmanagement/Page.tsx @@ -1,10 +1,10 @@ -import {PageSection, PageSectionVariants} from '@patternfly/react-core'; -import React, {useState} from 'react'; +import { PageSection, PageSectionVariants } from '@patternfly/react-core'; +import React, { useState } from 'react'; -import {Title} from '@kobsio/plugin-core'; -import {services} from '../../utils/services'; -import ActualCosts from "./ActualCosts"; -import CostManagementToolbar from "./CostManagementToolbar"; +import ActualCosts from './ActualCosts'; +import CostManagementToolbar from './CostManagementToolbar'; +import { Title } from '@kobsio/plugin-core'; +import { services } from '../../utils/services'; const service = 'costmanagement'; @@ -14,21 +14,21 @@ interface ICostManagementPageProps { } const CostManagementPage: React.FunctionComponent<ICostManagementPageProps> = ({ - name, - displayName, - }: ICostManagementPageProps) => { + name, + displayName, +}: ICostManagementPageProps) => { const [timeframe, setTimeframe] = useState<number>(7); return ( <React.Fragment> <PageSection variant={PageSectionVariants.light}> - <Title title={services[service].name} subtitle={displayName} size="xl"/> + <Title title={services[service].name} subtitle={displayName} size="xl" /> <p>{services[service].description}</p> - <CostManagementToolbar timeframe={timeframe} setTimeframe={setTimeframe}/> + <CostManagementToolbar timeframe={timeframe} setTimeframe={setTimeframe} /> </PageSection> - <PageSection style={{minHeight: '100%'}} variant={PageSectionVariants.default}> - <ActualCosts name={name} timeframe={timeframe}/> + <PageSection style={{ minHeight: '100%' }} variant={PageSectionVariants.default}> + <ActualCosts name={name} timeframe={timeframe} /> </PageSection> </React.Fragment> ); diff --git a/plugins/azure/src/components/costmanagement/interfaces.ts b/plugins/azure/src/components/costmanagement/interfaces.ts index 39ee60a32..fcdaa7c58 100644 --- a/plugins/azure/src/components/costmanagement/interfaces.ts +++ b/plugins/azure/src/components/costmanagement/interfaces.ts @@ -4,6 +4,7 @@ export interface IQueryResult { export interface IQueryProperties { columns: IQueryColumn[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any rows: any[][]; } @@ -12,7 +13,7 @@ export interface IQueryColumn { type: string; } -export interface PieDatum { +export interface IPieDatum { id: string; label: string; value: number; diff --git a/plugins/azure/src/components/page/Page.tsx b/plugins/azure/src/components/page/Page.tsx index a11af56a4..50ab58d0a 100644 --- a/plugins/azure/src/components/page/Page.tsx +++ b/plugins/azure/src/components/page/Page.tsx @@ -4,10 +4,10 @@ import { Route, Switch } from 'react-router-dom'; import React from 'react'; import ContainerInstancesPage from '../containerinstances/Page'; +import CostManagementPage from '../costmanagement/Page'; import { IPluginPageProps } from '@kobsio/plugin-core'; import KubernetesServicesPage from '../kubernetesservices/Page'; import OverviewPage from './OverviewPage'; -import CostManagementPage from "../costmanagement/Page"; // IResourceGroup is the interface for a resource group returned by the Azure API. This interface is only required for // the returned data, because we are only passing the name of the resource group to the other components. diff --git a/plugins/azure/src/utils/helpers.ts b/plugins/azure/src/utils/helpers.ts index 2bf2a06cb..fd61e182a 100644 --- a/plugins/azure/src/utils/helpers.ts +++ b/plugins/azure/src/utils/helpers.ts @@ -1,8 +1,8 @@ import { Serie } from '@nivo/line'; +import { IPieDatum, IQueryResult } from '../components/costmanagement/interfaces'; import { IMetric } from './interfaces'; import { formatTime as formatTimeCore } from '@kobsio/plugin-core'; -import {IQueryResult, PieDatum} from "../components/costmanagement/interfaces"; // getResourceGroupFromID returns the resource group name from a given Azure ID. For that we are splitting the ID by "/" // and looking for the resourceGroups parameter. Then the next parameter must be the name of the resource group. If the @@ -61,8 +61,8 @@ export const convertMetric = (metric: IMetric): Serie[] => { }; // convertQueryResult returns the cost management query result in the format required for nivo pie canvas. -export const convertQueryResult = (data: IQueryResult): PieDatum[] => { - const pieData: PieDatum[] = []; +export const convertQueryResult = (data: IQueryResult): IPieDatum[] => { + const pieData: IPieDatum[] = []; for (let i = 0; i < data.properties.rows.length; i++) { pieData.push({ diff --git a/plugins/azure/src/utils/services.ts b/plugins/azure/src/utils/services.ts index f74558d3c..ddc250db8 100644 --- a/plugins/azure/src/utils/services.ts +++ b/plugins/azure/src/utils/services.ts @@ -1,5 +1,6 @@ import containerInstancesIcon from '../assets/services/container-instances.svg'; -(??) +import costManagementIcon from '../assets/services/cost-management.svg'; +import kubernetesServicesIcon from '../assets/services/kubernetes-services.svg'; export interface IServices { [key: string]: IService; @@ -24,6 +25,7 @@ export const services: IServices = { description: 'Cost Management helps you understand your Azure invoice', icon: costManagementIcon, name: 'Cost Management', + provider: 'Microsoft.CostManagement/query/', }, kubernetesservices: { description: 'Deploy and scale containers on managed Kubernetes', From 5f7b771d1ccbe95a4cd304aa4c9bb3284ae95fa6 Mon Sep 17 00:00:00 2001 From: ricoberger <mail@ricoberger.de> Date: Tue, 7 Dec 2021 14:46:34 +0100 Subject: [PATCH 5/7] Re-add authorizer --- go.mod | 3 +++ go.sum | 7 +++++++ .../azure/pkg/instance/costmanagement/costmanagement.go | 3 +-- plugins/azure/pkg/instance/instance.go | 9 ++++++++- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8264ebd2d..4f9b0490b 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor v0.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v0.2.0 github.com/Azure/go-autorest/autorest v0.11.19 + github.com/Azure/go-autorest/autorest/azure/auth v0.5.9 github.com/Azure/go-autorest/autorest/date v0.3.0 github.com/ClickHouse/clickhouse-go v1.5.1 github.com/fluxcd/helm-controller/api v0.13.0 @@ -43,6 +44,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/adal v0.9.14 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect @@ -54,6 +56,7 @@ require ( github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/evanphx/json-patch v4.11.0+incompatible // indirect github.com/fluxcd/pkg/apis/kustomize v0.2.0 // indirect diff --git a/go.sum b/go.sum index 4ff160303..dbef49f9f 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,10 @@ github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyC github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/adal v0.9.14 h1:G8hexQdV5D4khOXrWG2YuLCFKhWYmWD8bHYaXN5ophk= github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.9 h1:Y2CgdzitFDsdMwYMzf9LIZWrrTFysqbRc7b94XVVJ78= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.9/go.mod h1:hg3/1yw0Bq87O3KvvnJoAh34/0zbP7SFizX/qN5JvjU= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 h1:dMOmEJfkLKW/7JsokJqkyoYSgmR08hi9KrhjZb+JALY= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= @@ -171,6 +175,9 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= diff --git a/plugins/azure/pkg/instance/costmanagement/costmanagement.go b/plugins/azure/pkg/instance/costmanagement/costmanagement.go index a415b96d4..36d1e0cc5 100644 --- a/plugins/azure/pkg/instance/costmanagement/costmanagement.go +++ b/plugins/azure/pkg/instance/costmanagement/costmanagement.go @@ -6,7 +6,6 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/services/costmanagement/mgmt/2019-11-01/costmanagement" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/date" @@ -68,7 +67,7 @@ func buildQueryParams(timeframe int) costmanagement.QueryDefinition { } // New returns a new client to interact with the cost management API. -func New(subscriptionID string, credentials *azidentity.ClientSecretCredential) *Client { +func New(subscriptionID string, authorizer autorest.Authorizer) *Client { client := costmanagement.NewQueryClient(subscriptionID) client.Authorizer = authorizer diff --git a/plugins/azure/pkg/instance/instance.go b/plugins/azure/pkg/instance/instance.go index 33fd9ba6c..324bf800c 100644 --- a/plugins/azure/pkg/instance/instance.go +++ b/plugins/azure/pkg/instance/instance.go @@ -8,6 +8,7 @@ import ( "github.com/kobsio/kobs/plugins/azure/pkg/instance/resourcegroups" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/sirupsen/logrus" ) @@ -50,11 +51,17 @@ func New(config Config) (*Instance, error) { return nil, err } + authorizerClientCredentialsConfig := auth.NewClientCredentialsConfig(config.Credentials.ClientID, config.Credentials.ClientSecret, config.Credentials.TenantID) + authorizer, err := authorizerClientCredentialsConfig.Authorizer() + if err != nil { + return nil, err + } + resourceGroups := resourcegroups.New(config.Credentials.SubscriptionID, credentials) kubernetesServices := kubernetesservices.New(config.Credentials.SubscriptionID, credentials) containerInstances := containerinstances.New(config.Credentials.SubscriptionID, credentials) monitor := monitor.New(config.Credentials.SubscriptionID, credentials) - costManagement := costmanagement.New(config.Credentials.SubscriptionID, credentials) + costManagement := costmanagement.New(config.Credentials.SubscriptionID, authorizer) return &Instance{ Name: config.Name, From a134be80d7654b6ac42de8dc02c1a60070a51141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20K=C3=B6hler?= <9337156+axdotl@users.noreply.github.com> Date: Wed, 8 Dec 2021 09:44:48 +0100 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Rico Berger <ricoberger@users.noreply.github.com> --- .gitignore | 1 - plugins/azure/azure.go | 2 +- plugins/azure/src/components/costmanagement/ActualCosts.tsx | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 8f3e738cb..46c8ff532 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ node_modules .env.production.local *.iml .idea -config.yaml *.orig npm-debug.log* diff --git a/plugins/azure/azure.go b/plugins/azure/azure.go index 4020e24da..cf6940f60 100644 --- a/plugins/azure/azure.go +++ b/plugins/azure/azure.go @@ -75,7 +75,7 @@ func Register(clusters *clusters.Clusters, plugins *plugin.Plugins, config Confi }) r.Route("/costmanagement", func(costManagementRouter chi.Router) { - costManagementRouter.Get("/actualCost", router.getActualCost) + costManagementRouter.Get("/actualcost", router.getActualCost) }) r.Route("/kubernetesservices", func(kubernetesServicesRouter chi.Router) { diff --git a/plugins/azure/src/components/costmanagement/ActualCosts.tsx b/plugins/azure/src/components/costmanagement/ActualCosts.tsx index de55bac0d..c8652a862 100644 --- a/plugins/azure/src/components/costmanagement/ActualCosts.tsx +++ b/plugins/azure/src/components/costmanagement/ActualCosts.tsx @@ -12,12 +12,12 @@ interface IActualCostsProps { const ActualCosts: React.FunctionComponent<IActualCostsProps> = ({ name, timeframe }: IActualCostsProps) => { const { isError, isLoading, error, data, refetch } = useQuery<IQueryResult, Error>( - ['azure/costmanagement/actualCost', name, timeframe], + ['azure/costmanagement/actualcost', name, timeframe], async () => { try { const timeframeParam = `timeframe=${timeframe}`; - const response = await fetch(`/api/plugins/azure/${name}/costmanagement/actualCost?${timeframeParam}`, { + const response = await fetch(`/api/plugins/azure/${name}/costmanagement/actualcost?${timeframeParam}`, { method: 'get', }); From af0f124c6b09150a779983f00a87b1e4921efcc4 Mon Sep 17 00:00:00 2001 From: Axel Koehler <axel@staffbase.com> Date: Wed, 8 Dec 2021 09:46:22 +0100 Subject: [PATCH 7/7] Organize imports --- .../src/components/costmanagement/CostManagementToolbar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx b/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx index 7e5ad31d5..7751afff1 100644 --- a/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx +++ b/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx @@ -1,8 +1,9 @@ import { Toolbar, ToolbarContent, ToolbarItem, ToolbarToggleGroup } from '@patternfly/react-core'; -import CostManagementToolbarItemTimeframe from './CostManagementToolbarItemTimeframe'; import { FilterIcon } from '@patternfly/react-icons'; import React from 'react'; +import CostManagementToolbarItemTimeframe from './CostManagementToolbarItemTimeframe'; + export interface ICostManagementToolbarProps { timeframe: number; setTimeframe: (timeframe: number) => void;