diff --git a/.gitignore b/.gitignore index a14d7610f..46c8ff532 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ node_modules .env.development.local .env.test.local .env.production.local +*.iml +.idea +*.orig npm-debug.log* yarn-debug.log* diff --git a/CHANGELOG.md b/CHANGELOG.md index c23c95204..5dd2f93ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,9 @@ 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. - [#221](https://github.com/kobsio/kobs/pull/221): [azure] :warning: _Breaking change:_ :warning: Add support for kubernetes services and refactor various places in the Azure plugin. +- [#222](https://github.com/kobsio/kobs/pull/222): [azure] Simple cost management on subscription level - [#224](https://github.com/kobsio/kobs/pull/224): [harbor] Add support for multi-arch images. ### Fixed diff --git a/go.mod b/go.mod index 12fed958e..4f9b0490b 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,16 @@ module github.com/kobsio/kobs go 1.17 require ( + 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/containerinstance/armcontainerinstance v0.0.0-20211201094935-a52477d726c1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v0.2.1 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 github.com/fluxcd/kustomize-controller/api v0.18.0 @@ -37,8 +41,14 @@ require ( ) require ( - github.com/Azure/azure-sdk-for-go v59.3.0+incompatible // indirect 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 + github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/PuerkitoBio/goquery v1.5.1 // indirect github.com/andybalholm/cascadia v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -46,10 +56,12 @@ 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 github.com/fluxcd/pkg/runtime v0.12.2 // indirect + github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/go-logr/logr v0.4.0 // indirect github.com/gogo/googleapis v1.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -87,6 +99,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 d01425e87..dbef49f9f 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,9 @@ 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= @@ -55,18 +56,35 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v0. github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest v0.11.19 h1:7/IqD2fEYVha1EPeaiytVKhzmPV223pfkRIQUGOK2IE= +github.com/Azure/go-autorest/autorest v0.11.19/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= 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= +github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= +github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -157,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= @@ -200,6 +221,7 @@ github.com/fluxcd/pkg/runtime v0.12.0/go.mod h1:EyaTR2TOYcjL5U//C4yH3bt2tvTgIOSX github.com/fluxcd/pkg/runtime v0.12.2 h1:4iOpx2j/w15kNemDOnZrF6ugJ/rhSmRu7aI+xn23+BI= github.com/fluxcd/pkg/runtime v0.12.2/go.mod h1:tuWdqpWPhgjQvYrSnojdZ4plyU8DRU1NDzsfOhnzl2g= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= @@ -607,6 +629,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 7391a9d23..cf6940f60 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") } - 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.Put("/containergroup/restart", router.restartContainerGroup) }) + r.Route("/costmanagement", func(costManagementRouter chi.Router) { + costManagementRouter.Get("/actualcost", router.getActualCost) + }) + r.Route("/kubernetesservices", func(kubernetesServicesRouter chi.Router) { kubernetesServicesRouter.Get("/managedclusters", router.getManagedClusters) kubernetesServicesRouter.Get("/managedcluster/details", router.getManagedCluster) diff --git a/plugins/azure/costmanagement.go b/plugins/azure/costmanagement.go new file mode 100644 index 000000000..ce65e7ba7 --- /dev/null +++ b/plugins/azure/costmanagement.go @@ -0,0 +1,38 @@ +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/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(), timeframe) + 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/package.json b/plugins/azure/package.json index 047e9f76b..1359c437b 100644 --- a/plugins/azure/package.json +++ b/plugins/azure/package.json @@ -13,7 +13,9 @@ "@azure/arm-containerinstance": "^7.1.0", "@azure/arm-containerservice": "^14.3.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 new file mode 100644 index 000000000..36d1e0cc5 --- /dev/null +++ b/plugins/azure/pkg/instance/costmanagement/costmanagement.go @@ -0,0 +1,78 @@ +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 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(timeframe)) + if err != nil { + return costmanagement.QueryResult{}, err + } + + return res, nil +} + +func buildQueryParams(timeframe int) 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, timeframe*-1)} + 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 933a5b85b..324bf800c 100644 --- a/plugins/azure/pkg/instance/instance.go +++ b/plugins/azure/pkg/instance/instance.go @@ -2,11 +2,13 @@ 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/kubernetesservices" "github.com/kobsio/kobs/plugins/azure/pkg/instance/monitor" "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" ) @@ -38,6 +40,7 @@ type Instance struct { ResourceGroups *resourcegroups.Client KubernetesServices *kubernetesservices.Client ContainerInstances *containerinstances.Client + CostManagement *costmanagement.Client Monitor *monitor.Client } @@ -48,10 +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, authorizer) return &Instance{ Name: config.Name, @@ -59,6 +69,7 @@ func New(config Config) (*Instance, error) { ResourceGroups: resourceGroups, KubernetesServices: kubernetesServices, ContainerInstances: containerInstances, + CostManagement: costManagement, Monitor: monitor, }, nil } 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..c8652a862 --- /dev/null +++ b/plugins/azure/src/components/costmanagement/ActualCosts.tsx @@ -0,0 +1,79 @@ +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; +} + +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..7751afff1 --- /dev/null +++ b/plugins/azure/src/components/costmanagement/CostManagementToolbar.tsx @@ -0,0 +1,30 @@ +import { Toolbar, ToolbarContent, ToolbarItem, ToolbarToggleGroup } from '@patternfly/react-core'; +import { FilterIcon } from '@patternfly/react-icons'; +import React from 'react'; + +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..a6af38ee5 --- /dev/null +++ b/plugins/azure/src/components/costmanagement/CostManagementToolbarItemTimeframe.tsx @@ -0,0 +1,34 @@ +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..6b84afee7 --- /dev/null +++ b/plugins/azure/src/components/costmanagement/CostPieChart.tsx @@ -0,0 +1,46 @@ +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 = ({ 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..2a1f13324 --- /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 ActualCosts from './ActualCosts'; +import CostManagementToolbar from './CostManagementToolbar'; +import { Title } from '@kobsio/plugin-core'; +import { services } from '../../utils/services'; + +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..fcdaa7c58 --- /dev/null +++ b/plugins/azure/src/components/costmanagement/interfaces.ts @@ -0,0 +1,20 @@ +export interface IQueryResult { + properties: IQueryProperties; +} + +export interface IQueryProperties { + columns: IQueryColumn[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rows: any[][]; +} + +export interface IQueryColumn { + name: string; + type: string; +} + +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 457e41c42..50ab58d0a 100644 --- a/plugins/azure/src/components/page/Page.tsx +++ b/plugins/azure/src/components/page/Page.tsx @@ -4,6 +4,7 @@ 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'; @@ -76,6 +77,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> <Route exact={true} path={`/${name}/kubernetesservices`}> <KubernetesServicesPage name={name} displayName={displayName} resourceGroups={data} /> </Route> diff --git a/plugins/azure/src/utils/helpers.ts b/plugins/azure/src/utils/helpers.ts index 3d65add1f..fd61e182a 100644 --- a/plugins/azure/src/utils/helpers.ts +++ b/plugins/azure/src/utils/helpers.ts @@ -1,5 +1,6 @@ import { Serie } from '@nivo/line'; +import { IPieDatum, IQueryResult } from '../components/costmanagement/interfaces'; import { IMetric } from './interfaces'; import { formatTime as formatTimeCore } from '@kobsio/plugin-core'; @@ -59,6 +60,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): IPieDatum[] => { + const pieData: IPieDatum[] = []; + + 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 f08dfd4b9..ddc250db8 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'; import kubernetesServicesIcon from '../assets/services/kubernetes-services.svg'; export interface IServices { @@ -20,6 +21,12 @@ export const services: IServices = { name: 'Container Instances', provider: 'Microsoft.ContainerInstance/containerGroups/', }, + costmanagement: { + 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', icon: kubernetesServicesIcon,