├── .ruby-version
├── frontend
├── src
│ ├── Components
│ │ ├── .Rhistory
│ │ ├── Error.jsx
│ │ ├── Loader.jsx
│ │ ├── Product.css
│ │ ├── IndexColumns.js
│ │ ├── Index.js
│ │ ├── Accounts.js
│ │ ├── Tags.js
│ │ ├── Tag.js
│ │ └── Instances.js
│ ├── data_updated.js
│ ├── App.js
│ ├── stores
│ │ ├── Tag.js
│ │ ├── account.js
│ │ ├── instance.js
│ │ └── dataStore.js
│ ├── index.css
│ ├── App.test.js
│ ├── App.css
│ ├── index.js
│ ├── logo.svg
│ ├── AppRouter.js
│ └── serviceWorker.js
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
├── client
│ └── node_modules
│ │ └── .cache
│ │ └── babel-loader
│ │ ├── ffe8f16a549a7754af5dcae1cc525106.json
│ │ ├── 811304b0872e550c047baa2dc3630537.json
│ │ ├── 671a414fb1ce152b30246f2cb4a32b98.json
│ │ ├── 00c76f34ba2005677158f01333f739ee.json
│ │ ├── cf2638a10404afff625668e82a8f8283.json
│ │ ├── c0f5f93f2c447b07d79b4fc89710371c.json
│ │ ├── fc15d57e5e4d0a26a26b2f41497b9907.json
│ │ ├── feb8fd9d8f2dfdd38254c4ec857c9009.json
│ │ ├── 60f461cd7234a6e08c3ba81b9c06341c.json
│ │ ├── 73d8d313001e22e325a6a5ad16922b21.json
│ │ ├── fc6a3809e451ad5eb4fd82e9b579cfef.json
│ │ ├── 439ed6798adf9ebb74763bd6a2d83f2f.json
│ │ └── 8b170153fdd7bc12c5c5a67b3fa7b628.json
├── config
│ ├── jest
│ │ ├── cssTransform.js
│ │ └── fileTransform.js
│ ├── pnpTs.js
│ ├── modules.js
│ ├── paths.js
│ ├── env.js
│ └── webpackDevServer.config.js
├── .gitignore
├── scripts
│ ├── test.js
│ ├── start.js
│ └── build.js
├── README.md
└── package.json
├── lib
├── aws
│ ├── main_test.go
│ ├── tagging
│ │ ├── s3.go
│ │ ├── rds.go
│ │ ├── cloudfront.go
│ │ ├── ec2.go
│ │ └── main.go
│ └── main.go
└── cloudability
│ ├── main_test.go
│ ├── tag_list.go
│ ├── tagger.go
│ ├── main.go
│ ├── tagger_test.go
│ └── db_writer.go
├── .gitignore
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── static
│ ├── media
│ │ ├── OpenSans-Bold.927f45f0.woff2
│ │ ├── OpenSans-Regular.358d3070.woff2
│ │ ├── CalibreWeb-Semibold.46341b1d.woff
│ │ ├── CalibreWeb-Semibold.c1106fe6.woff2
│ │ ├── OpenSans-BoldItalic.09546ed8.woff2
│ │ ├── OpenSans-Semibold.a01def30.woff2
│ │ ├── CalibreWeb-SemiboldItalic.658d3cc4.woff
│ │ ├── OpenSans-RegularItalic.d6ea71f0.woff2
│ │ ├── OpenSans-SemiboldItalic.2ae5183e.woff2
│ │ └── CalibreWeb-SemiboldItalic.7a37491a.woff2
│ ├── css
│ │ ├── main.150fd98d.chunk.css
│ │ └── main.150fd98d.chunk.css.map
│ └── js
│ │ ├── runtime~main.1dc4db23.js
│ │ ├── runtime~main.1dc4db23.js.map
│ │ ├── main.0c7d5706.chunk.js
│ │ └── main.775c0979.chunk.js
├── manifest.json
├── asset-manifest.json
├── service-worker.js
├── precache-manifest.38fd2a53a2b4d17aa0df0009cdb14607.js
├── precache-manifest.807dc3244f46babd91801ddae5a319a3.js
└── index.html
├── Dockerfile
├── .gitmodules
├── README.md
├── cloud_pricing.go
├── cmd
├── config
│ ├── main_test.go
│ └── main.go
├── web
│ ├── routes
│ │ ├── helpers.go
│ │ ├── instances.go
│ │ └── tagging.go
│ └── main.go
└── alter_tag
│ └── main.go
└── config.example.toml
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.4.1
2 |
--------------------------------------------------------------------------------
/frontend/src/Components/.Rhistory:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/aws/main_test.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | frontend/build
3 | config.toml
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/frontend/src/data_updated.js:
--------------------------------------------------------------------------------
1 | export default function date() { return "owjefij" };
2 |
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/logo512.png
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 |
3 | ADD ./web /web
4 | ADD ./public /public/
5 |
6 | ENTRYPOINT ["./web"]
7 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/frontend/public/logo512.png
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "themes/ananke"]
2 | path = themes/ananke
3 | url = https://github.com/budparr/gohugo-theme-ananke.git
4 |
--------------------------------------------------------------------------------
/lib/cloudability/main_test.go:
--------------------------------------------------------------------------------
1 | package cloudability
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestXxx(*testing.T) {
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/public/static/media/OpenSans-Bold.927f45f0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/static/media/OpenSans-Bold.927f45f0.woff2
--------------------------------------------------------------------------------
/public/static/media/OpenSans-Regular.358d3070.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/static/media/OpenSans-Regular.358d3070.woff2
--------------------------------------------------------------------------------
/public/static/media/CalibreWeb-Semibold.46341b1d.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/static/media/CalibreWeb-Semibold.46341b1d.woff
--------------------------------------------------------------------------------
/public/static/media/CalibreWeb-Semibold.c1106fe6.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/static/media/CalibreWeb-Semibold.c1106fe6.woff2
--------------------------------------------------------------------------------
/public/static/media/OpenSans-BoldItalic.09546ed8.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/static/media/OpenSans-BoldItalic.09546ed8.woff2
--------------------------------------------------------------------------------
/public/static/media/OpenSans-Semibold.a01def30.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/static/media/OpenSans-Semibold.a01def30.woff2
--------------------------------------------------------------------------------
/public/static/media/CalibreWeb-SemiboldItalic.658d3cc4.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/static/media/CalibreWeb-SemiboldItalic.658d3cc4.woff
--------------------------------------------------------------------------------
/public/static/media/OpenSans-RegularItalic.d6ea71f0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/static/media/OpenSans-RegularItalic.d6ea71f0.woff2
--------------------------------------------------------------------------------
/public/static/media/OpenSans-SemiboldItalic.2ae5183e.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/static/media/OpenSans-SemiboldItalic.2ae5183e.woff2
--------------------------------------------------------------------------------
/public/static/media/CalibreWeb-SemiboldItalic.7a37491a.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/puppetlabs/cloud-pricing-browser/master/public/static/media/CalibreWeb-SemiboldItalic.7a37491a.woff2
--------------------------------------------------------------------------------
/frontend/src/Components/Error.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Error() {
4 | return (
5 |
6 | Sorry, there was an error. Is the server running?
7 |
8 | );
9 | }
10 |
11 | export default Error;
12 |
--------------------------------------------------------------------------------
/frontend/src/Components/Loader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Loader() {
4 | return (
5 |
6 | Loading...
7 |
8 | );
9 | }
10 |
11 | export default Loader;
12 |
--------------------------------------------------------------------------------
/frontend/client/node_modules/.cache/babel-loader/ffe8f16a549a7754af5dcae1cc525106.json:
--------------------------------------------------------------------------------
1 | {"ast":null,"code":"var isRegExp = require('../internals/is-regexp');\n\nmodule.exports = function (it) {\n if (isRegExp(it)) {\n throw TypeError(\"The method doesn't accept regular expressions\");\n }\n\n return it;\n};","map":null,"metadata":{},"sourceType":"script"}
--------------------------------------------------------------------------------
/frontend/client/node_modules/.cache/babel-loader/811304b0872e550c047baa2dc3630537.json:
--------------------------------------------------------------------------------
1 | {"ast":null,"code":"// `SameValue` abstract operation\n// https://tc39.github.io/ecma262/#sec-samevalue\nmodule.exports = Object.is || function is(x, y) {\n // eslint-disable-next-line no-self-compare\n return x === y ? x !== 0 || 1 / x === 1 / y : x != x && y != y;\n};","map":null,"metadata":{},"sourceType":"script"}
--------------------------------------------------------------------------------
/lib/cloudability/tag_list.go:
--------------------------------------------------------------------------------
1 | package cloudability
2 |
3 | type TagList struct {
4 | Tags []Tag
5 | }
6 |
7 | func (tl *TagList) ReplaceTag(newTag Tag) {
8 | var retTags []Tag
9 | for _, iTag := range tl.Tags {
10 | if iTag.Key == newTag.Key {
11 | retTags = append(retTags, newTag)
12 | } else {
13 | retTags = append(retTags, iTag)
14 | }
15 | }
16 |
17 | tl.Tags = retTags
18 | }
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cloud Pricing
2 |
3 | Managing Cloud costs is hard. This Frontend will integrate with cloudability and the AWS console to allow you to view your cloud costs and instances by team, and tag resources that have yet to be tagged
4 |
5 |
6 | Configuration
7 |
8 | Environment Variables:
9 |
10 | * **CLOUDABILITY_API_KEY** Your cloudability API key for pulling down information about servers.
11 |
--------------------------------------------------------------------------------
/frontend/config/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is a custom Jest transformer turning style imports into empty objects.
4 | // http://facebook.github.io/jest/docs/en/webpack.html
5 |
6 | module.exports = {
7 | process() {
8 | return 'module.exports = {};';
9 | },
10 | getCacheKey() {
11 | // The output is always the same.
12 | return 'cssTransform';
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './App.css';
3 |
4 | import AppRouter from './AppRouter';
5 |
6 | import date from './data_updated.js'
7 |
8 | function App() {
9 | return (
10 |
11 |
12 |
15 |
16 | );
17 | }
18 |
19 | export default App;
20 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/frontend/client/node_modules/.cache/babel-loader/671a414fb1ce152b30246f2cb4a32b98.json:
--------------------------------------------------------------------------------
1 | {"ast":null,"code":"'use strict';\n\nvar charAt = require('../internals/string-multibyte').charAt; // `AdvanceStringIndex` abstract operation\n// https://tc39.github.io/ecma262/#sec-advancestringindex\n\n\nmodule.exports = function (S, index, unicode) {\n return index + (unicode ? charAt(S, index).length : 1);\n};","map":null,"metadata":{},"sourceType":"script"}
--------------------------------------------------------------------------------
/cloud_pricing.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/puppetlabs/cloud-pricing-browser/lib/cloudability"
4 |
5 | func main() {
6 | // teamCosts := cloudability.FetchTeamCosts()
7 | // cloudability.DeleteAll()
8 | instances := cloudability.FetchInstances()
9 | cloudability.PopulateUniqueTags(instances)
10 |
11 | buckets := cloudability.FetchBuckets()
12 | cloudability.PopulateUniqueTags(buckets)
13 |
14 | // aws.Instances()
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/client/node_modules/.cache/babel-loader/00c76f34ba2005677158f01333f739ee.json:
--------------------------------------------------------------------------------
1 | {"ast":null,"code":"var fails = require('../internals/fails'); // check the existence of a method, lowercase\n// of a tag and escaping quotes in arguments\n\n\nmodule.exports = function (METHOD_NAME) {\n return fails(function () {\n var test = ''[METHOD_NAME]('\"');\n return test !== test.toLowerCase() || test.split('\"').length > 3;\n });\n};","map":null,"metadata":{},"sourceType":"script"}
--------------------------------------------------------------------------------
/frontend/src/Components/Product.css:
--------------------------------------------------------------------------------
1 | .pipeline-row {
2 | background-color: #f9f9f9;
3 | }
4 |
5 | .train-row {
6 | background-color: #fff;
7 | padding: 1em 0em;
8 | border-bottom: 1px solid #dfdfdf;
9 | }
10 |
11 | .row.table-row {
12 | margin-right: 0px;
13 | margin-left: 0px;
14 | }
15 |
16 | .padded-row {
17 | padding: 2em 0em;
18 | }
19 |
20 | .rc-table {
21 | margin-left: -15px;
22 | margin-right: -15px;
23 | }
24 |
25 | .rc-table-header {
26 | width: 100%;
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/client/node_modules/.cache/babel-loader/cf2638a10404afff625668e82a8f8283.json:
--------------------------------------------------------------------------------
1 | {"ast":null,"code":"var wellKnownSymbol = require('../internals/well-known-symbol');\n\nvar MATCH = wellKnownSymbol('match');\n\nmodule.exports = function (METHOD_NAME) {\n var regexp = /./;\n\n try {\n '/./'[METHOD_NAME](regexp);\n } catch (e) {\n try {\n regexp[MATCH] = false;\n return '/./'[METHOD_NAME](regexp);\n } catch (f) {\n /* empty */\n }\n }\n\n return false;\n};","map":null,"metadata":{},"sourceType":"script"}
--------------------------------------------------------------------------------
/frontend/src/stores/Tag.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx'
2 |
3 | class Tag {
4 | @observable key
5 | @observable value
6 | @observable count
7 | @observable cost
8 | @observable hourly
9 | @observable monthly
10 |
11 | constructor(key, value, tag, instances) {
12 | this.key = key
13 | this.value = value
14 | this.hourly = tag.hourly
15 | this.count = tag.count
16 | this.monthly = tag.monthly
17 | this.cost = tag.cost
18 | }
19 | }
20 |
21 | export default Tag
--------------------------------------------------------------------------------
/frontend/src/stores/account.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx'
2 |
3 | class Account {
4 | @observable name
5 | @observable number
6 | @observable contactname
7 | @observable contactemail
8 | @observable reaperchannel
9 |
10 | constructor(account) {
11 | this.name = account.name
12 | this.number = account.number
13 | this.contactname = account.contactname
14 | this.contactemail = account.contactemail
15 | this.reaperchannel = account.reaperchannel
16 | }
17 | }
18 |
19 | export default Account
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
12 | }
13 |
14 | .app-main-content {
15 | flex: 1;
16 | overflow: auto;
17 | padding: 40px;
18 | background-color: #f5f8fa;
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/client/node_modules/.cache/babel-loader/c0f5f93f2c447b07d79b4fc89710371c.json:
--------------------------------------------------------------------------------
1 | {"ast":null,"code":"var fails = require('../internals/fails');\n\nvar whitespaces = require('../internals/whitespaces');\n\nvar non = '\\u200B\\u0085\\u180E'; // check that a method works with the correct list\n// of whitespaces and has a correct name\n\nmodule.exports = function (METHOD_NAME) {\n return fails(function () {\n return !!whitespaces[METHOD_NAME]() || non[METHOD_NAME]() != non || whitespaces[METHOD_NAME].name !== METHOD_NAME;\n });\n};","map":null,"metadata":{},"sourceType":"script"}
--------------------------------------------------------------------------------
/frontend/client/node_modules/.cache/babel-loader/fc15d57e5e4d0a26a26b2f41497b9907.json:
--------------------------------------------------------------------------------
1 | {"ast":null,"code":"var isObject = require('../internals/is-object');\n\nvar classof = require('../internals/classof-raw');\n\nvar wellKnownSymbol = require('../internals/well-known-symbol');\n\nvar MATCH = wellKnownSymbol('match'); // `IsRegExp` abstract operation\n// https://tc39.github.io/ecma262/#sec-isregexp\n\nmodule.exports = function (it) {\n var isRegExp;\n return isObject(it) && ((isRegExp = it[MATCH]) !== undefined ? !!isRegExp : classof(it) == 'RegExp');\n};","map":null,"metadata":{},"sourceType":"script"}
--------------------------------------------------------------------------------
/frontend/client/node_modules/.cache/babel-loader/feb8fd9d8f2dfdd38254c4ec857c9009.json:
--------------------------------------------------------------------------------
1 | {"ast":null,"code":"'use strict';\n\nexports.__esModule = true;\n\nvar _react = require('react');\n\nvar _react2 = _interopRequireDefault(_react);\n\nvar _implementation = require('./implementation');\n\nvar _implementation2 = _interopRequireDefault(_implementation);\n\nfunction _interopRequireDefault(obj) {\n return obj && obj.__esModule ? obj : {\n default: obj\n };\n}\n\nexports.default = _react2.default.createContext || _implementation2.default;\nmodule.exports = exports['default'];","map":null,"metadata":{},"sourceType":"script"}
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/client/node_modules/.cache/babel-loader/60f461cd7234a6e08c3ba81b9c06341c.json:
--------------------------------------------------------------------------------
1 | {"ast":null,"code":"var requireObjectCoercible = require('../internals/require-object-coercible');\n\nvar quot = /\"/g; // B.2.3.2.1 CreateHTML(string, tag, attribute, value)\n// https://tc39.github.io/ecma262/#sec-createhtml\n\nmodule.exports = function (string, tag, attribute, value) {\n var S = String(requireObjectCoercible(string));\n var p1 = '<' + tag;\n if (attribute !== '') p1 += ' ' + attribute + '=\"' + String(value).replace(quot, '"') + '\"';\n return p1 + '>' + S + '' + tag + '>';\n};","map":null,"metadata":{},"sourceType":"script"}
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import { Provider } from 'mobx-react'
5 |
6 | import DataStore from './stores/dataStore'
7 |
8 | import Enzyme from "enzyme";
9 | import Adapter from "enzyme-adapter-react-16";
10 | import { shallow } from "enzyme";
11 |
12 | Enzyme.configure({ adapter: new Adapter() });
13 |
14 | class RootStore {
15 | constructor() {
16 | this.dataStore = new DataStore(this)
17 | }
18 | }
19 |
20 | it('renders without crashing', () => {
21 | const div = document.createElement('div');
22 | shallow();
23 | });
24 |
--------------------------------------------------------------------------------
/frontend/client/node_modules/.cache/babel-loader/73d8d313001e22e325a6a5ad16922b21.json:
--------------------------------------------------------------------------------
1 | {"ast":null,"code":"var classof = require('./classof-raw');\n\nvar regexpExec = require('./regexp-exec'); // `RegExpExec` abstract operation\n// https://tc39.github.io/ecma262/#sec-regexpexec\n\n\nmodule.exports = function (R, S) {\n var exec = R.exec;\n\n if (typeof exec === 'function') {\n var result = exec.call(R, S);\n\n if (typeof result !== 'object') {\n throw TypeError('RegExp exec method returned something other than an Object or null');\n }\n\n return result;\n }\n\n if (classof(R) !== 'RegExp') {\n throw TypeError('RegExp#exec called on incompatible receiver');\n }\n\n return regexpExec.call(R, S);\n};","map":null,"metadata":{},"sourceType":"script"}
--------------------------------------------------------------------------------
/frontend/config/pnpTs.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { resolveModuleName } = require('ts-pnp');
4 |
5 | exports.resolveModuleName = (
6 | typescript,
7 | moduleName,
8 | containingFile,
9 | compilerOptions,
10 | resolutionHost
11 | ) => {
12 | return resolveModuleName(
13 | moduleName,
14 | containingFile,
15 | compilerOptions,
16 | resolutionHost,
17 | typescript.resolveModuleName
18 | );
19 | };
20 |
21 | exports.resolveTypeReferenceDirective = (
22 | typescript,
23 | moduleName,
24 | containingFile,
25 | compilerOptions,
26 | resolutionHost
27 | ) => {
28 | return resolveModuleName(
29 | moduleName,
30 | containingFile,
31 | compilerOptions,
32 | resolutionHost,
33 | typescript.resolveTypeReferenceDirective
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/cmd/config/main_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestMarshalAccounts(t *testing.T) {
8 | var c Config
9 | c.Accounts = []Account{
10 | Account{
11 | Name: "name",
12 | ContactName: "contact_name",
13 | ContactEmail: "contact_email",
14 | Number: "number",
15 | ReaperChannel: "reaper_channel",
16 | },
17 | }
18 |
19 | marshaledAccounts := string(c.MarshalAccounts())
20 | expected := "[{\"name\":\"name\",\"contactname\":\"contact_name\",\"contactemail\":\"contact_email\",\"number\":\"number\",\"reaperchannel\":\"reaper_channel\"}]"
21 | if marshaledAccounts != expected {
22 | t.Errorf("Account didn't marshal: %+v, %s, %s",
23 | c.Accounts,
24 | marshaledAccounts,
25 | expected,
26 | )
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 |
2 | .App-logo {
3 | animation: App-logo-spin infinite 20s linear;
4 | height: 40vmin;
5 | pointer-events: none;
6 | }
7 |
8 | .App-header {
9 | background-color: #282c34;
10 | min-height: 5vh;
11 | display: flex;
12 | flex-direction: column;
13 | align-items: center;
14 | justify-content: center;
15 | font-size: calc(10px + 2vmin);
16 | color: white;
17 | }
18 |
19 | .App-link {
20 | color: #61dafb;
21 | }
22 |
23 | @keyframes App-logo-spin {
24 | from {
25 | transform: rotate(0deg);
26 | }
27 | to {
28 | transform: rotate(360deg);
29 | }
30 | }
31 |
32 | footer {
33 | position: fixed;
34 | background-color: white;
35 | color: #777;
36 | text-align: left;
37 | bottom: 0;
38 | height: 3vh;
39 | padding-left: 1em;
40 | }
41 |
--------------------------------------------------------------------------------
/config.example.toml:
--------------------------------------------------------------------------------
1 | [[interestingtags]]
2 | label = "Tag 1"
3 | value = "tag-1"
4 | [[interestingtags]]
5 | label = "Tag 2"
6 | value = "tag-2"
7 |
8 | [[tag-1]]
9 | label = "Tag 1 Label 1"
10 | value = "Tag 1 Value 1"
11 | [[tag-1]]
12 | label = "Tag 1 Label 2"
13 | value = "Tag 1 Value 2"
14 |
15 | [[tag-2]]
16 | label = "Tag 2 Label 1"
17 | value = "Tag 2 Value 1"
18 | [[tag-2]]
19 | label = "Tag 2 Label 2"
20 | value = "Tag 2 Value 2"
21 |
22 |
23 | [[accounts]]
24 | name = "Account 1"
25 | contactname = "Account 1 Contact"
26 | contactemail = "Account 1 Contact Email"
27 | number = "000000000000"
28 | reaperchannel = "#reaper-account-2"
29 |
30 | [[accounts]]
31 | name = "Account"
32 | contactname = "Account 2 Contact"
33 | contactemail = "Account 2 Contact Email"
34 | number = "000000000000"
35 | reaperchannel = "#reaper-account-2"
36 |
37 |
--------------------------------------------------------------------------------
/frontend/src/Components/IndexColumns.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default [
4 | {
5 | label: 'Name',
6 | dataKey: 'name',
7 | cellDataGetter: ({ rowData }) => rowData,
8 | cellRenderer: ({ rowData }) => {rowData.key}
9 | },
10 | {
11 | label: 'Value',
12 | dataKey: 'value',
13 | cellDataGetter: ({ rowData }) => rowData,
14 | cellRenderer: ({ rowData }) => {rowData.value}
15 | },
16 | { label: 'Count', dataKey: 'count' },
17 | {
18 | label: 'Hourly',
19 | dataKey: 'hourly',
20 | cellRenderer: ({ rowData }) => `$${rowData.hourly.toFixed(2)}`
21 | },
22 | {
23 | label: 'Cost',
24 | dataKey: 'cost',
25 | cellRenderer: ({ rowData }) => `$${rowData.cost.toFixed(2)}`
26 | },
27 | ];
28 |
--------------------------------------------------------------------------------
/cmd/web/routes/helpers.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "net/url"
5 | "strconv"
6 | )
7 |
8 | func readString(query url.Values, key string, stringDefault string) string {
9 | if len(query[key]) > 0 {
10 | return query[key][0]
11 | }
12 | return stringDefault
13 | }
14 |
15 | func readStringArray(query url.Values, key string, stringArrayDefault []string) []string {
16 | if len(query[key]) > 0 {
17 | return query[key]
18 | }
19 | return stringArrayDefault
20 | }
21 |
22 | func readInt(query url.Values, key string, intDefault int) int {
23 | if len(query[key]) > 0 {
24 | retVal, _ := strconv.Atoi(query[key][0])
25 | return retVal
26 | }
27 | return intDefault
28 | }
29 |
30 | func readBool(query url.Values, key string, boolDefault bool) bool {
31 | if len(query[key]) > 0 {
32 | b, _ := strconv.ParseBool(query[key][0])
33 | return b
34 | }
35 | return boolDefault
36 | }
37 |
--------------------------------------------------------------------------------
/public/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "main.css": "/static/css/main.150fd98d.chunk.css",
4 | "main.js": "/static/js/main.775c0979.chunk.js",
5 | "main.js.map": "/static/js/main.775c0979.chunk.js.map",
6 | "runtime~main.js": "/static/js/runtime~main.1dc4db23.js",
7 | "runtime~main.js.map": "/static/js/runtime~main.1dc4db23.js.map",
8 | "static/css/2.7e2823f8.chunk.css": "/static/css/2.7e2823f8.chunk.css",
9 | "static/js/2.eebe2a05.chunk.js": "/static/js/2.eebe2a05.chunk.js",
10 | "static/js/2.eebe2a05.chunk.js.map": "/static/js/2.eebe2a05.chunk.js.map",
11 | "index.html": "/index.html",
12 | "precache-manifest.807dc3244f46babd91801ddae5a319a3.js": "/precache-manifest.807dc3244f46babd91801ddae5a319a3.js",
13 | "service-worker.js": "/service-worker.js",
14 | "static/css/2.7e2823f8.chunk.css.map": "/static/css/2.7e2823f8.chunk.css.map",
15 | "static/css/main.150fd98d.chunk.css.map": "/static/css/main.150fd98d.chunk.css.map",
16 | "static/media/ui.scss": "/static/media/OpenSans-SemiboldItalic.2ae5183e.woff2"
17 | }
18 | }
--------------------------------------------------------------------------------
/public/static/css/main.150fd98d.chunk.css:
--------------------------------------------------------------------------------
1 | body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.app-main-content{flex:1 1;overflow:auto;padding:40px;background-color:#f5f8fa}.App-logo{-webkit-animation:App-logo-spin 20s linear infinite;animation:App-logo-spin 20s linear infinite;height:40vmin;pointer-events:none}.App-header{background-color:#282c34;min-height:5vh;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:calc(10px + 2vmin);color:#fff}.App-link{color:#61dafb}@-webkit-keyframes App-logo-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes App-logo-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}footer{position:fixed;background-color:#fff;color:#777;text-align:left;bottom:0;height:3vh;padding-left:1em}
2 | /*# sourceMappingURL=main.150fd98d.chunk.css.map */
--------------------------------------------------------------------------------
/cmd/web/routes/instances.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 |
8 | "github.com/puppetlabs/cloud-pricing-browser/lib/cloudability"
9 | )
10 |
11 | type Instances struct {
12 | }
13 |
14 | func (i *Instances) Get(w http.ResponseWriter, r *http.Request) {
15 | fmt.Printf("%+v", r.URL.Query())
16 |
17 | tagKey := readString(r.URL.Query(), "tag_key", "")
18 | tagVal := readString(r.URL.Query(), "tag_val", "")
19 | vendorAccountId := readString(r.URL.Query(), "vendorAccountId", "")
20 | untagged := readBool(r.URL.Query(), "untagged", false)
21 | size := readInt(r.URL.Query(), "size", 100)
22 | page := readInt(r.URL.Query(), "page", 100)
23 |
24 | var instancesJSON cloudability.ReturnInstances
25 | fmt.Println(untagged)
26 |
27 | if untagged {
28 | instancesJSON = cloudability.UntaggedInstanceReport(vendorAccountId, size, page)
29 | } else {
30 | instancesJSON = cloudability.GetInstances(vendorAccountId, tagKey, tagVal, size, page)
31 | }
32 | fmt.Printf("Returning %d instances\n", len(instancesJSON.Instances))
33 |
34 | instancesOut, _ := json.Marshal(instancesJSON)
35 | fmt.Fprintf(w, string(instancesOut))
36 | }
37 |
--------------------------------------------------------------------------------
/cmd/web/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 |
8 | _ "github.com/BurntSushi/toml"
9 | "github.com/puppetlabs/cloud-pricing-browser/cmd/config"
10 | "github.com/puppetlabs/cloud-pricing-browser/cmd/web/routes"
11 | )
12 |
13 | func main() {
14 | var c config.Config
15 | c.Initialize()
16 |
17 | var i routes.Instances
18 | var t routes.Tagging
19 |
20 | http.HandleFunc("/api/v1/instances", i.Get)
21 | http.HandleFunc("/api/v1/tags", t.Put)
22 |
23 | http.HandleFunc("/api/v1/portfolios", func(w http.ResponseWriter, r *http.Request) {
24 | fmt.Fprintf(w, c.MarshalOptions(c.Portfolios))
25 | })
26 |
27 | http.HandleFunc("/api/v1/accounts", func(w http.ResponseWriter, r *http.Request) {
28 | accounts := c.MarshalAccounts()
29 | fmt.Fprintf(w, string(accounts))
30 | })
31 |
32 | http.HandleFunc("/api/v1/organizations", func(w http.ResponseWriter, r *http.Request) {
33 | fmt.Fprintf(w, c.MarshalOptions(c.Organizations))
34 | })
35 |
36 | http.HandleFunc("/api/v1/interesting_tags", func(w http.ResponseWriter, r *http.Request) {
37 | fmt.Fprintf(w, c.MarshalOptions(c.InterestingTags))
38 | })
39 |
40 | fmt.Println("Listening on localhost:8080")
41 | log.Fatal(http.ListenAndServe(":8080", nil))
42 | }
43 |
--------------------------------------------------------------------------------
/frontend/client/node_modules/.cache/babel-loader/fc6a3809e451ad5eb4fd82e9b579cfef.json:
--------------------------------------------------------------------------------
1 | {"ast":null,"code":"var toInteger = require('../internals/to-integer');\n\nvar requireObjectCoercible = require('../internals/require-object-coercible'); // `String.prototype.{ codePointAt, at }` methods implementation\n\n\nvar createMethod = function (CONVERT_TO_STRING) {\n return function ($this, pos) {\n var S = String(requireObjectCoercible($this));\n var position = toInteger(pos);\n var size = S.length;\n var first, second;\n if (position < 0 || position >= size) return CONVERT_TO_STRING ? '' : undefined;\n first = S.charCodeAt(position);\n return first < 0xD800 || first > 0xDBFF || position + 1 === size || (second = S.charCodeAt(position + 1)) < 0xDC00 || second > 0xDFFF ? CONVERT_TO_STRING ? S.charAt(position) : first : CONVERT_TO_STRING ? S.slice(position, position + 2) : (first - 0xD800 << 10) + (second - 0xDC00) + 0x10000;\n };\n};\n\nmodule.exports = {\n // `String.prototype.codePointAt` method\n // https://tc39.github.io/ecma262/#sec-string.prototype.codepointat\n codeAt: createMethod(false),\n // `String.prototype.at` method\n // https://github.com/mathiasbynens/String.prototype.at\n charAt: createMethod(true)\n};","map":null,"metadata":{},"sourceType":"script"}
--------------------------------------------------------------------------------
/lib/aws/tagging/s3.go:
--------------------------------------------------------------------------------
1 | package tagging
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/aws/aws-sdk-go/aws"
7 | "github.com/aws/aws-sdk-go/aws/awserr"
8 | "github.com/aws/aws-sdk-go/aws/client"
9 | "github.com/aws/aws-sdk-go/aws/credentials"
10 | "github.com/aws/aws-sdk-go/service/s3"
11 | )
12 |
13 | func TagS3(sess client.ConfigProvider, creds *credentials.Credentials, instance_id string, tag_name string, tag_value string) error {
14 | // Create S3 service client
15 | svc := s3.New(sess, &aws.Config{Credentials: creds})
16 |
17 | input := &s3.PutBucketTaggingInput{
18 | Bucket: aws.String(instance_id),
19 | Tagging: &s3.Tagging{
20 | TagSet: []*s3.Tag{
21 | {
22 | Key: aws.String(tag_name),
23 | Value: aws.String(tag_value),
24 | },
25 | },
26 | },
27 | }
28 |
29 | result, err := svc.PutBucketTagging(input)
30 |
31 | if err != nil {
32 | if aerr, ok := err.(awserr.Error); ok {
33 | switch aerr.Code() {
34 | default:
35 | fmt.Println(aerr.Error())
36 | return aerr
37 | }
38 | } else {
39 | // Print the error, cast err to awserr.Error to get the Code and
40 | // Message from an error.
41 | fmt.Println(err.Error())
42 | }
43 | return err
44 | }
45 |
46 | fmt.Printf("%+v", result)
47 | return nil
48 | }
49 |
--------------------------------------------------------------------------------
/public/service-worker.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Welcome to your Workbox-powered service worker!
3 | *
4 | * You'll need to register this file in your web app and you should
5 | * disable HTTP caching for this file too.
6 | * See https://goo.gl/nhQhGp
7 | *
8 | * The rest of the code is auto-generated. Please don't update this file
9 | * directly; instead, make changes to your Workbox build configuration
10 | * and re-run your build process.
11 | * See https://goo.gl/2aRDsh
12 | */
13 |
14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
15 |
16 | importScripts(
17 | "/precache-manifest.807dc3244f46babd91801ddae5a319a3.js"
18 | );
19 |
20 | self.addEventListener('message', (event) => {
21 | if (event.data && event.data.type === 'SKIP_WAITING') {
22 | self.skipWaiting();
23 | }
24 | });
25 |
26 | workbox.core.clientsClaim();
27 |
28 | /**
29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to
30 | * requests for URLs in the manifest.
31 | * See https://goo.gl/S9QRab
32 | */
33 | self.__precacheManifest = [].concat(self.__precacheManifest || []);
34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
35 |
36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), {
37 |
38 | blacklist: [/^\/_/,/\/[^/?]+\.[^/]+$/],
39 | });
40 |
--------------------------------------------------------------------------------
/lib/aws/tagging/rds.go:
--------------------------------------------------------------------------------
1 | package tagging
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/aws/aws-sdk-go/aws"
7 | "github.com/aws/aws-sdk-go/aws/awserr"
8 | "github.com/aws/aws-sdk-go/aws/client"
9 | "github.com/aws/aws-sdk-go/aws/credentials"
10 | "github.com/aws/aws-sdk-go/service/rds"
11 | )
12 |
13 | func TagRDS(sess client.ConfigProvider, creds *credentials.Credentials, account string, region string, instance_id string, tag_name string, tag_value string) error {
14 | // Create rds service client
15 | svc := rds.New(sess, &aws.Config{Credentials: creds})
16 |
17 | arn := fmt.Sprintf("arn:aws:rds:%s:%s:cluster:%s", region, account, instance_id)
18 |
19 | input := &rds.AddTagsToResourceInput{
20 | ResourceName: aws.String(arn),
21 | Tags: []*rds.Tag{
22 | {
23 | Key: aws.String(tag_name),
24 | Value: aws.String(tag_value),
25 | },
26 | },
27 | }
28 |
29 | result, err := svc.AddTagsToResource(input)
30 |
31 | if err != nil {
32 | if aerr, ok := err.(awserr.Error); ok {
33 | switch aerr.Code() {
34 | default:
35 | fmt.Println(aerr.Error())
36 | return aerr
37 | }
38 | } else {
39 | // Print the error, cast err to awserr.Error to get the Code and
40 | // Message from an error.
41 | fmt.Println(err.Error())
42 | return err
43 | }
44 | return nil
45 | }
46 | fmt.Printf("%+v", result)
47 | return nil
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/lib/aws/tagging/cloudfront.go:
--------------------------------------------------------------------------------
1 | package tagging
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/aws/aws-sdk-go/aws"
7 | "github.com/aws/aws-sdk-go/aws/awserr"
8 | "github.com/aws/aws-sdk-go/aws/client"
9 | "github.com/aws/aws-sdk-go/aws/credentials"
10 | "github.com/aws/aws-sdk-go/service/cloudfront"
11 | )
12 |
13 | func TagCloudfront(sess client.ConfigProvider, creds *credentials.Credentials, account string, instance_id string, tag_name string, tag_value string) error {
14 | // Create EC2 service client
15 | svc := cloudfront.New(sess, &aws.Config{Credentials: creds})
16 |
17 | arn := fmt.Sprintf("arn:aws:cloudfront::%s:distribution/%s", account, instance_id)
18 | input := &cloudfront.TagResourceInput{
19 | Resource: aws.String(arn),
20 | Tags: &cloudfront.Tags{
21 | Items: []*cloudfront.Tag{
22 | {
23 | Key: aws.String(tag_name),
24 | Value: aws.String(tag_value),
25 | },
26 | },
27 | },
28 | }
29 |
30 | result, err := svc.TagResource(input)
31 |
32 | if err != nil {
33 | if aerr, ok := err.(awserr.Error); ok {
34 | switch aerr.Code() {
35 | default:
36 | fmt.Println(aerr.Error())
37 | return aerr
38 | }
39 | } else {
40 | // Print the error, cast err to awserr.Error to get the Code and
41 | // Message from an error.
42 | fmt.Println(err.Error())
43 | return err
44 | }
45 | return nil
46 | }
47 |
48 | fmt.Printf("%+v", result)
49 | return nil
50 | }
51 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | import { Provider } from 'mobx-react'
8 |
9 | import DataStore from './stores/dataStore'
10 |
11 | // import 'bootstrap/dist/css/bootstrap.min.css';
12 | import '@puppet/react-components/source/scss/library/ui.scss';
13 |
14 | import { transitions, positions, Provider as AlertProvider } from 'react-alert'
15 | import AlertTemplate from 'react-alert-template-basic'
16 |
17 | // optional cofiguration
18 | const options = {
19 | // you can also just use 'bottom center'
20 | position: positions.BOTTOM_LEFT,
21 | timeout: 5000,
22 | offset: '30px',
23 | // you can also just use 'scale'
24 | transition: transitions.SCALE
25 | }
26 |
27 | class RootStore {
28 | constructor() {
29 | this.dataStore = new DataStore(this)
30 | }
31 | }
32 |
33 |
34 | ReactDOM.render(
35 |
36 |
37 |
38 |
39 | ,
40 | document.getElementById('root')
41 | )
42 | // If you want your app to work offline and load faster, you can change
43 | // unregister() to register() below. Note this comes with some pitfalls.
44 | // Learn more about service workers: https://bit.ly/CRA-PWA
45 | serviceWorker.unregister();
46 |
--------------------------------------------------------------------------------
/frontend/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const camelcase = require('camelcase');
5 |
6 | // This is a custom Jest transformer turning file imports into filenames.
7 | // http://facebook.github.io/jest/docs/en/webpack.html
8 |
9 | module.exports = {
10 | process(src, filename) {
11 | const assetFilename = JSON.stringify(path.basename(filename));
12 |
13 | if (filename.match(/\.svg$/)) {
14 | // Based on how SVGR generates a component name:
15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
16 | const pascalCaseFileName = camelcase(path.parse(filename).name, {
17 | pascalCase: true,
18 | });
19 | const componentName = `Svg${pascalCaseFileName}`;
20 | return `const React = require('react');
21 | module.exports = {
22 | __esModule: true,
23 | default: ${assetFilename},
24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
25 | return {
26 | $$typeof: Symbol.for('react.element'),
27 | type: 'svg',
28 | ref: ref,
29 | key: null,
30 | props: Object.assign({}, props, {
31 | children: ${assetFilename}
32 | })
33 | };
34 | }),
35 | };`;
36 | }
37 |
38 | return `module.exports = ${assetFilename};`;
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/public/static/js/runtime~main.1dc4db23.js:
--------------------------------------------------------------------------------
1 | !function(e){function r(r){for(var n,f,l=r[0],i=r[1],a=r[2],c=0,s=[];c {
12 | throw err;
13 | });
14 |
15 | // Ensure environment variables are read.
16 | require('../config/env');
17 |
18 |
19 | const jest = require('jest');
20 | const execSync = require('child_process').execSync;
21 | let argv = process.argv.slice(2);
22 |
23 | function isInGitRepository() {
24 | try {
25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
26 | return true;
27 | } catch (e) {
28 | return false;
29 | }
30 | }
31 |
32 | function isInMercurialRepository() {
33 | try {
34 | execSync('hg --cwd . root', { stdio: 'ignore' });
35 | return true;
36 | } catch (e) {
37 | return false;
38 | }
39 | }
40 |
41 | // Watch unless on CI or explicitly running all tests
42 | if (
43 | !process.env.CI &&
44 | argv.indexOf('--watchAll') === -1 &&
45 | argv.indexOf('--watchAll=false') === -1
46 | ) {
47 | // https://github.com/facebook/create-react-app/issues/5210
48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository();
49 | argv.push(hasSourceControl ? '--watch' : '--watchAll');
50 | }
51 |
52 |
53 | jest.run(argv);
54 |
--------------------------------------------------------------------------------
/frontend/src/Components/Index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { inject, observer } from 'mobx-react'
3 |
4 | import { Table } from '@puppet/react-components';
5 | import { toJS } from 'mobx'
6 |
7 | import axios from 'axios';
8 |
9 | import IndexColumns from './IndexColumns';
10 | import Error from './Error';
11 | import Loader from './Loader';
12 |
13 | @inject('rootStore')
14 | @observer
15 | class Index extends Component {
16 | static isPrivate = true
17 |
18 | constructor(props) {
19 | super(props);
20 |
21 | this.state = {
22 | loading: true,
23 | loadingThing: 'tags',
24 | interestingTags: [],
25 | };
26 |
27 | this.props.rootStore.dataStore.fetchTags(() => {
28 | this.setState({loading: false});
29 | });
30 | }
31 |
32 | componentDidMount() {
33 | const component = this
34 |
35 | axios.get("/api/v1/interesting_tags").then(function(res) {
36 | component.setState({
37 | interesting_tags: res.data,
38 | });
39 | });
40 | }
41 |
42 | render () {
43 | let tag_data;
44 | if (this.state.interesting_tags) {
45 | tag_data = this.props.rootStore.dataStore.summarizedTags(this.state.interesting_tags.map((tag) => tag.value));
46 | }
47 |
48 | if (this.props.rootStore.dataStore.state === "error") return ;
49 | if (this.state.error) return ;
50 | if (this.state.loading) return ;
51 |
52 | return (
53 |
54 |
55 |
Summary
56 |
57 |
58 | );
59 | }
60 | }
61 |
62 | export default Index
63 |
--------------------------------------------------------------------------------
/lib/aws/tagging/ec2.go:
--------------------------------------------------------------------------------
1 | package tagging
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/aws/aws-sdk-go/aws"
7 | "github.com/aws/aws-sdk-go/aws/awserr"
8 | "github.com/aws/aws-sdk-go/aws/client"
9 | "github.com/aws/aws-sdk-go/aws/credentials"
10 | "github.com/aws/aws-sdk-go/service/ec2"
11 | )
12 |
13 | func TagEC2(sess client.ConfigProvider, creds *credentials.Credentials, instanceID string, tag_name string, tag_value string) error {
14 | // Create EC2 service client
15 | svc := ec2.New(sess, &aws.Config{Credentials: creds})
16 |
17 | var instanceIDs = []string{instanceID}
18 |
19 | var awsStringInstanceIDs []*string
20 | for _, instanceID := range instanceIDs {
21 | awsStringInstanceIDs = append(awsStringInstanceIDs, aws.String(instanceID))
22 | }
23 |
24 | input := &ec2.CreateTagsInput{
25 | Resources: awsStringInstanceIDs,
26 | Tags: []*ec2.Tag{
27 | {
28 | Key: aws.String(tag_name),
29 | Value: aws.String(tag_value),
30 | },
31 | },
32 | }
33 |
34 | result, err := svc.CreateTags(input)
35 |
36 | if err != nil {
37 | if aerr, ok := err.(awserr.Error); ok {
38 | switch aerr.Code() {
39 | case "InvalidInstanceID.NotFound":
40 | fmt.Println("awserr.Error: ")
41 | fmt.Println(aerr.Code())
42 | fmt.Println(aerr.Error())
43 | return err
44 | default:
45 | fmt.Println("awserr.Error: ")
46 | fmt.Println(aerr.Code())
47 | fmt.Println(aerr.Error())
48 | return err
49 | }
50 | } else {
51 | // Print the error, cast err to awserr.Error to get the Code and
52 | // Message from an error.
53 | fmt.Printf("Tagging EC2 instances returned error %s\n", err.Error())
54 | return err
55 | }
56 | return err
57 | }
58 |
59 | fmt.Printf("%+v", result)
60 | return nil
61 | }
62 |
--------------------------------------------------------------------------------
/public/static/css/main.150fd98d.chunk.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["index.css","App.css"],"names":[],"mappings":"AAAA,KACE,QAAS,CACT,mIAEY,CACZ,kCAAmC,CACnC,iCACF,CAEA,KACE,uEACF,CAEA,kBACE,QAAO,CACP,aAAc,CACd,YAAa,CACb,wBACF,CCjBA,UACE,mDAA4C,CAA5C,2CAA4C,CAC5C,aAAc,CACd,mBACF,CAEA,YACE,wBAAyB,CACzB,cAAe,CACf,YAAa,CACb,qBAAsB,CACtB,kBAAmB,CACnB,sBAAuB,CACvB,4BAA6B,CAC7B,UACF,CAEA,UACE,aACF,CAEA,iCACE,GACE,8BAAuB,CAAvB,sBACF,CACA,GACE,+BAAyB,CAAzB,uBACF,CACF,CAPA,yBACE,GACE,8BAAuB,CAAvB,sBACF,CACA,GACE,+BAAyB,CAAzB,uBACF,CACF,CAEA,OACI,cAAe,CACf,qBAAuB,CACvB,UAAW,CACX,eAAgB,CAChB,QAAS,CACT,UAAW,CACX,gBACJ","file":"main.150fd98d.chunk.css","sourcesContent":["body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\",\n \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n\n.app-main-content {\n flex: 1;\n overflow: auto;\n padding: 40px;\n background-color: #f5f8fa;\n}\n","\n.App-logo {\n animation: App-logo-spin infinite 20s linear;\n height: 40vmin;\n pointer-events: none;\n}\n\n.App-header {\n background-color: #282c34;\n min-height: 5vh;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n font-size: calc(10px + 2vmin);\n color: white;\n}\n\n.App-link {\n color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n\nfooter {\n position: fixed;\n background-color: white;\n color: #777;\n text-align: left;\n bottom: 0;\n height: 3vh;\n padding-left: 1em;\n}\n"]}
--------------------------------------------------------------------------------
/lib/cloudability/tagger.go:
--------------------------------------------------------------------------------
1 | package cloudability
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/jinzhu/gorm"
7 | )
8 |
9 | func tagsContain(tags []Tag, key string) bool {
10 | for _, tag := range tags {
11 | if tag.Key == key {
12 | return true
13 | }
14 | }
15 |
16 | return false
17 | }
18 |
19 | func GetTagKeysAndValues() []UniqueTag {
20 | var tags []UniqueTag
21 | // db, err := gorm.Open("sqlite3", "test.db")
22 | db := PostgresConnect()
23 | db.Find(&tags)
24 | return tags
25 | }
26 |
27 | type Tagger struct {
28 | DB *gorm.DB
29 | }
30 |
31 | func (t *Tagger) ConnectToDB() {
32 | t.DB = PostgresConnect()
33 | }
34 |
35 | func (t *Tagger) InstanceByResourceID(resourceID string) Result {
36 | var result Result
37 | t.DB.Where("resource_identifier = ?", resourceID).Preload("Tags").First(&result)
38 | return result
39 | }
40 |
41 | func (t *Tagger) TagObject(result Result, key string, value string) Tag {
42 | tag := Tag{
43 | Key: fmt.Sprintf("tag_user_%s", key),
44 | Value: value,
45 | ResultID: result.ID,
46 | }
47 |
48 | return tag
49 | }
50 |
51 | func tagID(tags []Tag, key string) uint {
52 | for _, tag := range tags {
53 | if tag.Key == key {
54 | return tag.ID
55 | }
56 | }
57 | return 0
58 | }
59 |
60 | func (t *Tagger) TagInstance(resourceID string, key string, value string) {
61 | instance := t.InstanceByResourceID(resourceID)
62 | tag := t.TagObject(instance, key, value)
63 |
64 | var tagList TagList
65 |
66 | // t.DB.Model(&instance).Association("Tags").Find(&tagList.Tags)
67 | tagList.Tags = instance.Tags
68 |
69 | if tagsContain(tagList.Tags, tag.Key) {
70 | tagID := tagID(instance.Tags, tag.Key)
71 | var tagToUpdate Tag
72 | if tagID != 0 {
73 | t.DB.Where("id = ?", tagID).First(&tagToUpdate)
74 | tagToUpdate.Value = tag.Value
75 | t.DB.Save(&tagToUpdate)
76 | }
77 | } else {
78 | t.DB.Model(&instance).Association("Tags").Append(tag)
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/frontend/src/stores/instance.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx'
2 |
3 | class SimpleTag {
4 | @observable key
5 | @observable value
6 |
7 | constructor(key, value) {
8 | this.key = key
9 | this.value = value
10 | }
11 | }
12 |
13 | class Instance {
14 | @observable id
15 | @observable effectiveHourly
16 | @observable name
17 | @observable nodeType
18 | @observable os
19 | @observable provider
20 | @observable region
21 | @observable resourceIdentifier
22 | @observable service
23 | @observable tags = []
24 | @observable totalSpend
25 | @observable vendorAccountId
26 | @observable lastSeen
27 | @observable hoursRunning
28 | @observable portfolio
29 | @observable organization
30 |
31 | getTag(instance, tag_key) {
32 | var retVal;
33 | instance.tags.forEach((tag) => {
34 | if (tag.vendorKey === tag_key) {
35 | retVal = tag.vendorValue;
36 | }
37 | });
38 |
39 | return retVal;
40 | }
41 |
42 | constructor(instance) {
43 | this.id = instance.id;
44 | this.effectiveHourly = instance.effectiveHourly;
45 | this.name = instance.name;
46 | this.nodeType = instance.nodeType;
47 | this.os = instance.os;
48 | this.provider = instance.provider;
49 | this.region = instance.region;
50 | this.resourceIdentifier = instance.resourceIdentifier;
51 | this.service = instance.service;
52 | this.lastSeen = instance.lastSeen;
53 | this.hoursRunning = instance.hoursRunning;
54 | if (instance.tags) {
55 | this.tags = instance.tags.map((tag) => {
56 | return new SimpleTag(tag.vendorKey, tag.vendorValue);
57 | });
58 | }
59 | this.totalSpend = instance.totalSpend;
60 | this.vendorAccountId = instance.vendorAccountId;
61 | this.portfolio = this.getTag(instance, "tag_user_portfolio")
62 | this.organization = this.getTag(instance, "tag_user_organization")
63 | }
64 | }
65 |
66 | export default Instance
--------------------------------------------------------------------------------
/cmd/config/main.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "sort"
7 |
8 | "github.com/spf13/viper"
9 | )
10 |
11 | // Option is a generic type for config items which are lists of optoins
12 | type Option struct {
13 | Label string `toml:"label" json:"label"`
14 | Value string `toml:"value" json:"value"`
15 | }
16 |
17 | // Account matches the format of the account list in the toml config
18 | type Account struct {
19 | Name string `toml:"name" json:"name"`
20 | ContactName string `toml:"contactname" json:"contactname"`
21 | ContactEmail string `toml:"contactemail" json:"contactemail"`
22 | Number string `toml:"number" json:"number"`
23 | ReaperChannel string `toml:"reaperchannel" json:"reaperchannel"`
24 | }
25 |
26 | // Config struct matches the format of the toml config.
27 | type Config struct {
28 | Accounts []Account `toml:"account"`
29 | Portfolios []Option `toml:"portfolios"`
30 | Organizations []Option `toml:"orgnizations"`
31 | InterestingTags []Option `toml:"interestingtags"`
32 | }
33 |
34 | // MarshalAccounts that are being sent over GET api endpoints.
35 | func (c *Config) MarshalAccounts() []byte {
36 | sort.Slice(c.Accounts[:], func(i, j int) bool {
37 | return c.Accounts[i].Number < c.Accounts[j].Number
38 | })
39 |
40 | retVal, _ := json.Marshal(c.Accounts)
41 | return retVal
42 | }
43 |
44 | // MarshalOptions that are being sent over GET api endpoints.
45 | func (c *Config) MarshalOptions(optionSet []Option) string {
46 | ba, _ := json.Marshal(optionSet)
47 | return string(ba)
48 | }
49 |
50 | // Initialize the toml config
51 | func (c *Config) Initialize() {
52 | viper.SetConfigType("toml")
53 | viper.AddConfigPath("./")
54 | viper.SetConfigName("./config")
55 |
56 | err := viper.ReadInConfig() // Find and read the config file
57 |
58 | if err != nil {
59 | log.Fatalf("unable to decode into struct, %v", err)
60 | }
61 |
62 | err = viper.Unmarshal(&c)
63 |
64 | if err != nil {
65 | log.Fatalf("unable to decode into struct, %v", err)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/public/precache-manifest.38fd2a53a2b4d17aa0df0009cdb14607.js:
--------------------------------------------------------------------------------
1 | self.__precacheManifest = (self.__precacheManifest || []).concat([
2 | {
3 | "revision": "558b141e6ab0e309fbd08f7ecff20f0e",
4 | "url": "/index.html"
5 | },
6 | {
7 | "revision": "267f3444a42f98e3c902",
8 | "url": "/static/css/2.7e2823f8.chunk.css"
9 | },
10 | {
11 | "revision": "811cb74deeff1d7d0f12",
12 | "url": "/static/css/main.150fd98d.chunk.css"
13 | },
14 | {
15 | "revision": "267f3444a42f98e3c902",
16 | "url": "/static/js/2.30a61fd4.chunk.js"
17 | },
18 | {
19 | "revision": "811cb74deeff1d7d0f12",
20 | "url": "/static/js/main.0c7d5706.chunk.js"
21 | },
22 | {
23 | "revision": "608567b3a286af875d2a",
24 | "url": "/static/js/runtime~main.1dc4db23.js"
25 | },
26 | {
27 | "revision": "46341b1dfc2e152fe5d5a4c771e307f1",
28 | "url": "/static/media/CalibreWeb-Semibold.46341b1d.woff"
29 | },
30 | {
31 | "revision": "c1106fe6dceeb033f52b2d371faa562e",
32 | "url": "/static/media/CalibreWeb-Semibold.c1106fe6.woff2"
33 | },
34 | {
35 | "revision": "658d3cc40620a82cf95b696a0cfa86b1",
36 | "url": "/static/media/CalibreWeb-SemiboldItalic.658d3cc4.woff"
37 | },
38 | {
39 | "revision": "7a37491a8d15eabad66efc24112950a3",
40 | "url": "/static/media/CalibreWeb-SemiboldItalic.7a37491a.woff2"
41 | },
42 | {
43 | "revision": "927f45f0c98e115c1f661f17d185771e",
44 | "url": "/static/media/OpenSans-Bold.927f45f0.woff2"
45 | },
46 | {
47 | "revision": "09546ed866243a7d205a85d03b41244c",
48 | "url": "/static/media/OpenSans-BoldItalic.09546ed8.woff2"
49 | },
50 | {
51 | "revision": "358d3070946a90b4960cd111154fdc12",
52 | "url": "/static/media/OpenSans-Regular.358d3070.woff2"
53 | },
54 | {
55 | "revision": "d6ea71f09bd1df48a652c88841731d99",
56 | "url": "/static/media/OpenSans-RegularItalic.d6ea71f0.woff2"
57 | },
58 | {
59 | "revision": "a01def30f4398df303f818579d05f4ea",
60 | "url": "/static/media/OpenSans-Semibold.a01def30.woff2"
61 | },
62 | {
63 | "revision": "2ae5183e9b50674d92b52daeb97112b4",
64 | "url": "/static/media/OpenSans-SemiboldItalic.2ae5183e.woff2"
65 | }
66 | ]);
--------------------------------------------------------------------------------
/public/precache-manifest.807dc3244f46babd91801ddae5a319a3.js:
--------------------------------------------------------------------------------
1 | self.__precacheManifest = (self.__precacheManifest || []).concat([
2 | {
3 | "revision": "947a627b5135360ea07ee000cc6f31f6",
4 | "url": "/index.html"
5 | },
6 | {
7 | "revision": "4aecc27eca9f5a93b29f",
8 | "url": "/static/css/2.7e2823f8.chunk.css"
9 | },
10 | {
11 | "revision": "4691033c3ab80a687820",
12 | "url": "/static/css/main.150fd98d.chunk.css"
13 | },
14 | {
15 | "revision": "4aecc27eca9f5a93b29f",
16 | "url": "/static/js/2.eebe2a05.chunk.js"
17 | },
18 | {
19 | "revision": "4691033c3ab80a687820",
20 | "url": "/static/js/main.775c0979.chunk.js"
21 | },
22 | {
23 | "revision": "608567b3a286af875d2a",
24 | "url": "/static/js/runtime~main.1dc4db23.js"
25 | },
26 | {
27 | "revision": "46341b1dfc2e152fe5d5a4c771e307f1",
28 | "url": "/static/media/CalibreWeb-Semibold.46341b1d.woff"
29 | },
30 | {
31 | "revision": "c1106fe6dceeb033f52b2d371faa562e",
32 | "url": "/static/media/CalibreWeb-Semibold.c1106fe6.woff2"
33 | },
34 | {
35 | "revision": "658d3cc40620a82cf95b696a0cfa86b1",
36 | "url": "/static/media/CalibreWeb-SemiboldItalic.658d3cc4.woff"
37 | },
38 | {
39 | "revision": "7a37491a8d15eabad66efc24112950a3",
40 | "url": "/static/media/CalibreWeb-SemiboldItalic.7a37491a.woff2"
41 | },
42 | {
43 | "revision": "927f45f0c98e115c1f661f17d185771e",
44 | "url": "/static/media/OpenSans-Bold.927f45f0.woff2"
45 | },
46 | {
47 | "revision": "09546ed866243a7d205a85d03b41244c",
48 | "url": "/static/media/OpenSans-BoldItalic.09546ed8.woff2"
49 | },
50 | {
51 | "revision": "358d3070946a90b4960cd111154fdc12",
52 | "url": "/static/media/OpenSans-Regular.358d3070.woff2"
53 | },
54 | {
55 | "revision": "d6ea71f09bd1df48a652c88841731d99",
56 | "url": "/static/media/OpenSans-RegularItalic.d6ea71f0.woff2"
57 | },
58 | {
59 | "revision": "a01def30f4398df303f818579d05f4ea",
60 | "url": "/static/media/OpenSans-Semibold.a01def30.woff2"
61 | },
62 | {
63 | "revision": "2ae5183e9b50674d92b52daeb97112b4",
64 | "url": "/static/media/OpenSans-SemiboldItalic.2ae5183e.woff2"
65 | }
66 | ]);
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
19 |
20 |
21 |
25 |
26 |
35 | Cloud Pricing
36 |
37 |
38 |
39 |
40 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/frontend/client/node_modules/.cache/babel-loader/439ed6798adf9ebb74763bd6a2d83f2f.json:
--------------------------------------------------------------------------------
1 | {"ast":null,"code":"'use strict';\n\nvar $ = require('../internals/export');\n\nvar fails = require('../internals/fails');\n\nvar isArray = require('../internals/is-array');\n\nvar isObject = require('../internals/is-object');\n\nvar toObject = require('../internals/to-object');\n\nvar toLength = require('../internals/to-length');\n\nvar createProperty = require('../internals/create-property');\n\nvar arraySpeciesCreate = require('../internals/array-species-create');\n\nvar arrayMethodHasSpeciesSupport = require('../internals/array-method-has-species-support');\n\nvar wellKnownSymbol = require('../internals/well-known-symbol');\n\nvar IS_CONCAT_SPREADABLE = wellKnownSymbol('isConcatSpreadable');\nvar MAX_SAFE_INTEGER = 0x1FFFFFFFFFFFFF;\nvar MAXIMUM_ALLOWED_INDEX_EXCEEDED = 'Maximum allowed index exceeded';\nvar IS_CONCAT_SPREADABLE_SUPPORT = !fails(function () {\n var array = [];\n array[IS_CONCAT_SPREADABLE] = false;\n return array.concat()[0] !== array;\n});\nvar SPECIES_SUPPORT = arrayMethodHasSpeciesSupport('concat');\n\nvar isConcatSpreadable = function (O) {\n if (!isObject(O)) return false;\n var spreadable = O[IS_CONCAT_SPREADABLE];\n return spreadable !== undefined ? !!spreadable : isArray(O);\n};\n\nvar FORCED = !IS_CONCAT_SPREADABLE_SUPPORT || !SPECIES_SUPPORT; // `Array.prototype.concat` method\n// https://tc39.github.io/ecma262/#sec-array.prototype.concat\n// with adding support of @@isConcatSpreadable and @@species\n\n$({\n target: 'Array',\n proto: true,\n forced: FORCED\n}, {\n concat: function concat(arg) {\n // eslint-disable-line no-unused-vars\n var O = toObject(this);\n var A = arraySpeciesCreate(O, 0);\n var n = 0;\n var i, k, length, len, E;\n\n for (i = -1, length = arguments.length; i < length; i++) {\n E = i === -1 ? O : arguments[i];\n\n if (isConcatSpreadable(E)) {\n len = toLength(E.length);\n if (n + len > MAX_SAFE_INTEGER) throw TypeError(MAXIMUM_ALLOWED_INDEX_EXCEEDED);\n\n for (k = 0; k < len; k++, n++) if (k in E) createProperty(A, n, E[k]);\n } else {\n if (n >= MAX_SAFE_INTEGER) throw TypeError(MAXIMUM_ALLOWED_INDEX_EXCEEDED);\n createProperty(A, n++, E);\n }\n }\n\n A.length = n;\n return A;\n }\n});","map":null,"metadata":{},"sourceType":"script"}
--------------------------------------------------------------------------------
/frontend/src/Components/Accounts.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { inject, observer } from 'mobx-react'
3 |
4 | import { Form, Select, Table } from '@puppet/react-components';
5 |
6 | import { toJS } from 'mobx'
7 |
8 | @inject('rootStore')
9 | @observer
10 | class Accounts extends Component {
11 | static isPrivate = true
12 |
13 | constructor(props) {
14 | super(props);
15 | this.state = {
16 | selectedAccount: ""
17 | }
18 |
19 | this.fetchAccounts = this.fetchAccounts.bind(this);
20 | }
21 |
22 | componentDidMount() {
23 | if (this.props.untagged) {
24 | this.fetchAccounts({
25 | untagged: true,
26 | })
27 | } else {
28 | this.fetchAccounts({
29 | untagged: false
30 | })
31 | }
32 | }
33 |
34 | fetchAccounts(options) {
35 | var params = {};
36 | if (this.state.selectedAccount) {
37 | params.vendorAccountId = this.state.selectedAccount
38 | params.untagged = options.untagged
39 | }
40 | this.props.rootStore.dataStore.fetchAccounts(params, () => {
41 |
42 | });
43 | }
44 |
45 | accountData() {
46 | var retVal = [];
47 | this.props.rootStore.dataStore.accounts.forEach((account) => {
48 | retVal.push({
49 | label: account,
50 | value: account,
51 | });
52 | });
53 |
54 | return retVal;
55 | }
56 |
57 | changeTag(vendorKey, vendorValue, accountID) {
58 | this.props.rootStore.dataStore.setTag([accountID], vendorKey, vendorValue)
59 | }
60 |
61 | render () {
62 | let accountData = this.props.rootStore.dataStore.accounts;
63 |
64 | let title = "Accounts"
65 | let columns = [
66 | { label: 'Name', dataKey: 'name' },
67 | { label: 'Contact Name', dataKey: 'contactname' },
68 | { label: 'Contact Email', dataKey: 'contactemail' },
69 | { label: 'Reaper Channel', dataKey: 'reaperchannel' },
70 | { label: 'Account ID', dataKey: 'number' },
71 | ];
72 |
73 | console.log(toJS(accountData));
74 |
75 | return (
76 |
77 |
78 |
{title}
79 |
Count: {toJS(accountData).length}
80 |
81 |
82 |
83 | )
84 | }
85 | }
86 |
87 | export default Accounts
88 |
--------------------------------------------------------------------------------
/lib/aws/main.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/aws/aws-sdk-go/aws"
9 | "github.com/aws/aws-sdk-go/aws/credentials/stscreds"
10 | "github.com/aws/aws-sdk-go/aws/session"
11 | "github.com/aws/aws-sdk-go/service/costexplorer"
12 | )
13 |
14 | func basicAuth(username, password string) string {
15 | auth := username + ":" + password
16 | return base64.StdEncoding.EncodeToString([]byte(auth))
17 | }
18 |
19 | /* Fetches Cost Data from AWS by instance/resource.. */
20 | func Instances() {
21 | sess := session.Must(session.NewSessionWithOptions(session.Options{
22 | SharedConfigState: session.SharedConfigEnable,
23 | }))
24 |
25 | // reader := bufio.NewReader(os.Stdin)
26 | // fmt.Print("MFA Token -> ")
27 | // text, _ := reader.ReadString('\n')
28 | // // convert CRLF to LF
29 | // tokenCode := strings.Replace(text, "\n", "", -1)
30 | //
31 | // fmt.Printf("|%s|\n", tokenCode)
32 |
33 | billingRole := os.Getenv("BILLING_ROLE")
34 | tokenSerialNumber := os.Getenv("TOKEN_SERIAL_NUMBER")
35 |
36 | fmt.Printf("%+v", sess)
37 |
38 | creds := stscreds.NewCredentials(sess, billingRole, func(p *stscreds.AssumeRoleProvider) {
39 | p.SerialNumber = aws.String(tokenSerialNumber)
40 | p.TokenProvider = stscreds.StdinTokenProvider
41 | })
42 |
43 | client := costexplorer.New(sess, &aws.Config{Credentials: creds})
44 |
45 | groupBy := []*costexplorer.GroupDefinition{
46 | {Key: aws.String("NAME"), Type: aws.String("TAG")},
47 | {Key: aws.String("RESOURCE_ID"), Type: aws.String("DIMENSION")},
48 | }
49 |
50 | start := "2019-10-01"
51 | end := "2019-11-01"
52 | dateInterval := costexplorer.DateInterval{
53 | Start: &start,
54 | End: &end,
55 | }
56 |
57 | getCostAndUsageWithResourcesInput := costexplorer.GetCostAndUsageWithResourcesInput{
58 | Granularity: aws.String("MONTHLY"),
59 | Metrics: []*string{aws.String("AMORTIZED_COST")},
60 | GroupBy: groupBy,
61 | TimePeriod: &dateInterval,
62 | Filter: &costexplorer.Expression{
63 | Dimensions: &costexplorer.DimensionValues{
64 | Key: aws.String("SERVICE"),
65 | Values: []*string{
66 | aws.String("Amazon Elastic Compute Cloud - Compute"),
67 | },
68 | },
69 | },
70 | }
71 |
72 | result, err := client.GetCostAndUsageWithResources(&getCostAndUsageWithResourcesInput)
73 |
74 | if err != nil { // resp is now filled
75 | panic(err)
76 | }
77 |
78 | fmt.Println(result)
79 | }
80 |
--------------------------------------------------------------------------------
/lib/aws/tagging/main.go:
--------------------------------------------------------------------------------
1 | package tagging
2 |
3 | import (
4 | "strings"
5 | "time"
6 |
7 | "github.com/aws/aws-sdk-go/aws"
8 | "github.com/aws/aws-sdk-go/aws/credentials/stscreds"
9 | "github.com/aws/aws-sdk-go/aws/session"
10 | "github.com/puppetlabs/cloud-pricing-browser/lib/cloudability"
11 |
12 | "fmt"
13 | )
14 |
15 | func TagResources(instanceIDs []string, tagName string, tagValue string) error {
16 | for _, instanceID := range instanceIDs {
17 | fmt.Printf("%+v", instanceIDs)
18 | if instanceID == "" {
19 | time.Sleep(10 * time.Minute)
20 |
21 | return nil
22 | }
23 |
24 | instance := cloudability.GetInstance(instanceID)
25 |
26 | fmt.Printf("Changing Tags on (from instance id: %s):", instanceID)
27 | fmt.Printf("%+v", instance)
28 | region := instance.Region
29 | account := instance.VendorAccountId
30 | service := instance.Service
31 |
32 | if service == "ec2-recs" {
33 | service = "ec2"
34 | }
35 |
36 | sess, err := session.NewSession(&aws.Config{
37 | Region: aws.String(region),
38 | })
39 |
40 | if err != nil {
41 | return err
42 | }
43 |
44 | taggingRoleName := "TaggingUser"
45 |
46 | taggingRole := fmt.Sprintf("arn:aws:iam::%s:role/%s", account, taggingRoleName)
47 | // tokenSerialNumber := os.Getenv("TOKEN_SERIAL_NUMBER")
48 |
49 | fmt.Printf("Using tagging Role: %s\n", taggingRole)
50 | fmt.Printf("Setting %s to %s on %+v in account %s\n", tagName, tagValue, instanceIDs, account)
51 |
52 | creds := stscreds.NewCredentials(sess, taggingRole)
53 |
54 | if service == "ec2" {
55 | err := TagEC2(sess, creds, instanceID, tagName, tagValue)
56 | if err != nil && strings.Contains(err.Error(), "InvalidInstanceID.NotFound") {
57 | cloudability.DeleteInstance(instanceID)
58 | return fmt.Errorf("Deleted Instance with ID: %s as it did not exist", instanceID)
59 | } else if err != nil {
60 | return fmt.Errorf(err.Error())
61 | }
62 |
63 | var tagger cloudability.Tagger
64 | tagger.ConnectToDB()
65 | tagger.TagInstance(instanceID, tagName, tagValue)
66 |
67 | return nil
68 | }
69 |
70 | if service == "s3" {
71 | return TagS3(sess, creds, instanceID, tagName, tagValue)
72 |
73 | } else if service == "cloudfront" {
74 | return TagCloudfront(sess, creds, account, instanceID, tagName, tagValue)
75 |
76 | } else if service == "rds" {
77 | return TagRDS(sess, creds, account, region, instanceID, tagName, tagValue)
78 |
79 | }
80 |
81 | return fmt.Errorf("Service Not found")
82 | }
83 |
84 | return nil
85 | }
86 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Cloud Pricing
--------------------------------------------------------------------------------
/frontend/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/frontend/src/Components/Tags.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { inject, observer } from 'mobx-react'
3 |
4 | import { Table } from '@puppet/react-components';
5 | import { toJS } from 'mobx'
6 |
7 | @inject('rootStore')
8 | @observer
9 | class Tags extends Component {
10 | static isPrivate = true
11 |
12 | constructor(props) {
13 | super(props);
14 | this.state = {
15 | loading: true,
16 | loadingThing: 'tags',
17 | };
18 | this.props.rootStore.dataStore.fetchTags(() => {
19 | this.setState({loading: false});
20 | });
21 | }
22 |
23 | render () {
24 | const tag_key = this.props.match.params.tag_key
25 | let tag_data;
26 | let only_tag_keys = false;
27 | let title = "Tags";
28 |
29 | if (tag_key && tag_key.length > 0) {
30 | console.log(tag_key);
31 | tag_data = this.props.rootStore.dataStore.tagsThatMatchKeys(tag_key);
32 | } else if (this.props.match.path === "/tag_keys") {
33 | tag_data = this.props.rootStore.dataStore.tag_keys;
34 | only_tag_keys = true;
35 | title = "Tag Keys"
36 | } else {
37 | tag_data = this.props.rootStore.dataStore.tags;
38 | }
39 |
40 | let columns = [
41 | {
42 | label: 'Name',
43 | dataKey: 'name',
44 | cellDataGetter: ({ rowData }) => rowData,
45 | cellRenderer: ({ rowData }) => {rowData.key}
46 | }
47 | ]
48 | if (only_tag_keys === false) {
49 | columns = columns.concat(
50 | [{
51 | label: 'Value',
52 | dataKey: 'value',
53 | cellDataGetter: ({ rowData }) => rowData,
54 | cellRenderer: ({ rowData }) => {rowData.value}
55 | },
56 | // { label: 'Hourly', dataKey: 'hourly' },
57 | // { label: 'Monthly', dataKey: 'monthly' },
58 | {
59 | label: 'Count',
60 | dataKey: 'count',
61 | },
62 | {
63 | label: 'Cost (30-Day)',
64 | dataKey: 'cost',
65 | cellRenderer: ({ rowData }) => ${rowData.count.toFixed(2)}
66 | },
67 | ]);
68 | }
69 |
70 | console.log(columns);
71 | console.log(only_tag_keys);
72 |
73 | var table;
74 | if (this.state.loading) {
75 | table = `Loading ${this.state.loadingThing}...`
76 | } else {
77 | table =
78 | }
79 |
80 |
81 | return (
82 |
83 |
84 |
{title}
85 | {table}
86 |
87 | )
88 | }
89 | }
90 |
91 | export default Tags
92 |
--------------------------------------------------------------------------------
/frontend/config/modules.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const paths = require('./paths');
6 | const chalk = require('react-dev-utils/chalk');
7 | const resolve = require('resolve');
8 |
9 | /**
10 | * Get the baseUrl of a compilerOptions object.
11 | *
12 | * @param {Object} options
13 | */
14 | function getAdditionalModulePaths(options = {}) {
15 | const baseUrl = options.baseUrl;
16 |
17 | // We need to explicitly check for null and undefined (and not a falsy value) because
18 | // TypeScript treats an empty string as `.`.
19 | if (baseUrl == null) {
20 | // If there's no baseUrl set we respect NODE_PATH
21 | // Note that NODE_PATH is deprecated and will be removed
22 | // in the next major release of create-react-app.
23 |
24 | const nodePath = process.env.NODE_PATH || '';
25 | return nodePath.split(path.delimiter).filter(Boolean);
26 | }
27 |
28 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
29 |
30 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is
31 | // the default behavior.
32 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
33 | return null;
34 | }
35 |
36 | // Allow the user set the `baseUrl` to `appSrc`.
37 | if (path.relative(paths.appSrc, baseUrlResolved) === '') {
38 | return [paths.appSrc];
39 | }
40 |
41 | // Otherwise, throw an error.
42 | throw new Error(
43 | chalk.red.bold(
44 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." +
45 | ' Create React App does not support other values at this time.'
46 | )
47 | );
48 | }
49 |
50 | function getModules() {
51 | // Check if TypeScript is setup
52 | const hasTsConfig = fs.existsSync(paths.appTsConfig);
53 | const hasJsConfig = fs.existsSync(paths.appJsConfig);
54 |
55 | if (hasTsConfig && hasJsConfig) {
56 | throw new Error(
57 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
58 | );
59 | }
60 |
61 | let config;
62 |
63 | // If there's a tsconfig.json we assume it's a
64 | // TypeScript project and set up the config
65 | // based on tsconfig.json
66 | if (hasTsConfig) {
67 | const ts = require(resolve.sync('typescript', {
68 | basedir: paths.appNodeModules,
69 | }));
70 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
71 | // Otherwise we'll check if there is jsconfig.json
72 | // for non TS projects.
73 | } else if (hasJsConfig) {
74 | config = require(paths.appJsConfig);
75 | }
76 |
77 | config = config || {};
78 | const options = config.compilerOptions || {};
79 |
80 | const additionalModulePaths = getAdditionalModulePaths(options);
81 |
82 | return {
83 | additionalModulePaths: additionalModulePaths,
84 | hasTsConfig,
85 | };
86 | }
87 |
88 | module.exports = getModules();
89 |
--------------------------------------------------------------------------------
/cmd/web/routes/tagging.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "strings"
9 |
10 | "github.com/puppetlabs/cloud-pricing-browser/lib/aws/tagging"
11 | "github.com/puppetlabs/cloud-pricing-browser/lib/cloudability"
12 | )
13 |
14 | type Tagging struct {
15 | }
16 |
17 | type TaggingReturn struct {
18 | InstanceIDs []string
19 | TagName string
20 | TagValue string
21 | Status string
22 | StatusMessage string
23 | }
24 |
25 | type TagRequest struct {
26 | InstanceIDs []string `json:"instance_ids"`
27 | VendorKey string `json:"vendorKey"`
28 | VendorValue string `json:"vendorValue"`
29 | }
30 |
31 | func (t *Tagging) Put(w http.ResponseWriter, r *http.Request) {
32 | fmt.Printf("t.Put Method %s\n", r.Method)
33 | if r.Method == "GET" {
34 | tagKeysAndValuesJSON := cloudability.GetTagKeysAndValues()
35 | out, _ := json.Marshal(tagKeysAndValuesJSON)
36 | fmt.Fprintf(w, string(out))
37 | } else {
38 | body, err := ioutil.ReadAll(r.Body)
39 |
40 | if err != nil {
41 | fmt.Println(err)
42 | }
43 |
44 | var tagRequest TagRequest
45 | fmt.Printf("%+v", string(body))
46 | json.Unmarshal(body, &tagRequest)
47 |
48 | fmt.Printf("%+v\n", tagRequest)
49 |
50 | if len(tagRequest.InstanceIDs) < 1 {
51 | w.WriteHeader(http.StatusInternalServerError)
52 | w.Write([]byte("Instance ID not set"))
53 | return
54 | }
55 |
56 | if tagRequest.VendorKey == "" {
57 | w.WriteHeader(http.StatusInternalServerError)
58 | w.Write([]byte("Vendor Key not set"))
59 | return
60 | }
61 |
62 | if tagRequest.VendorValue == "" {
63 | w.WriteHeader(http.StatusInternalServerError)
64 | w.Write([]byte("Vendor Value not set"))
65 | return
66 | }
67 |
68 | err = tagging.TagResources(tagRequest.InstanceIDs, tagRequest.VendorKey, tagRequest.VendorValue)
69 |
70 | if err != nil {
71 | w.WriteHeader(http.StatusInternalServerError)
72 |
73 | var status string
74 | if strings.Contains(err.Error(), "Deleted Instance") {
75 | status = "Deleted"
76 | } else {
77 | status = "Failure"
78 | }
79 |
80 | taggingReturn := TaggingReturn{
81 | InstanceIDs: tagRequest.InstanceIDs,
82 | TagName: tagRequest.VendorKey,
83 | TagValue: tagRequest.VendorValue,
84 | StatusMessage: err.Error(),
85 | Status: status,
86 | }
87 |
88 | retVal, _ := json.Marshal(taggingReturn)
89 | w.Write([]byte(retVal))
90 | } else {
91 | taggingReturn := TaggingReturn{
92 | InstanceIDs: tagRequest.InstanceIDs,
93 | TagName: tagRequest.VendorKey,
94 | TagValue: tagRequest.VendorValue,
95 | StatusMessage: fmt.Sprintf("Set tag %s to %s", tagRequest.VendorKey, tagRequest.VendorValue),
96 | Status: "Success",
97 | }
98 | retVal, _ := json.Marshal(taggingReturn)
99 | w.Write([]byte(retVal))
100 | }
101 | }
102 | return
103 | }
104 |
--------------------------------------------------------------------------------
/lib/cloudability/main.go:
--------------------------------------------------------------------------------
1 | package cloudability
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 |
7 | "fmt"
8 | "io/ioutil"
9 | "log"
10 | "net/http"
11 | "os"
12 | "time"
13 | )
14 |
15 | func basicAuth(username, password string) string {
16 | auth := username + ":" + password
17 | return base64.StdEncoding.EncodeToString([]byte(auth))
18 | }
19 |
20 | func Get(endpoint string) []byte {
21 | cloudabilityApiKey := os.Getenv("CLOUDABILITY_API_KEY")
22 | cloudabilityURL := fmt.Sprintf("https://api.cloudability.com/v3%s", endpoint)
23 | fmt.Println(cloudabilityURL)
24 | req, _ := http.NewRequest("GET", cloudabilityURL, nil)
25 | req.Header.Set("Authorization", fmt.Sprintf("Basic %s", basicAuth(cloudabilityApiKey, os.Getenv("CLOUDABILITY_USER_PASSWORD"))))
26 | client := &http.Client{}
27 |
28 | resp, err := client.Do(req)
29 |
30 | if err != nil {
31 | fmt.Println("Fatal Error Line 27")
32 | log.Fatal(err)
33 | }
34 |
35 | defer resp.Body.Close()
36 |
37 | fmt.Println(resp.StatusCode)
38 |
39 | if resp.StatusCode == http.StatusOK {
40 | bodyBytes, err := ioutil.ReadAll(resp.Body)
41 | if err != nil {
42 | fmt.Println("Fatal Error Line 35")
43 | log.Fatal(err)
44 | }
45 | return bodyBytes
46 | }
47 |
48 | return []byte{}
49 | }
50 |
51 | type RecommendationResults struct {
52 | Result []Result `json:"result"`
53 | }
54 |
55 | func FetchInstances() []Result {
56 | ec2Data := Get("/rightsizing/aws/recommendations/ec2?duration=thirty-day")
57 |
58 | var recRes RecommendationResults
59 |
60 | json.Unmarshal(ec2Data, &recRes)
61 |
62 | return WriteResults(recRes.Result)
63 | }
64 |
65 | func FetchBuckets() []Result {
66 | ec2Data := Get("/rightsizing/aws/recommendations/s3?duration=thirty-day")
67 |
68 | var recRes RecommendationResults
69 | json.Unmarshal(ec2Data, &recRes)
70 |
71 | return WriteResults(recRes.Result)
72 | }
73 |
74 | /* Run should fetch report data from cloudability. */
75 | func FetchTeamCosts() []byte {
76 | cloudabilityApiKey := os.Getenv("CLOUDABILITY_API_KEY")
77 |
78 | year, month, _ := time.Now().Date()
79 |
80 | cloudabilityURL := fmt.Sprintf("https://app.cloudability.com/api/1/reporting/cost/run?dimensions=tag3,year_month,tag4&end_date=%d-%2d-28&metrics=unblended_cost,total_amortized_cost&order=desc&sort_by=unblended_cost&start_date=%d-%2d-01&auth_token=%s", year, int(month), year, int(month), cloudabilityApiKey)
81 | fmt.Println(cloudabilityURL)
82 | req, _ := http.NewRequest("GET", cloudabilityURL, nil)
83 |
84 | client := &http.Client{}
85 |
86 | resp, err := client.Do(req)
87 |
88 | if err != nil {
89 | fmt.Println("Fatal Error Line 27")
90 | log.Fatal(err)
91 | }
92 |
93 | defer resp.Body.Close()
94 |
95 | if resp.StatusCode == http.StatusOK {
96 | bodyBytes, err := ioutil.ReadAll(resp.Body)
97 | if err != nil {
98 | fmt.Println("Fatal Error Line 35")
99 | log.Fatal(err)
100 | }
101 | return bodyBytes
102 | }
103 |
104 | return []byte{}
105 | }
106 |
--------------------------------------------------------------------------------
/frontend/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const url = require('url');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebook/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | const envPublicUrl = process.env.PUBLIC_URL;
13 |
14 | function ensureSlash(inputPath, needsSlash) {
15 | const hasSlash = inputPath.endsWith('/');
16 | if (hasSlash && !needsSlash) {
17 | return inputPath.substr(0, inputPath.length - 1);
18 | } else if (!hasSlash && needsSlash) {
19 | return `${inputPath}/`;
20 | } else {
21 | return inputPath;
22 | }
23 | }
24 |
25 | const getPublicUrl = appPackageJson =>
26 | envPublicUrl || require(appPackageJson).homepage;
27 |
28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
29 | // "public path" at which the app is served.
30 | // Webpack needs to know it to put the right