├── .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 |
13 | Data Last Updated: { date() } 14 |
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 + '';\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 | 2 | 3 | 4 | 5 | 6 | 7 | 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