├── ui ├── src │ ├── assets │ │ ├── .gitkeep │ │ └── images │ │ │ └── GitHub-Mark-32px.png │ ├── app │ │ ├── modules │ │ │ ├── login │ │ │ │ ├── components │ │ │ │ │ ├── login.component.scss │ │ │ │ │ ├── login.component.spec.ts │ │ │ │ │ ├── login.component.ts │ │ │ │ │ └── login.component.html │ │ │ │ ├── services │ │ │ │ │ └── login.service.ts │ │ │ │ └── login.module.ts │ │ │ ├── logout │ │ │ │ ├── components │ │ │ │ │ ├── logout.component.scss │ │ │ │ │ ├── logout.component.html │ │ │ │ │ ├── logout.component.spec.ts │ │ │ │ │ └── logout.component.ts │ │ │ │ └── logout.module.ts │ │ │ ├── options │ │ │ │ ├── components │ │ │ │ │ ├── options.component.scss │ │ │ │ │ ├── options.component.html │ │ │ │ │ ├── options.component.spec.ts │ │ │ │ │ └── options.component.ts │ │ │ │ └── options.module.ts │ │ │ ├── changepassword │ │ │ │ ├── components │ │ │ │ │ ├── changepassword.component.scss │ │ │ │ │ ├── changepassword.component.spec.ts │ │ │ │ │ ├── changepassword.component.ts │ │ │ │ │ └── changepassword.component.html │ │ │ │ ├── services │ │ │ │ │ └── changepassword.service.ts │ │ │ │ └── changepassword.module.ts │ │ │ ├── topologyGraph │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── topologyGraph.component.scss │ │ │ │ │ └── topologyGraph.component.html │ │ │ │ ├── modules.ts │ │ │ │ └── services │ │ │ │ │ └── topologyGraph.service.ts │ │ │ ├── logical-group │ │ │ │ ├── components │ │ │ │ │ ├── logical-group.component.css │ │ │ │ │ └── logical-group.component.spec.ts │ │ │ │ ├── logical-group.module.ts │ │ │ │ └── services │ │ │ │ │ └── logical-group.service.ts │ │ │ ├── topo-graph │ │ │ │ ├── components │ │ │ │ │ ├── topo-graph.component.spec.ts │ │ │ │ │ └── topo-graph.component.scss │ │ │ │ ├── modules.ts │ │ │ │ └── services │ │ │ │ │ └── topo-graph.service.ts │ │ │ └── capacity-graph │ │ │ │ ├── capacity-graph.module.ts │ │ │ │ ├── components │ │ │ │ ├── capactiy-graph.component.spec.ts │ │ │ │ └── capactiy-graph.component.scss │ │ │ │ └── services │ │ │ │ └── capacity-graph.service.ts │ │ ├── common │ │ │ └── messages │ │ │ │ ├── common.messages.ts │ │ │ │ └── left-navigation.messages.ts │ │ ├── app.constants.ts │ │ ├── app.component.spec.ts │ │ ├── app.routing.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── app.component.html │ │ └── app.component.scss │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── tsconfig.app.json │ ├── styles.css │ ├── tsconfig.spec.json │ ├── tslint.json │ ├── main.ts │ ├── browserslist │ ├── index.html │ ├── test.ts │ ├── karma.conf.js │ └── json │ │ └── logicalGroup.json ├── proxy.conf.json ├── e2e │ ├── src │ │ ├── app.po.ts │ │ └── app.e2e-spec.ts │ ├── tsconfig.e2e.json │ └── protractor.conf.js ├── nginx.conf ├── tsconfig.json ├── Dockerfile.deploy.purser ├── README.md └── package.json ├── LICENSE ├── docs ├── img │ ├── purser-cli.gif │ ├── architecture.png │ └── architecture01.png ├── plugin-installation.md ├── purser-deployment.md ├── architecture.md ├── plugin-usage.md └── custom-group-installation-and-usage.md ├── cluster ├── helm │ └── chart │ │ └── purser │ │ ├── Chart.yaml │ │ ├── templates │ │ ├── purser-controller-serviceaccount.yaml │ │ ├── purser-controller-svc.yaml │ │ ├── purser-ui-svc.yaml │ │ ├── purser-database-svc.yaml │ │ ├── purser-ui-configmap.yaml │ │ ├── _helpers.tpl │ │ ├── purser-ui-ingress.yaml │ │ ├── NOTES.txt │ │ ├── purser-controller-rbac.yaml │ │ ├── purser-ui-deployment.yaml │ │ └── purser-controller-deployment.yaml │ │ ├── .helmignore │ │ └── README.md ├── artifacts │ ├── example-subscriber.yaml │ ├── example-group.yaml │ ├── purser-group-crd.yaml │ ├── purser-subscriber-crd.yaml │ └── group-template.json ├── minimal │ ├── purser-ui-setup.yaml │ ├── purser-controller-setup.yaml │ └── purser-database-setup.yaml ├── purser-ui-setup.yaml ├── purser-controller-setup.yaml └── purser-database-setup.yaml ├── .gitignore ├── Dockerfile.in ├── .github ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .travis.yml ├── plugin.yaml ├── NOTICE ├── pkg ├── controller │ ├── dgraph │ │ ├── models │ │ │ ├── constants.go │ │ │ ├── query │ │ │ │ ├── subscriber.go │ │ │ │ ├── helpers_test.go │ │ │ │ ├── label.go │ │ │ │ ├── constants_test.go │ │ │ │ ├── label_test.go │ │ │ │ ├── subscriber_test.go │ │ │ │ ├── types.go │ │ │ │ └── login.go │ │ │ ├── pod_test.go │ │ │ ├── label.go │ │ │ └── subscriber.go │ │ ├── login.go │ │ └── purge.go │ ├── utils │ │ ├── purge_test.go │ │ ├── timeUtils.go │ │ ├── jsonutils.go │ │ ├── unitConversions_test.go │ │ ├── unitConversions.go │ │ └── purge.go │ ├── payload.go │ ├── controller_test.go │ ├── types.go │ ├── metrics │ │ └── metrics.go │ └── discovery │ │ ├── executer │ │ └── exec.go │ │ ├── processor │ │ └── svc.go │ │ └── linker │ │ └── processlinks.go ├── apis │ ├── groups │ │ └── v1 │ │ │ ├── docs.go │ │ │ ├── deepcopy.go │ │ │ └── register.go │ └── subscriber │ │ └── v1 │ │ ├── docs.go │ │ ├── deepcopy.go │ │ ├── types.go │ │ └── register.go ├── plugin │ ├── node.go │ ├── utils.go │ └── volume.go ├── utils │ ├── logutil.go │ ├── fileutils.go │ └── k8sutil.go ├── client │ └── clientset.go └── pricing │ ├── cloud.go │ └── aws │ └── aws.go ├── test ├── pricing │ └── pricing_aws_test.go ├── controller │ └── buffering │ │ └── ring_buffer_test.go └── utils │ └── checkUtil.go ├── cmd ├── plugin │ └── types.go └── controller │ ├── api │ ├── logger.go │ ├── router.go │ ├── api.go │ └── apiHandlers │ │ └── helpers.go │ └── config │ └── config.go ├── .make └── Makefile.deploy.purser ├── Gopkg.toml └── CODE_OF_CONDUCT.md /ui/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/app/modules/login/components/login.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/app/modules/logout/components/logout.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/app/modules/options/components/options.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/HEAD/LICENSE -------------------------------------------------------------------------------- /ui/src/app/modules/changepassword/components/changepassword.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/app/modules/logout/components/logout.component.html: -------------------------------------------------------------------------------- 1 |

Logging out

-------------------------------------------------------------------------------- /ui/src/app/modules/topologyGraph/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './topologyGraph.component'; -------------------------------------------------------------------------------- /docs/img/purser-cli.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/HEAD/docs/img/purser-cli.gif -------------------------------------------------------------------------------- /ui/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /docs/img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/HEAD/docs/img/architecture.png -------------------------------------------------------------------------------- /docs/img/architecture01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/HEAD/docs/img/architecture01.png -------------------------------------------------------------------------------- /ui/src/app/common/messages/common.messages.ts: -------------------------------------------------------------------------------- 1 | export const MCommon: any = Object.freeze({ 2 | appHeader: 'PURSER' 3 | }); -------------------------------------------------------------------------------- /ui/src/app/common/messages/left-navigation.messages.ts: -------------------------------------------------------------------------------- 1 | export const MLeftNav: any = Object.freeze({ 2 | homeText: 'Home' 3 | }); -------------------------------------------------------------------------------- /ui/src/assets/images/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/HEAD/ui/src/assets/images/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /ui/src/app/modules/options/components/options.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:3030/", 4 | "changeOrigin": true, 5 | "secure":false 6 | } 7 | } -------------------------------------------------------------------------------- /cluster/helm/chart/purser/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Purser 4 | name: purser 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .go/ 4 | bin/ 5 | vendor/ 6 | *.log 7 | 8 | # compiled output 9 | /dist 10 | /tmp 11 | /out-tsc 12 | tmp/ 13 | 14 | # dependencies 15 | node_modules/ -------------------------------------------------------------------------------- /Dockerfile.in: -------------------------------------------------------------------------------- 1 | FROM ARG_FROM 2 | 3 | LABEL maintainer = "VMware " 4 | LABEL author = "Krishna Karthik " 5 | 6 | ADD ARG_DOCK/bin/ARG_ARCH/ARG_BIN /ARG_BIN -------------------------------------------------------------------------------- /ui/src/app/app.constants.ts: -------------------------------------------------------------------------------- 1 | const BACKEND_BASE_URL = "http://10.112.141.194"; 2 | export const BACKEND_URL = BACKEND_BASE_URL + '/api/' 3 | export const BACKEND_AUTH_URL = BACKEND_BASE_URL + '/auth/' 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /ui/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ui/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - docker 3 | 4 | language: go 5 | 6 | os: 7 | - linux 8 | 9 | go: 10 | - "1.10" 11 | 12 | script: 13 | - make tools 14 | - make deps 15 | - make install 16 | - make travis-build 17 | - make check 18 | 19 | notifications: 20 | email: false 21 | -------------------------------------------------------------------------------- /ui/src/styles.css: -------------------------------------------------------------------------------- 1 | .loading-container { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | right: 0; 6 | left: 0; 7 | } 8 | 9 | .loading-container .spinner { 10 | position: absolute; 11 | top: 0; 12 | bottom: 0; 13 | right: 0; 14 | left: 0; 15 | margin: auto; 16 | } -------------------------------------------------------------------------------- /cluster/artifacts/example-subscriber.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: vmware.purser.com/v1 2 | kind: Subscriber 3 | metadata: 4 | name: example-subscriber 5 | spec: 6 | name: example-subscriber 7 | headers: 8 | authorization: "Bearer " 9 | cluster: "" 10 | url: -------------------------------------------------------------------------------- /plugin.yaml: -------------------------------------------------------------------------------- 1 | name: "purser" 2 | shortDesc: "Cost Insight integration with kubernetes" 3 | longDesc: > 4 | Purser gives cost insights of kubernetes deployments. 5 | command: purser_plugin $@ 6 | flags: 7 | - name: info 8 | desc: Show more details about the plugin. 9 | - name: version 10 | desc: Show plugin version 11 | -------------------------------------------------------------------------------- /ui/src/app/modules/logical-group/components/logical-group.component.css: -------------------------------------------------------------------------------- 1 | .header-wrapper { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: baseline; 5 | margin-bottom: 15px; 6 | } 7 | 8 | .quick-filters { 9 | font-weight: bold; 10 | } 11 | 12 | ::ng-deep .datagrid-overlay-wrapper { 13 | overflow-x: hidden; 14 | } -------------------------------------------------------------------------------- /ui/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /ui/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to purser!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /ui/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | 14 | -------------------------------------------------------------------------------- /ui/src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /cluster/artifacts/example-group.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: vmware.purser.com/v1 2 | kind: Group 3 | metadata: 4 | name: example-group 5 | spec: 6 | name: example-group 7 | labels: 8 | expr1: 9 | app: 10 | - sample-app 11 | - sample-app2 12 | env: 13 | - dev 14 | expr2: 15 | namespace: 16 | - ns1 17 | - ns2 18 | expr3: 19 | key1: 20 | - val1 21 | key2: 22 | - val2 -------------------------------------------------------------------------------- /cluster/helm/chart/purser/templates/purser-controller-serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "purser.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "purser.name" . }} 8 | helm.sh/chart: {{ include "purser.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} -------------------------------------------------------------------------------- /ui/src/app/modules/topologyGraph/components/topologyGraph.component.scss: -------------------------------------------------------------------------------- 1 | .mainDiv{ 2 | height: 500px; 3 | width: 100%; 4 | border: 1px solid #ddd; 5 | } 6 | .serviceSelect{ 7 | width: 30%; 8 | } 9 | .serviceSelectLabel{ 10 | padding-right: 15px; 11 | } 12 | .actionDiv{ 13 | display: flex; 14 | } 15 | .buttonDiv{ 16 | width: 100%; 17 | text-align: right; 18 | } 19 | .spinnerDiv{ 20 | width: 100%; 21 | text-align: center; 22 | } -------------------------------------------------------------------------------- /ui/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream purser { 2 | server purser.purser.svc.cluster.local:3030; 3 | } 4 | 5 | server { 6 | listen 4200; 7 | 8 | location /auth { 9 | proxy_pass http://purser; 10 | } 11 | 12 | location /api { 13 | proxy_pass http://purser; 14 | } 15 | 16 | location / { 17 | root /usr/share/nginx/html/purser; 18 | index index.html index.htm; 19 | try_files $uri $uri/ /index.html =404; 20 | } 21 | } -------------------------------------------------------------------------------- /cluster/helm/chart/purser/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /cluster/artifacts/purser-group-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: groups.vmware.purser.com 5 | spec: 6 | group: vmware.purser.com 7 | names: 8 | kind: Group 9 | listKind: GroupList 10 | plural: groups 11 | singular: group 12 | scope: Namespaced 13 | version: v1 14 | status: 15 | acceptedNames: 16 | kind: Group 17 | listKind: GroupList 18 | plural: groups 19 | singular: group -------------------------------------------------------------------------------- /ui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Purser 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | Loading... 17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Purser 2 | 3 | Copyright (c) 2018 VMware, Inc. All Rights Reserved. 4 | 5 | This product is licensed to you under the Apache 2.0 license (the "License"). 6 | You may not use this product except in compliance with the Apache 2.0 License. 7 | 8 | This product may include a number of subcomponents with separate copyright notices and license terms. 9 | Your use of these subcomponents is subject to the terms and conditions of the subcomponent's license, 10 | as noted in the LICENSE file. 11 | 12 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2017", 18 | "dom" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cluster/artifacts/purser-subscriber-crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: subscribers.vmware.purser.com 5 | spec: 6 | group: vmware.purser.com 7 | names: 8 | kind: Subscriber 9 | listKind: SubscriberList 10 | plural: subscribers 11 | singular: subscriber 12 | scope: Namespaced 13 | version: v1 14 | status: 15 | acceptedNames: 16 | kind: Subscriber 17 | listKind: SubscriberList 18 | plural: subscribers 19 | singular: subscriber -------------------------------------------------------------------------------- /ui/src/app/modules/changepassword/services/changepassword.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { BACKEND_AUTH_URL } from '../../../app.constants'; 4 | 5 | @Injectable() 6 | export class ChangepasswordService { 7 | url: string; 8 | constructor(private http: HttpClient) { 9 | this.url = BACKEND_AUTH_URL + 'changePassword'; 10 | } 11 | 12 | public sendLoginCredentials(credentials) { 13 | return this.http.post(this.url, credentials); 14 | } 15 | } -------------------------------------------------------------------------------- /pkg/controller/dgraph/models/constants.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Cost and other cloud constants 4 | const ( 5 | // Cost constants 6 | DefaultCPUCostPerCPUPerHour = "0.024" 7 | DefaultMemCostPerGBPerHour = "0.01" 8 | DefaultStorageCostPerGBPerHour = "0.00013888888" 9 | DefaultCPUCostInFloat64 = 0.024 10 | DefaultMemCostInFloat64 = 0.01 11 | DefaultStorageCostInFloat64 = 0.00013888888 12 | 13 | // Cloud provider constants 14 | AWS = "aws" 15 | 16 | // Time constants 17 | HoursInMonth = 720 18 | 19 | // Other constants 20 | PriceError = -1.0 21 | ) 22 | -------------------------------------------------------------------------------- /ui/src/app/modules/logout/logout.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { ClarityModule } from '@clr/angular'; 5 | import { LogoutComponent } from './components/logout.component'; 6 | 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, ClarityModule, FormsModule 11 | ], 12 | exports: [LogoutComponent], 13 | declarations: [LogoutComponent], 14 | providers: [], 15 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 16 | }) 17 | export class LogoutModule { } -------------------------------------------------------------------------------- /ui/src/app/modules/options/options.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { ClarityModule } from '@clr/angular'; 5 | import { OptionsComponent } from './components/options.component'; 6 | 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, ClarityModule, FormsModule 11 | ], 12 | exports: [OptionsComponent], 13 | declarations: [OptionsComponent], 14 | providers: [], 15 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 16 | }) 17 | export class OptionsModule { } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Is this a BUG REPORT or FEATURE REQUEST?**: 2 | 3 | > Uncomment only one, leave it on its own line: 4 | > 5 | > /kind bug 6 | > /kind feature 7 | 8 | 9 | **What happened**: 10 | 11 | **What you expected to happen**: 12 | 13 | **How to reproduce it (as minimally and precisely as possible)**: 14 | 15 | 16 | **Anything else we need to know?**: 17 | 18 | **Environment**: 19 | - golang version: 20 | - Kubernetes version (use `kubectl version`): 21 | - Cloud provider or hardware configuration: 22 | - OS (e.g. from /etc/os-release): 23 | - Kernel (e.g. `uname -a`): 24 | - Install tools: 25 | - Others: 26 | 27 | -------------------------------------------------------------------------------- /ui/src/app/modules/login/services/login.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { BACKEND_AUTH_URL } from '../../../app.constants'; 4 | 5 | @Injectable() 6 | export class LoginService { 7 | url: string; 8 | private sessionID: string; 9 | constructor(private http: HttpClient) { 10 | this.url = BACKEND_AUTH_URL + 'login'; 11 | } 12 | 13 | public sendLoginCredential(credentials) { 14 | const httpPostOptions = 15 | { 16 | withCredentials: true, 17 | } 18 | return this.http.post(this.url, credentials, httpPostOptions); 19 | } 20 | } -------------------------------------------------------------------------------- /ui/src/app/modules/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { ClarityModule } from '@clr/angular'; 5 | import { LoginComponent } from './components/login.component'; 6 | import { LoginService } from './services/login.service'; 7 | 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, ClarityModule, FormsModule 12 | ], 13 | exports: [LoginComponent], 14 | declarations: [LoginComponent], 15 | providers: [LoginService], 16 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 17 | }) 18 | export class LoginModule { } -------------------------------------------------------------------------------- /cluster/artifacts/group-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "vmware.purser.com/v1", 3 | "kind": "Group", 4 | "metadata": { 5 | "name": "" 6 | }, 7 | "spec": { 8 | "name": "", 9 | "labels": { 10 | "expr1": { 11 | "": [ 12 | "", 13 | "" 14 | ], 15 | "": [ 16 | "" 17 | ] 18 | }, 19 | "expr2": { 20 | "": [ 21 | "" 22 | ], 23 | "": [ 24 | "" 25 | ] 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /ui/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /cluster/helm/chart/purser/templates/purser-controller-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "purser.fullname" . }}-controller 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "purser.name" . }} 8 | helm.sh/chart: {{ include "purser.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | spec: 12 | type: {{ .Values.controller.service.type }} 13 | ports: 14 | - port: 3030 15 | targetPort: http 16 | protocol: TCP 17 | selector: 18 | app.kubernetes.io/name: {{ include "purser.name" . }}-controller 19 | app.kubernetes.io/instance: {{ .Release.Name }} 20 | -------------------------------------------------------------------------------- /ui/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /cluster/helm/chart/purser/templates/purser-ui-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "purser.fullname" . }}-ui 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "purser.name" . }}-ui 8 | helm.sh/chart: {{ include "purser.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | spec: 12 | type: {{ .Values.ui.service.type }} 13 | ports: 14 | - port: {{ .Values.ui.service.port }} 15 | targetPort: http 16 | protocol: TCP 17 | name: http 18 | selector: 19 | app.kubernetes.io/name: {{ include "purser.name" . }}-ui 20 | app.kubernetes.io/instance: {{ .Release.Name }} 21 | -------------------------------------------------------------------------------- /pkg/apis/groups/v1/docs.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package v1 19 | -------------------------------------------------------------------------------- /pkg/apis/subscriber/v1/docs.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package v1 19 | -------------------------------------------------------------------------------- /ui/src/app/modules/changepassword/changepassword.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { ClarityModule } from '@clr/angular'; 5 | import { ChangepasswordComponent } from './components/changepassword.component'; 6 | import { ChangepasswordService } from './services/changepassword.service'; 7 | 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, ClarityModule, FormsModule 12 | ], 13 | exports: [ChangepasswordComponent], 14 | declarations: [ChangepasswordComponent], 15 | providers: [ChangepasswordService], 16 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 17 | }) 18 | export class ChangepasswordModule { } -------------------------------------------------------------------------------- /ui/src/app/modules/login/components/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /ui/src/app/modules/logout/components/logout.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LogoutComponent } from './logout.component'; 4 | 5 | describe('LogoutComponent', () => { 6 | let component: LogoutComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LogoutComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LogoutComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /ui/src/app/modules/options/components/options.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { OptionsComponent } from './options.component'; 4 | 5 | describe('OptionsComponent', () => { 6 | let component: OptionsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ OptionsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(OptionsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /ui/src/app/modules/topologyGraph/modules.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { ClarityModule } from '@clr/angular'; 5 | import { Routes, RouterModule } from '@angular/router'; 6 | import { TopologyGraphComponent } from './components/topologyGraph.component'; 7 | import { TopologyGraphService } from './services/topologyGraph.service'; 8 | 9 | @NgModule({ 10 | imports: [RouterModule, CommonModule, ClarityModule, FormsModule], 11 | declarations: [TopologyGraphComponent], 12 | exports: [TopologyGraphComponent], 13 | providers: [TopologyGraphService], 14 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 15 | }) 16 | export class TopologyGraphModule { 17 | 18 | } -------------------------------------------------------------------------------- /ui/Dockerfile.deploy.purser: -------------------------------------------------------------------------------- 1 | FROM node:9.6.1 as builder 2 | 3 | LABEL maintainer = "VMware " 4 | LABEL author = "Krishna Karthik " 5 | 6 | # set working directory 7 | RUN mkdir /usr/src/app 8 | WORKDIR /usr/src/app 9 | 10 | # add `/usr/src/app/node_modules/.bin` to $PATH 11 | ENV PATH /usr/src/app/node_modules/.bin:$PATH 12 | 13 | # install and cache app dependencies 14 | COPY package.json package-lock.json ./ 15 | RUN npm install 16 | RUN npm install -g @angular/cli@6.2.1 17 | 18 | # add purser application to the working directory 19 | COPY . . 20 | 21 | # start purser application 22 | RUN npm run build 23 | 24 | # Build a small nginx image 25 | FROM nginx:latest 26 | COPY nginx.conf /etc/nginx/conf.d/default.conf 27 | COPY --from=builder /usr/src/app/dist /usr/share/nginx/html -------------------------------------------------------------------------------- /ui/src/app/modules/topo-graph/components/topo-graph.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TopoGraphComponent } from './topo-graph.component'; 4 | 5 | describe('TopoGraphComponent', () => { 6 | let component: TopoGraphComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TopoGraphComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TopoGraphComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /ui/src/app/modules/capacity-graph/capacity-graph.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { ClarityModule } from '@clr/angular'; 5 | import { GoogleChartsModule } from 'angular-google-charts'; 6 | import { CapactiyGraphComponent } from './components/capactiy-graph.component'; 7 | import { CapacityGraphService } from './services/capacity-graph.service'; 8 | 9 | 10 | @NgModule({ 11 | imports: [ 12 | CommonModule, ClarityModule, FormsModule, GoogleChartsModule 13 | ], 14 | exports: [CapactiyGraphComponent], 15 | declarations: [CapactiyGraphComponent], 16 | providers: [CapacityGraphService], 17 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 18 | }) 19 | export class CapacityGraphModule { } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | **What this PR does / why we need it**: 6 | 7 | **Which issue(s) this PR fixes** *(optional, in `fixes #(, fixes #, ...)` format, will close the issue(s) when PR gets merged)*: 8 | Fixes # 9 | 10 | **Special notes for your reviewer**: 11 | 12 | **Release note**: 13 | 17 | ```release-note 18 | 19 | ``` 20 | -------------------------------------------------------------------------------- /ui/src/app/modules/logical-group/components/logical-group.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LogicalGroupComponent } from './logical-group.component'; 4 | 5 | describe('LogicalGroupComponent', () => { 6 | let component: LogicalGroupComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LogicalGroupComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LogicalGroupComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /ui/src/app/modules/logical-group/logical-group.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { ClarityModule } from '@clr/angular'; 5 | import { GoogleChartsModule } from 'angular-google-charts'; 6 | import { LogicalGroupComponent } from './components/logical-group.component'; 7 | import { LogicalGroupService } from './services/logical-group.service'; 8 | 9 | 10 | @NgModule({ 11 | imports: [ 12 | CommonModule, ClarityModule, FormsModule, GoogleChartsModule 13 | ], 14 | exports: [LogicalGroupComponent], 15 | declarations: [LogicalGroupComponent], 16 | providers: [LogicalGroupService], 17 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 18 | }) 19 | export class LogicalGroupModule { } 20 | -------------------------------------------------------------------------------- /cluster/minimal/purser-ui-setup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: purser-ui 5 | labels: 6 | run: purser-ui 7 | app: purser 8 | spec: 9 | selector: 10 | app: purser 11 | run: purser-ui 12 | ports: 13 | - protocol: TCP 14 | port: 80 15 | targetPort: 4200 16 | type: LoadBalancer 17 | --- 18 | apiVersion: apps/v1 19 | kind: Deployment 20 | metadata: 21 | name: purser-ui 22 | spec: 23 | selector: 24 | matchLabels: 25 | app: purser 26 | run: purser-ui 27 | replicas: 1 28 | template: 29 | metadata: 30 | labels: 31 | app: purser 32 | run: purser-ui 33 | spec: 34 | containers: 35 | - name: purser-ui 36 | image: kreddyj/purser:ui-1.0.2 37 | imagePullPolicy: Always 38 | ports: 39 | - containerPort: 4200 40 | -------------------------------------------------------------------------------- /ui/src/app/modules/capacity-graph/components/capactiy-graph.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CapactiyGraphComponent } from './capactiy-graph.component'; 4 | 5 | describe('CapactiyGraphComponent', () => { 6 | let component: CapactiyGraphComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CapactiyGraphComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CapactiyGraphComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /ui/src/app/modules/topo-graph/modules.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { ClarityModule } from '@clr/angular'; 5 | import { Routes, RouterModule } from '@angular/router'; 6 | import { TopoGraphComponent } from './components/topo-graph.component'; 7 | import { TopoGraphService } from './services/topo-graph.service'; 8 | import { GoogleChartsModule } from 'angular-google-charts'; 9 | 10 | @NgModule({ 11 | imports: [RouterModule, CommonModule, ClarityModule, FormsModule, GoogleChartsModule], 12 | declarations: [TopoGraphComponent], 13 | exports: [TopoGraphComponent], 14 | providers: [TopoGraphService], 15 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 16 | }) 17 | export class TopoGraphModule { 18 | 19 | } -------------------------------------------------------------------------------- /ui/src/app/modules/changepassword/components/changepassword.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChangepasswordComponent } from './changepassword.component'; 4 | 5 | describe('CapactiyGraphComponent', () => { 6 | let component: ChangepasswordComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChangepasswordComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChangepasswordComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /ui/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /ui/src/app/modules/capacity-graph/components/capactiy-graph.component.scss: -------------------------------------------------------------------------------- 1 | .graphCardBlock{ 2 | ::ng-deep .googleChart{ 3 | width: 100%; 4 | } 5 | .headerBlock{ 6 | display: flex; 7 | .headerText{ 8 | font-size: 18px; 9 | } 10 | .card-title{ 11 | flex: 1; 12 | } 13 | .toggleDiv{ 14 | .viewSwitchLeftLabel{ 15 | padding-right: 5px; 16 | } 17 | } 18 | } 19 | .card-text{ 20 | text-align: center; 21 | } 22 | .radioWrapper{ 23 | padding: 5px; 24 | .radioLabel{ 25 | padding-left: 5px; 26 | } 27 | } 28 | .googleChartDiv{ 29 | padding-top: 10px; 30 | } 31 | .selectDiv{ 32 | display: flex; 33 | .selectDropdownDiv{ 34 | padding-left: 60px; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /cluster/helm/chart/purser/templates/purser-database-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "purser.fullname" . }}-database 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "purser.name" . }} 8 | helm.sh/chart: {{ include "purser.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | spec: 12 | type: {{ .Values.database.service.type }} 13 | ports: 14 | - port: 5080 15 | targetPort: 5080 16 | name: zero-grpc 17 | - port: 6080 18 | targetPort: 6080 19 | name: zero-http 20 | - port: 8080 21 | targetPort: 8080 22 | name: server-http 23 | - port: 9080 24 | targetPort: 9080 25 | name: server-grpc 26 | selector: 27 | app.kubernetes.io/name: {{ include "purser.name" . }}-database 28 | app.kubernetes.io/instance: {{ .Release.Name }} 29 | -------------------------------------------------------------------------------- /docs/plugin-installation.md: -------------------------------------------------------------------------------- 1 | # Purser Plugin Setup 2 | _NOTE: This Plugin installation is optional. Install it if you want to use CLI of Purser._ 3 | 4 | ## Linux and macOS 5 | 6 | ``` bash 7 | # Binary installation 8 | wget -q https://github.com/vmware/purser/blob/master/build/purser-binary-install.sh && sh purser-binary-install.sh 9 | ``` 10 | 11 | Enter your cluster's configuration path when prompted. The plugin binary needs to be in your `PATH` environment variable, so once the download of the binary is finished the script tries to move it to `/usr/local/bin`. This may need your sudo permission. 12 | 13 | ## Windows/Others 14 | 15 | For installation on Windows follow the steps in the [manual installation guide](./docs/manual-installation.md). 16 | 17 | ## Uninstalling Purser Plugin 18 | 19 | ### Linux/macOS 20 | 21 | ``` bash 22 | curl https://raw.githubusercontent.com/vmware/purser/master/build/purser-binary-install.sh -O && sh purser-binary-uninstall.sh 23 | ``` 24 | -------------------------------------------------------------------------------- /cluster/helm/chart/purser/templates/purser-ui-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "purser.fullname" . }}-ui 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "purser.name" . }}-ui 8 | helm.sh/chart: {{ include "purser.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | data: 12 | nginx.conf: | 13 | upstream purser { 14 | server {{ include "purser.fullname" . }}-controller:3030; 15 | } 16 | server { 17 | listen 4200; 18 | 19 | location /auth { 20 | proxy_pass http://purser; 21 | } 22 | 23 | location /api { 24 | proxy_pass http://purser; 25 | } 26 | 27 | location / { 28 | root /usr/share/nginx/html/purser; 29 | index index.html index.htm; 30 | try_files $uri $uri/ /index.html =404; 31 | } 32 | } -------------------------------------------------------------------------------- /pkg/controller/utils/purge_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "testing" 22 | 23 | "github.com/vmware/purser/test/utils" 24 | ) 25 | 26 | func TestHexToDecIP(t *testing.T) { 27 | act := hexToDecIP("030310AC") 28 | exp := "172.16.3.3" 29 | utils.Equals(t, exp, act) 30 | } 31 | -------------------------------------------------------------------------------- /cluster/purser-ui-setup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: purser-ui 5 | labels: 6 | run: purser-ui 7 | app: purser 8 | spec: 9 | selector: 10 | app: purser 11 | run: purser-ui 12 | ports: 13 | - protocol: TCP 14 | port: 80 15 | targetPort: 4200 16 | type: LoadBalancer 17 | --- 18 | apiVersion: apps/v1 19 | kind: Deployment 20 | metadata: 21 | name: purser-ui 22 | spec: 23 | selector: 24 | matchLabels: 25 | app: purser 26 | run: purser-ui 27 | replicas: 1 28 | template: 29 | metadata: 30 | labels: 31 | app: purser 32 | run: purser-ui 33 | spec: 34 | containers: 35 | - name: purser-ui 36 | image: kreddyj/purser:ui-1.0.2 37 | imagePullPolicy: Always 38 | resources: 39 | limits: 40 | memory: 1200Mi 41 | cpu: 500m 42 | requests: 43 | memory: 1200Mi 44 | cpu: 500m 45 | ports: 46 | - containerPort: 4200 -------------------------------------------------------------------------------- /ui/src/app/modules/options/components/options.component.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { BACKEND_URL } from '../../../app.constants'; 4 | 5 | @Component({ 6 | selector: 'app-options', 7 | templateUrl: './options.component.html', 8 | styleUrls: ['./options.component.scss'] 9 | }) 10 | export class OptionsComponent implements OnInit { 11 | public SYNC_STATUS = "wait"; 12 | ngOnInit() { 13 | this.SYNC_STATUS = "wait"; 14 | } 15 | 16 | constructor(private http: HttpClient) { } 17 | 18 | public initiateSync() { 19 | let syncURL = BACKEND_URL + 'sync'; 20 | const syncOptions = { 21 | withCredentials: true 22 | }; 23 | this.http.get(syncURL, syncOptions).subscribe((response) => { 24 | this.SYNC_STATUS = "requested"; 25 | console.log("sync status", this.SYNC_STATUS); 26 | }, (err) => { 27 | console.log("Error", err); 28 | this.SYNC_STATUS = "failed"; 29 | console.log("sync request status", this.SYNC_STATUS); 30 | }); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /ui/src/app/modules/capacity-graph/services/capacity-graph.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { BACKEND_URL } from '../../../app.constants'; 4 | 5 | @Injectable() 6 | export class CapacityGraphService { 7 | constructor(private http: HttpClient) { 8 | 9 | } 10 | 11 | public getCapacityData(view?, type?, name?) { 12 | let _devUrl: string = './json/capacity.json'; 13 | let _url: string = BACKEND_URL + 'metrics'; 14 | 15 | if (type) { 16 | _url = _url + '/' + type; 17 | } 18 | 19 | if (view && !name) { 20 | _url = _url + '?view=physical'; 21 | } 22 | 23 | if (name) { 24 | _url = _url + '?name=' + name; 25 | _devUrl = './json/capacity1.json'; //testing purpose 26 | } 27 | 28 | //console.log(_url); 29 | 30 | return this.http.get(_url, { 31 | observe: 'body', 32 | responseType: 'json', 33 | withCredentials: true, 34 | }); 35 | } 36 | } -------------------------------------------------------------------------------- /ui/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /ui/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach(async(() => { 5 | TestBed.configureTestingModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | }).compileComponents(); 10 | })); 11 | it('should create the app', async(() => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | })); 16 | it(`should have as title 'purser'`, async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app.title).toEqual('purser'); 20 | })); 21 | it('should render title in a h1 tag', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.debugElement.nativeElement; 25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to purser!'); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /ui/src/app/modules/topo-graph/services/topo-graph.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { CookieService } from 'ngx-cookie-service'; 4 | import { BACKEND_URL } from '../../../app.constants'; 5 | 6 | @Injectable() 7 | export class TopoGraphService { 8 | constructor(private http: HttpClient, private cookieService: CookieService) { 9 | 10 | } 11 | 12 | public getTopoData(view?, type?, name?) { 13 | let _devUrl: string = './json/topology.json'; 14 | let _url: string = BACKEND_URL + 'hierarchy'; 15 | 16 | if (type) { 17 | _url = _url + '/' + type; 18 | } 19 | 20 | if (view && !name) { 21 | _url = _url + '?view=physical'; 22 | } 23 | 24 | if (name) { 25 | _url = _url + '?name=' + name; 26 | _devUrl = './json/topology1.json'; //testing purpose 27 | } 28 | 29 | return this.http.get(_url, { 30 | observe: 'body', 31 | responseType: 'json', 32 | withCredentials: true, 33 | }); 34 | } 35 | } -------------------------------------------------------------------------------- /pkg/plugin/node.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package plugin 19 | 20 | import ( 21 | v1 "k8s.io/api/core/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | // GetClusterNodes returns the list of nodes in the cluster. 26 | func GetClusterNodes() []v1.Node { 27 | nodes, err := ClientSetInstance.CoreV1().Nodes().List(metav1.ListOptions{}) 28 | if err != nil { 29 | panic(err.Error()) 30 | } 31 | return nodes.Items 32 | } 33 | -------------------------------------------------------------------------------- /test/pricing/pricing_aws_test.go: -------------------------------------------------------------------------------- 1 | package pricing 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/vmware/purser/test/utils" 7 | 8 | "github.com/Sirupsen/logrus" 9 | "github.com/vmware/purser/pkg/controller/dgraph" 10 | "github.com/vmware/purser/pkg/controller/dgraph/models" 11 | "github.com/vmware/purser/pkg/pricing/aws" 12 | ) 13 | 14 | // TestAWSPricingFlow it should populate your dgraph running at localhost 9080 port with aws compute and storage prices 15 | // The following dgraph query will give the rate card data 16 | // { 17 | // rateCard(func: has(isRateCard)) { 18 | // cloudProvider 19 | // region 20 | // nodePrices { 21 | // instanceType 22 | // operatingSystem 23 | // price 24 | // instanceFamily 25 | // } 26 | // storagePrices { 27 | // volumeType 28 | // usageType 29 | // price 30 | // } 31 | // } 32 | // } 33 | func TestAWSPricingFlow(t *testing.T) { 34 | logrus.SetLevel(logrus.DebugLevel) 35 | dgraph.Start("localhost", "9080") 36 | rateCard := aws.GetRateCardForAWS("us-east-1") 37 | models.StoreRateCard(rateCard) 38 | defer dgraph.Close() 39 | utils.Assert(t, rateCard != nil, "rate card is nil") 40 | } 41 | -------------------------------------------------------------------------------- /cmd/plugin/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package main 19 | 20 | // These are possible actions for resources 21 | const ( 22 | Get = "get" 23 | Set = "set" 24 | ) 25 | 26 | // These are kubernetes components 27 | const ( 28 | Label = "label" 29 | Pod = "pod" 30 | Node = "node" 31 | Namespace = "namespace" 32 | Group = "group" 33 | ) 34 | 35 | // These are utilisation metrics 36 | const ( 37 | Cost = "cost" 38 | Resources = "resources" 39 | ) 40 | -------------------------------------------------------------------------------- /cluster/helm/chart/purser/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "purser.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "purser.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "purser.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /cmd/controller/api/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package api 19 | 20 | import ( 21 | "net/http" 22 | "time" 23 | 24 | "github.com/Sirupsen/logrus" 25 | ) 26 | 27 | // Logger implements web logging logic 28 | func Logger(inner http.Handler, name string) http.Handler { 29 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 30 | start := time.Now() 31 | inner.ServeHTTP(w, r) 32 | logrus.Infof( 33 | "%s\t%s\t%s\t%s", 34 | r.Method, 35 | r.RequestURI, 36 | name, 37 | time.Since(start), 38 | ) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /cmd/controller/api/router.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package api 19 | 20 | import ( 21 | "github.com/gorilla/mux" 22 | ) 23 | 24 | // NewRouter returns a new instance of the router 25 | func NewRouter() *mux.Router { 26 | router := mux.NewRouter().StrictSlash(true) 27 | for _, route := range routes { 28 | handlerFunc := route.HandlerFunc 29 | handler := Logger(handlerFunc, route.Name) 30 | 31 | router. 32 | Methods(route.Method). 33 | Path(route.Pattern). 34 | Name(route.Name). 35 | Handler(handler) 36 | } 37 | return router 38 | } 39 | -------------------------------------------------------------------------------- /cluster/helm/chart/purser/templates/purser-ui-ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ui.ingress.enabled -}} 2 | {{- $fullName := include "purser.fullname" . -}} 3 | apiVersion: extensions/v1beta1 4 | kind: Ingress 5 | metadata: 6 | name: {{ $fullName }} 7 | namespace: {{ .Release.Namespace }} 8 | labels: 9 | app.kubernetes.io/name: {{ include "purser.name" . }} 10 | helm.sh/chart: {{ include "purser.chart" . }} 11 | app.kubernetes.io/instance: {{ .Release.Name }} 12 | app.kubernetes.io/managed-by: {{ .Release.Service }} 13 | {{- with .Values.ui.ingress.annotations }} 14 | annotations: 15 | {{- toYaml . | nindent 4 }} 16 | {{- end }} 17 | spec: 18 | {{- if .Values.ui.ingress.tls }} 19 | tls: 20 | {{- range .Values.ui.ingress.tls }} 21 | - hosts: 22 | {{- range .hosts }} 23 | - {{ . | quote }} 24 | {{- end }} 25 | secretName: {{ .secretName }} 26 | {{- end }} 27 | {{- end }} 28 | rules: 29 | {{- range .Values.ui.ingress.hosts }} 30 | - host: {{ .host | quote }} 31 | http: 32 | paths: 33 | {{- range .paths }} 34 | - path: {{ . }} 35 | backend: 36 | serviceName: {{ $fullName }}-ui 37 | servicePort: http 38 | {{- end }} 39 | {{- end }} 40 | {{- end }} 41 | -------------------------------------------------------------------------------- /pkg/utils/logutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "io" 22 | "os" 23 | 24 | log "github.com/Sirupsen/logrus" 25 | ) 26 | 27 | const logFile = "purser.log" 28 | 29 | // InitializeLogger sets and configures logger options. 30 | func InitializeLogger(logLevel string) { 31 | logFile := OpenFile(logFile) 32 | 33 | log.SetOutput(io.MultiWriter(os.Stdout, logFile)) 34 | log.SetFormatter(&log.TextFormatter{ForceColors: true}) 35 | 36 | if logLevel == "debug" { 37 | log.SetLevel(log.DebugLevel) 38 | } else { 39 | log.SetLevel(log.InfoLevel) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pkg/controller/payload.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package controller 19 | 20 | import meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | // PayloadWrapper holds additional information about payload 23 | type PayloadWrapper struct { 24 | Data []*interface{} `json:"data"` 25 | } 26 | 27 | // Payload holds payload information 28 | type Payload struct { 29 | Key string `json:"key"` 30 | EventType string `json:"eventType"` 31 | ResourceType string `json:"resourceType"` 32 | CloudType string `json:"cloudType"` 33 | Data string `json:"data"` 34 | CaptureTime meta_v1.Time 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/app/modules/logical-group/services/logical-group.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { BACKEND_URL } from '../../../app.constants'; 4 | 5 | @Injectable() 6 | export class LogicalGroupService { 7 | constructor(private http: HttpClient) { 8 | 9 | } 10 | 11 | public getLogicalGroupData(name?) { 12 | let _devUrl: string = './json/logicalGroup.json'; 13 | let _url: string = BACKEND_URL + 'groups'; 14 | 15 | if (name) { 16 | _url = _url + '?name=' + name; 17 | _devUrl = './json/logicalGroup1.json'; //testing purpose 18 | } 19 | 20 | //console.log(_url); 21 | 22 | return this.http.get(_url, { 23 | observe: 'body', 24 | responseType: 'json', 25 | withCredentials: true, 26 | }); 27 | } 28 | 29 | public deleteCustomGroup(name) { 30 | let _url: string = BACKEND_URL + 'group/delete?name=' + name; 31 | return this.http.post(_url, null, { withCredentials: true }) 32 | } 33 | 34 | public createCustomGroup(groupDef) { 35 | let _url: string = BACKEND_URL + 'group/create'; 36 | return this.http.post(_url, groupDef, { withCredentials: true }) 37 | } 38 | } -------------------------------------------------------------------------------- /ui/src/app/modules/login/components/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { AppComponent } from '../../../app.component'; 5 | import { LoginService } from '../services/login.service'; 6 | 7 | @Component({ 8 | selector: 'app-login', 9 | templateUrl: './login.component.html', 10 | styleUrls: ['./login.component.scss'] 11 | }) 12 | export class LoginComponent implements OnInit { 13 | public form: any = {}; 14 | public LOGIN_STATUS = "wait"; 15 | ngOnInit() { 16 | this.LOGIN_STATUS = "wait"; 17 | this.appComponent.IS_LOGEDIN = false; 18 | } 19 | 20 | constructor(private router: Router, private loginService: LoginService, private appComponent: AppComponent) { } 21 | 22 | public submitLogin() { 23 | var credentials = JSON.stringify(this.form); 24 | let observableEntity: Observable = this.loginService.sendLoginCredential(credentials); 25 | observableEntity.subscribe((response) => { 26 | this.LOGIN_STATUS = "success"; 27 | this.appComponent.IS_LOGEDIN = true; 28 | this.router.navigateByUrl('/group'); 29 | }, (err) => { 30 | this.LOGIN_STATUS = "wrong"; 31 | this.appComponent.IS_LOGEDIN = false; 32 | }); 33 | } 34 | } -------------------------------------------------------------------------------- /pkg/controller/dgraph/models/query/subscriber.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package query 19 | 20 | import ( 21 | "github.com/vmware/purser/pkg/controller/dgraph/models" 22 | ) 23 | 24 | type subscriberRoot struct { 25 | Subscribers []models.SubscriberCRD `json:"subscribers"` 26 | } 27 | 28 | // RetrieveSubscribers gets all live subscribers 29 | func RetrieveSubscribers() ([]models.SubscriberCRD, error) { 30 | q := getQueryForSubscribersRetrieval() 31 | newRoot := subscriberRoot{} 32 | err := executeQuery(q, &newRoot) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return newRoot.Subscribers, nil 37 | } 38 | -------------------------------------------------------------------------------- /.make/Makefile.deploy.purser: -------------------------------------------------------------------------------- 1 | DEPLOY_DOCKERFILE?=ui/Dockerfile.deploy.purser 2 | 3 | CLUSTER_DIR?=${PWD}/cluster 4 | 5 | COMMIT:=$(shell git rev-parse --short HEAD) 6 | TIMESTAMP:=$(shell date +%s) 7 | TAG?=$(COMMIT)-$(TIMESTAMP) 8 | 9 | .PHONY: deploy-purser 10 | deploy-purser: kubectl-deploy-purser-db kubectl-deploy-purser-ui 11 | 12 | .PHONY: kubectl-deploy-purser-ui 13 | kubectl-deploy-purser-ui: 14 | @echo "Deploys purser-ui service" 15 | @kubectl create -f $(CLUSTER_DIR)/purser-ui.yaml 16 | 17 | .PHONY: deploy-purser-ui 18 | deploy-purser-ui: build-purser-ui-image push-purser-ui-image 19 | 20 | .PHONY: build-purser-ui-image 21 | build-purser-ui-image: 22 | @docker build --build-arg BINARY=purser-ui -t $(REGISTRY)/$(DOCKER_REPO)/purser-ui -f $(DEPLOY_DOCKERFILE) . 23 | @docker tag $(REGISTRY)/$(DOCKER_REPO)/purser-ui $(REGISTRY)/$(DOCKER_REPO)/purser-ui:$(TAG) 24 | 25 | .PHONY: push-purser-ui-image 26 | push-purser-ui-image: build-purser-ui-image 27 | @docker push $(REGISTRY)/$(DOCKER_REPO)/purser-ui 28 | 29 | .PHONY: clean-purser-ui-image 30 | clean-purser-ui-image: 31 | @docker rmi -f $(REGISTRY)/$(DOCKER_REPO)/purser-ui 32 | 33 | .PHONY: kubectl-deploy-purser-db 34 | kubectl-deploy-purser-db: 35 | @echo "Deploys purser purser-db service" 36 | @kubectl create -f $(CLUSTER_DIR)/purser-db.yaml 37 | 38 | -------------------------------------------------------------------------------- /ui/src/app/modules/logout/components/logout.component.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { AppComponent } from '../../../app.component'; 5 | import { BACKEND_AUTH_URL } from '../../../app.constants'; 6 | 7 | @Component({ 8 | selector: 'app-logout', 9 | templateUrl: './logout.component.html', 10 | styleUrls: ['./logout.component.scss'] 11 | }) 12 | export class LogoutComponent implements OnInit { 13 | public form: any = {}; 14 | public LOGIN_STATUS = "wait"; 15 | ngOnInit() { 16 | this.handleLogout(); 17 | this.LOGIN_STATUS = "wait"; 18 | } 19 | 20 | constructor(private router: Router, private http: HttpClient, private appComponent: AppComponent) { } 21 | 22 | public handleLogout() { 23 | let logoutURL = BACKEND_AUTH_URL + 'logout'; 24 | const logoutOptions = { 25 | withCredentials: true 26 | }; 27 | this.http.post(logoutURL, JSON.stringify({}), logoutOptions).subscribe((response) => { 28 | this.appComponent.IS_LOGEDIN = false; 29 | }, (err) => { 30 | console.log("Error", err); 31 | } 32 | ); 33 | this.router.navigateByUrl("./login"); 34 | } 35 | } -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # Purser UI 2 | 3 | Purser UI is designed to provide a visual representation to a host of features provided by Purser such as **cluster hierarchy**, **Pod and Service interactions** and **capacity allocations** for CPU, memory, disk space and other resources. 4 | 5 | It has been generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.2.1 and [Clarity Design System](https://clarity.design/). 6 | 7 | ## Installing Dependencies 8 | 9 | Use "npm" or "yarn" to install/manage dependencies. Run `npm install` inside this directory to install all the needed dependencies. 10 | 11 | ## Development server 12 | 13 | Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 14 | 15 | ## Code scaffolding 16 | 17 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 18 | 19 | ## Build 20 | 21 | Run `npm build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 22 | 23 | ## Further help 24 | 25 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). -------------------------------------------------------------------------------- /ui/src/app/app.routing.ts: -------------------------------------------------------------------------------- 1 | /*Framework imports, 3rd party imports */ 2 | import { ModuleWithProviders } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | import { LogicalGroupComponent } from './modules/logical-group/components/logical-group.component' 5 | import { TopologyGraphComponent } from './modules/topologyGraph/components/topologyGraph.component' 6 | import { TopoGraphComponent } from './modules/topo-graph/components/topo-graph.component' 7 | import { CapactiyGraphComponent } from './modules/capacity-graph/components/capactiy-graph.component' 8 | import { OptionsComponent } from './modules/options/components/options.component' 9 | import { ChangepasswordComponent } from './modules/changepassword/components/changepassword.component' 10 | 11 | export const ROUTES: Routes = [ 12 | { path: 'group', component: LogicalGroupComponent }, 13 | { path: 'network', component: TopologyGraphComponent }, 14 | { path: 'hierarchy', component: TopoGraphComponent }, 15 | { path: 'capacity', component: CapactiyGraphComponent }, 16 | { path: 'changepassword', component: ChangepasswordComponent }, 17 | { path: 'options', component: OptionsComponent }, 18 | { path: '**', redirectTo: 'group', pathMatch: 'full' } 19 | ]; 20 | 21 | export const ROUTING: ModuleWithProviders = RouterModule.forRoot(ROUTES); -------------------------------------------------------------------------------- /pkg/controller/dgraph/models/query/helpers_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package query 19 | 20 | import ( 21 | "strconv" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | // TestGetSecondsSinceMonthStart ... 28 | func TestGetSecondsSinceMonthStart(t *testing.T) { 29 | maxSecondsInAMonth := 2678400.0 30 | got := getSecondsSinceMonthStart() 31 | gotFloat, err := strconv.ParseFloat(got, 64) 32 | assert.NoError(t, err, "unable to convert secondsSinceMonthStart to float64") 33 | assert.False(t, gotFloat > maxSecondsInAMonth, "secondsSinceMonthStart can't be greater than 2678400") 34 | assert.False(t, gotFloat < 0, "secondsSinceMonthStart can't be less than 0") 35 | } 36 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "k8s.io/api" 30 | version = "kubernetes-1.9.0" 31 | 32 | [[constraint]] 33 | name = "k8s.io/apiextensions-apiserver" 34 | version = "kubernetes-1.9.0" 35 | 36 | [[constraint]] 37 | name = "k8s.io/apimachinery" 38 | version = "kubernetes-1.9.0" 39 | 40 | [[constraint]] 41 | name = "k8s.io/client-go" 42 | version = "6.0.0" 43 | 44 | [[constraint]] 45 | name = "google.golang.org/grpc" 46 | version = "1.15.0" 47 | 48 | [[constraint]] 49 | name = "github.com/dgraph-io/dgo" 50 | branch = "master" 51 | 52 | [[override]] 53 | name = "github.com/tidwall/gjson" 54 | version = "1.1.2" 55 | 56 | [prune] 57 | go-tests = true 58 | unused-packages = true 59 | 60 | -------------------------------------------------------------------------------- /pkg/utils/fileutils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "os" 22 | "os/user" 23 | 24 | log "github.com/Sirupsen/logrus" 25 | ) 26 | 27 | // OpenFile handles opening file in Read/Write mode, creating and appending to it as needed. 28 | func OpenFile(filename string) *os.File { 29 | f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600) 30 | if err != nil { 31 | log.Errorf("failed to open file %s, %v", filename, err) 32 | } 33 | return f 34 | } 35 | 36 | // GetUsrHomeDir returns the current user's Home Directory 37 | func GetUsrHomeDir() string { 38 | usr, err := user.Current() 39 | if err != nil { 40 | log.Errorf("failed to fetch current user %v", err) 41 | } 42 | return usr.HomeDir 43 | } 44 | -------------------------------------------------------------------------------- /cmd/controller/api/api.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package api 19 | 20 | import ( 21 | "net/http" 22 | 23 | "github.com/Sirupsen/logrus" 24 | "github.com/gorilla/handlers" 25 | "github.com/vmware/purser/cmd/controller/api/apiHandlers" 26 | "github.com/vmware/purser/pkg/controller" 27 | ) 28 | 29 | // StartServer starts api server 30 | func StartServer(conf controller.Config) { 31 | apiHandlers.SetKubeClientAndGroupClient(conf) 32 | allowedOrigins := handlers.AllowedOrigins([]string{"*"}) 33 | allowedCredentials := handlers.AllowCredentials() 34 | router := NewRouter() 35 | logrus.Info("Purser server started on port `localhost:3030`") 36 | logrus.Fatal(http.ListenAndServe(":3030", handlers.CORS(allowedOrigins, allowedCredentials)(router))) 37 | } 38 | -------------------------------------------------------------------------------- /ui/src/app/modules/topologyGraph/services/topologyGraph.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { BACKEND_URL } from '../../../app.constants'; 4 | 5 | @Injectable() 6 | export class TopologyGraphService { 7 | constructor(private http: HttpClient) { 8 | 9 | } 10 | 11 | public getNodes(serviceName) { 12 | let _devUrl: string = './json/nodes.json'; 13 | let _url: string = BACKEND_URL + 'nodes'; 14 | if (serviceName && serviceName !== 'ALL') { 15 | _url = _url + '?service=' + serviceName; 16 | } 17 | 18 | return this.http.get(_url, { 19 | observe: 'body', 20 | responseType: 'json', 21 | withCredentials: true 22 | }); 23 | } 24 | 25 | public getEdges(serviceName) { 26 | let _devUrl: string = './json/edges.json'; 27 | let _url: string = BACKEND_URL + 'edges'; 28 | if (serviceName && serviceName !== 'ALL') { 29 | _url = _url + '?service=' + serviceName; 30 | } 31 | 32 | return this.http.get(_url, { 33 | observe: 'body', 34 | responseType: 'json', 35 | withCredentials: true 36 | }); 37 | } 38 | 39 | public getServiceList() { 40 | let _devUrl: string = './json/serviceList.json'; 41 | let _url: string = BACKEND_URL + 'services'; 42 | 43 | return this.http.get(_url, { 44 | observe: 'body', 45 | responseType: 'json', 46 | withCredentials: true 47 | }); 48 | } 49 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Contributor Code of Conduct 2 | ====================== 3 | 4 | As contributors and maintainers of this project, we pledge to respect 5 | everyone who contributes by posting issues, updating documentation, 6 | submitting pull requests, providing feedback in comments, and any other 7 | activities. 8 | 9 | Communication through any project channels (GitHub, mailing lists, 10 | Twitter, and so on) must be constructive and never resort to personal 11 | attacks, trolling, public or private harassment, insults, or other 12 | unprofessional conduct. 13 | 14 | We promise to extend courtesy and respect to everyone involved in 15 | this project, regardless of gender, gender identity, sexual 16 | orientation, disability, age, race, ethnicity, religious beliefs, 17 | or level of experience. We expect anyone contributing to this project 18 | to do the same. 19 | 20 | If any member of the community violates this code of conduct, the 21 | maintainers of this project may take action, including removing issues, 22 | comments, and PRs or blocking accounts, as deemed appropriate. 23 | 24 | If you are subjected to or witness unacceptable behavior, or have any 25 | other concerns, please communicate with us. 26 | 27 | If you have suggestions to improve the code of conduct, please submit 28 | an issue or PR. 29 | 30 | 31 | **Attribution** 32 | 33 | This Code of Conduct is adapted from the VMware Clarity project, available at this page: https://github.com/vmware/clarity/blob/master/CODE_OF_CONDUCT.md 34 | -------------------------------------------------------------------------------- /ui/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, RouterEvent } from '@angular/router'; 3 | import { MCommon } from './common/messages/common.messages'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.scss'] 9 | }) 10 | export class AppComponent implements OnInit { 11 | 12 | public routeLoading: boolean = false; 13 | public messages: any = {}; 14 | public IS_LOGEDIN = true; 15 | 16 | constructor(public router: Router) { 17 | this.messages = { 18 | 'common': MCommon 19 | } 20 | } 21 | 22 | private loadApp() { 23 | this.router.events.subscribe((event: RouterEvent) => { 24 | this.navigationEventHandler(event); 25 | }); 26 | } 27 | 28 | private navigationEventHandler(event: RouterEvent): void { 29 | if (event instanceof NavigationStart) { 30 | this.routeLoading = true; 31 | } 32 | if (event instanceof NavigationEnd) { 33 | this.routeLoading = false; 34 | } 35 | 36 | // Set loading state to false in both of the below events to hide the spinner in case a request fails. 37 | if (event instanceof NavigationCancel) { 38 | this.routeLoading = false; 39 | } 40 | if (event instanceof NavigationError) { 41 | this.routeLoading = false; 42 | } 43 | } 44 | 45 | ngOnInit() { 46 | this.loadApp(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /ui/src/app/modules/changepassword/components/changepassword.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; 2 | import { HttpClient } from "@angular/common/http"; 3 | import { Router } from '@angular/router'; 4 | import { Observable } from 'rxjs'; 5 | import { ChangepasswordService } from '../services/changepassword.service'; 6 | 7 | @Component({ 8 | selector: 'app-changepassword', 9 | templateUrl: './changepassword.component.html', 10 | styleUrls: ['./changepassword.component.scss'] 11 | }) 12 | export class ChangepasswordComponent implements OnInit { 13 | public form: any = {}; 14 | public notMatch = false; 15 | public LOGIN_STATUS = "wait"; 16 | ngOnInit() { 17 | this.notMatch = false; 18 | this.LOGIN_STATUS = "wait"; 19 | } 20 | 21 | constructor(private router: Router, private changepasswordService: ChangepasswordService) { } 22 | 23 | public submitChangePassword() { 24 | if (this.form.newPassword != this.form.newPasswordCheck) { 25 | this.notMatch = true; 26 | return; 27 | } 28 | this.notMatch = false; 29 | var credentials = JSON.stringify(this.form); 30 | let observableEntity: Observable = this.changepasswordService.sendLoginCredentials(credentials); 31 | observableEntity.subscribe((response) => { 32 | this.LOGIN_STATUS = "success"; 33 | }, (err) => { 34 | this.LOGIN_STATUS = "wrong"; 35 | }); 36 | } 37 | } -------------------------------------------------------------------------------- /pkg/utils/k8sutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "github.com/Sirupsen/logrus" 22 | "k8s.io/client-go/kubernetes" 23 | "k8s.io/client-go/rest" 24 | "k8s.io/client-go/tools/clientcmd" 25 | ) 26 | 27 | // GetKubeclient returns a k8s clientset from the kubeconfig, if nil fallback to 28 | // client from inCluster config. 29 | func GetKubeclient(config *rest.Config) *kubernetes.Clientset { 30 | clientset, err := kubernetes.NewForConfig(config) 31 | if err != nil { 32 | logrus.Fatalf("failed to create kubernetes clientset: %v", err) 33 | } 34 | return clientset 35 | } 36 | 37 | // GetKubeconfig builds config from the kubeconfig path, if nil fallback to 38 | // inCluster config. 39 | func GetKubeconfig(kubeconfigPath string) (*rest.Config, error) { 40 | return clientcmd.BuildConfigFromFlags("", kubeconfigPath) 41 | } 42 | -------------------------------------------------------------------------------- /ui/src/json/logicalGroup.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "vrbc", 4 | "podsCount": 882, 5 | "mtdCPU": 662.475759, 6 | "cpu": 56.9, 7 | "mtdCPUCost": 15.899418, 8 | "mtdCost": 306.798539, 9 | "mtdMemory": 29043.16396, 10 | "mtdStorage": 3365.862511, 11 | "memory": 1951.792969, 12 | "storage": 275, 13 | "mtdMemoryCost": 290.43164, 14 | "mtdStorageCost": 0.467481 15 | }, 16 | { 17 | "name": "tango", 18 | "podsCount": 6, 19 | "mtdCPU": 223.35569, 20 | "mtdMemory": 589.78287, 21 | "cpu": 14.55, 22 | "memory": 38.406295, 23 | "mtdCPUCost": 5.360537, 24 | "mtdMemoryCost": 5.897829, 25 | "mtdCost": 11.258366 26 | }, 27 | { 28 | "name": "symphony", 29 | "podsCount": 3, 30 | "mtdCPU": 206.529147, 31 | "mtdMemory": 812.197643, 32 | "mtdStorage": 49.76606, 33 | "cpu": 12.45, 34 | "memory": 48.960938, 35 | "storage": 3, 36 | "mtdCPUCost": 4.9567, 37 | "mtdMemoryCost": 8.121976, 38 | "mtdStorageCost": 0.006912, 39 | "mtdCost": 13.085588 40 | }, 41 | { 42 | "name": "ops-all", 43 | "podsCount": 19, 44 | "mtdCPU": 1336.188258, 45 | "mtdMemory": 6132.373898, 46 | "mtdStorage": 289910.168038, 47 | "cpu": 88.75, 48 | "memory": 399.859913, 49 | "storage": 24380, 50 | "mtdCPUCost": 32.068518, 51 | "mtdMemoryCost": 61.323739, 52 | "mtdStorageCost": 40.265299, 53 | "mtdCost": 133.657556 54 | } 55 | ] -------------------------------------------------------------------------------- /ui/src/app/modules/login/components/login.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/client/clientset.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package client 19 | 20 | import ( 21 | log "github.com/Sirupsen/logrus" 22 | 23 | "github.com/vmware/purser/pkg/utils" 24 | 25 | apiextcs "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 26 | "k8s.io/client-go/rest" 27 | ) 28 | 29 | // GetAPIExtensionClient returns a client for the cluster and it's config. 30 | func GetAPIExtensionClient(kubeconfigPath string) (*apiextcs.Clientset, *rest.Config) { 31 | config, err := utils.GetKubeconfig(kubeconfigPath) 32 | if err != nil { 33 | log.Fatalf("failed to fetch kubeconfig %v", err) 34 | } 35 | 36 | // create clientset and create our CRD, this only need to run once 37 | clientset, clientErr := apiextcs.NewForConfig(config) 38 | if clientErr != nil { 39 | log.Fatalf("failed to connect to the cluster %v", clientErr) 40 | } 41 | 42 | return clientset, config 43 | } 44 | -------------------------------------------------------------------------------- /docs/purser-deployment.md: -------------------------------------------------------------------------------- 1 | # Purser Deployment 2 | 3 | In order to deploy the Purser UI and DGraph database service, follow the below listed steps: 4 | 5 | 1. Switch the current context to point to the desired cluster. 6 | 7 | ``` bash 8 | kubectl config use-context 9 | ``` 10 | 11 | Read more about configuring and setting the `KUBECONFIG` and kubernetes context [here](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/). 12 | 13 | 2. If the cluster does not have a valid public IP, set proxy in order to expose the service externally. 14 | 15 | ``` bash 16 | kubectl proxy 17 | ``` 18 | 19 | 3. When set, you can simply deploy the Purser UI and Dgraph database service using target `make deploy-purser`. 20 | 21 | _If you wish to however, deploy the database service and the UI service separately, execute the following targets respectively._ 22 | 23 | ``` bash 24 | # deploy Dgraph database 25 | make kubectl-deploy-purser-db 26 | 27 | # deploy purser UI 28 | make kubectl-deploy-purser-ui 29 | ``` 30 | 31 | 4. Once deployed, if proxy was set the UI service can be accessed from [this url](http://127.0.0.1:8001/api/v1/namespaces/default/services/http:purser-ui:4200/proxy/home). 32 | 33 | If public IP was available for your cluster, the UI service should be accessible from path `:`. 34 | 35 | Eg. `http://:/home` 36 | 37 | 5. In order to drop the Dgraph entries from the database, delete the `Persistent Volume` corresponding to the `dgraph datadir`. -------------------------------------------------------------------------------- /pkg/controller/dgraph/models/query/label.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package query 19 | 20 | // CreateFilterFromListOfLabels will return a filter logic like 21 | // (eq(key, "k1") AND eq(value, "v1")) OR (eq(key, "k1") AND eq(value, "v1")) OR (eq(key, "k1") AND eq(value, "v1")) 22 | func CreateFilterFromListOfLabels(labels map[string][]string) string { 23 | separator := " OR " 24 | var filter string 25 | isFirst := true 26 | for key, values := range labels { 27 | for _, value := range values { 28 | if !isFirst { 29 | filter += separator 30 | } else { 31 | isFirst = false 32 | } 33 | filter += createFilterFromLabel(key, value) 34 | } 35 | } 36 | return filter 37 | } 38 | 39 | // createFilterFromLabel takes key: k1, value: v1 and returns (eq(key, "k1") AND eq(value, "v1")) 40 | func createFilterFromLabel(key, value string) string { 41 | return `(eq(key, "` + key + `") AND eq(value, "` + value + `"))` 42 | } 43 | -------------------------------------------------------------------------------- /cluster/helm/chart/purser/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ui.ingress.enabled }} 3 | {{- range $host := .Values.ui.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ui.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.ui.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "purser.fullname" . }}-ui) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.ui.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "purser.fullname" . }}-ui' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "purser.fullname" . }}-ui -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.ui.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "purser.name" . }}-ui,app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | echo "Visit http://127.0.0.1:8080 to use your application" 20 | kubectl port-forward $POD_NAME 8080:80 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /cluster/helm/chart/purser/templates/purser-controller-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "purser.fullname" . }} 5 | labels: 6 | app.kubernetes.io/name: {{ include "purser.name" . }} 7 | helm.sh/chart: {{ include "purser.chart" . }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | app.kubernetes.io/managed-by: {{ .Release.Service }} 10 | rules: 11 | - apiGroups: ["apiextensions.k8s.io"] 12 | resources: ["customresourcedefinitions"] 13 | verbs: ["get", "watch", "list", "update", "create", "delete"] 14 | - apiGroups: ["vmware.purser.com"] 15 | resources: ["groups", "subscribers"] 16 | verbs: ["get", "watch", "list", "update", "create", "delete"] 17 | - apiGroups: ["*"] 18 | resources: ["*"] 19 | verbs: ["get", "watch", "list"] 20 | {{- if .Values.controller.interaction }} 21 | - apiGroups: ["*"] 22 | resources: ["pods/exec"] 23 | verbs: ["create"] 24 | {{- end }} 25 | --- 26 | # ClusterRoleBinding 27 | apiVersion: rbac.authorization.k8s.io/v1beta1 28 | kind: ClusterRoleBinding 29 | metadata: 30 | name: {{ include "purser.fullname" . }} 31 | labels: 32 | app.kubernetes.io/name: {{ include "purser.name" . }} 33 | helm.sh/chart: {{ include "purser.chart" . }} 34 | app.kubernetes.io/instance: {{ .Release.Name }} 35 | app.kubernetes.io/managed-by: {{ .Release.Service }} 36 | roleRef: 37 | apiGroup: rbac.authorization.k8s.io 38 | kind: ClusterRole 39 | name: {{ include "purser.fullname" . }} 40 | subjects: 41 | - kind: ServiceAccount 42 | name: {{ include "purser.fullname" . }} 43 | namespace: {{ .Release.Namespace }} -------------------------------------------------------------------------------- /pkg/controller/dgraph/models/pod_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package models 19 | 20 | import ( 21 | "fmt" 22 | "testing" 23 | 24 | "github.com/vmware/purser/pkg/controller/dgraph" 25 | ) 26 | 27 | // TestStorePodsInteraction ... 28 | func TestStorePodsInteraction(t *testing.T) { 29 | fmt.Println("Hello World") 30 | err := dgraph.Open("127.0.0.1:9080") 31 | if err != nil { 32 | fmt.Println("Error while opening connection to Dgraph ", err) 33 | } 34 | 35 | err = dgraph.CreateSchema() 36 | if err != nil { 37 | fmt.Println("Error while creating schema ", err) 38 | } 39 | 40 | sourcePod := "weave:weave-scope-app-6d6b76b846-z92wk" 41 | destinationPods := []string{"fiaasco:ccs-billing-deployment-1-1-92-75dc8749f4-gld6q", "weave:weave-scope-agent-lbfpj"} 42 | interactionCounts := []float64{2.0} 43 | 44 | err = StorePodsInteraction(sourcePod, destinationPods, interactionCounts) 45 | if err != nil { 46 | fmt.Println("Error while building interation graph ", err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/controller/utils/timeUtils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package utils 19 | 20 | import "time" 21 | 22 | // GetCurrentMonthStartTime returns month start time as k8s apimachinery Time object 23 | func GetCurrentMonthStartTime() time.Time { 24 | now := time.Now() 25 | monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.Local) 26 | return monthStart 27 | } 28 | 29 | // ConverTimeToRFC3339 returns query time in RFC3339 format 30 | func ConverTimeToRFC3339(queryTime time.Time) string { 31 | return queryTime.Format(time.RFC3339) 32 | } 33 | 34 | // GetSecondsSince returns number of seconds since query time 35 | func GetSecondsSince(queryTime time.Time) float64 { 36 | return time.Since(queryTime).Seconds() 37 | } 38 | 39 | // GetHoursRemainingInCurrentMonth returns number of hours remaining in the month 40 | func GetHoursRemainingInCurrentMonth() float64 { 41 | now := time.Now() 42 | monthEnd := time.Date(now.Year(), now.Month(), 30, 23, 59, 0, 0, time.Local) 43 | return -time.Since(monthEnd).Hours() 44 | } 45 | -------------------------------------------------------------------------------- /pkg/controller/utils/jsonutils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "encoding/json" 22 | "net/http" 23 | 24 | log "github.com/Sirupsen/logrus" 25 | ) 26 | 27 | // JSONMarshal marshal object and returns in byte. If there is an error then it return nil. 28 | func JSONMarshal(obj interface{}) []byte { 29 | bytes, err := json.Marshal(obj) 30 | if err != nil { 31 | log.Error(err) 32 | } 33 | return bytes 34 | } 35 | 36 | // GetJSONResponse retrieves json response and converts it to target object. 37 | // Returns error if any failure is encountered. 38 | func GetJSONResponse(client *http.Client, url string, target interface{}) error { 39 | resp, err := client.Get(url) 40 | if err != nil { 41 | return err 42 | } 43 | defer closeResponse(resp) 44 | 45 | return json.NewDecoder(resp.Body).Decode(target) 46 | } 47 | 48 | func closeResponse(resp *http.Response) { 49 | err := resp.Body.Close() 50 | if err != nil { 51 | log.Errorf("unable to close response body. Reason: %v", err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/apis/groups/v1/deepcopy.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package v1 19 | 20 | import "k8s.io/apimachinery/pkg/runtime" 21 | 22 | // DeepCopyInto copies all properties of this object into another object of the 23 | // same type that is provided as a pointer. 24 | func (in *Group) DeepCopyInto(out *Group) { 25 | out.TypeMeta = in.TypeMeta 26 | out.ObjectMeta = in.ObjectMeta 27 | out.Spec = in.Spec 28 | out.Status = in.Status 29 | } 30 | 31 | // DeepCopyObject returns a generically typed copy of an object 32 | func (in *Group) DeepCopyObject() runtime.Object { 33 | out := Group{} 34 | in.DeepCopyInto(&out) 35 | return &out 36 | } 37 | 38 | // DeepCopyObject returns a generically typed copy of an object 39 | func (in *GroupList) DeepCopyObject() runtime.Object { 40 | out := GroupList{} 41 | out.TypeMeta = in.TypeMeta 42 | out.ListMeta = in.ListMeta 43 | 44 | if in.Items != nil { 45 | out.Items = make([]*Group, len(in.Items)) 46 | for i := range in.Items { 47 | in.Items[i].DeepCopyInto(out.Items[i]) 48 | } 49 | } 50 | return &out 51 | } 52 | -------------------------------------------------------------------------------- /test/controller/buffering/ring_buffer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package buffering_test 19 | 20 | import ( 21 | "sync" 22 | "testing" 23 | 24 | "github.com/vmware/purser/pkg/controller/buffering" 25 | "github.com/vmware/purser/test/utils" 26 | ) 27 | 28 | func TestPut(t *testing.T) { 29 | // use Put to add one more, return from Put should be True 30 | r := &buffering.RingBuffer{Size: 2, Mutex: &sync.Mutex{}} 31 | 32 | testValue1 := 1 33 | ret1 := r.Put(testValue1) 34 | utils.Assert(t, ret1, "inserting into not full buffer") 35 | 36 | testValue2 := 38 37 | ret2 := r.Put(testValue2) 38 | utils.Assert(t, !ret2, "inserting into full buffer") 39 | } 40 | 41 | func TestGet(t *testing.T) { 42 | // use Put to add one more, return from Put should be True 43 | r := &buffering.RingBuffer{Size: 2, Mutex: &sync.Mutex{}} 44 | 45 | ret1 := r.Get() 46 | utils.Assert(t, ret1 == nil, "get elements of empty buffer") 47 | 48 | testValue := 1 49 | r.Put(testValue) 50 | ret2 := r.Get() 51 | utils.Assert(t, (*ret2).(int) == testValue, "get elements from non empty buffer") 52 | } 53 | -------------------------------------------------------------------------------- /pkg/apis/subscriber/v1/deepcopy.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package v1 19 | 20 | import "k8s.io/apimachinery/pkg/runtime" 21 | 22 | // DeepCopyInto copies all properties of this object into another object of the 23 | // same type that is provided as a pointer. 24 | func (in *Subscriber) DeepCopyInto(out *Subscriber) { 25 | out.TypeMeta = in.TypeMeta 26 | out.ObjectMeta = in.ObjectMeta 27 | out.Spec = in.Spec 28 | out.Status = in.Status 29 | } 30 | 31 | // DeepCopyObject returns a generically typed copy of an object 32 | func (in *Subscriber) DeepCopyObject() runtime.Object { 33 | out := Subscriber{} 34 | in.DeepCopyInto(&out) 35 | return &out 36 | } 37 | 38 | // DeepCopyObject returns a generically typed copy of an object 39 | func (in *SubscriberList) DeepCopyObject() runtime.Object { 40 | out := SubscriberList{} 41 | out.TypeMeta = in.TypeMeta 42 | out.ListMeta = in.ListMeta 43 | 44 | if in.Items != nil { 45 | out.Items = make([]Subscriber, len(in.Items)) 46 | for i := range in.Items { 47 | in.Items[i].DeepCopyInto(&out.Items[i]) 48 | } 49 | } 50 | return &out 51 | } 52 | -------------------------------------------------------------------------------- /pkg/controller/dgraph/models/query/constants_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package query 19 | 20 | const ( 21 | testSecondsSinceMonthStart = "1.45" 22 | testPodUIDList = "0x3e283, 0x3e288" 23 | testPodName = "pod-purser-dgraph-0" 24 | testDaemonsetName = "daemonset-purser" 25 | testResourceName = "resource-purser" 26 | testPodUID = "0x3e283" 27 | testPodXID = "purser:pod-purser-dgraph-0" 28 | 29 | testHierarchy = "hierarchy" 30 | testMetrics = "metrics" 31 | testRetrieveAllGroups = "retrieveAllGroups" 32 | testRetrieveGroupMetrics = "retrieveGroupMetrics" 33 | testRetrieveSubscribers = "retrieveSubscribers" 34 | testLabelFilterPods = "labelFilterPods" 35 | testAlivePods = "alivePods" 36 | testPodInteractions = "podInteractions" 37 | testPodPrices = "podPrices" 38 | testCapacity = "capacityAllocation" 39 | testWrongQuery = "wrongQuery" 40 | testCPUPrice = 0.24 41 | testMemoryPrice = 0.1 42 | ) 43 | -------------------------------------------------------------------------------- /pkg/controller/dgraph/models/query/label_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package query 19 | 20 | import ( 21 | "testing" 22 | 23 | "github.com/vmware/purser/test/utils" 24 | ) 25 | 26 | // TestCreateFilterForLabel ... 27 | func TestCreateFilterFromLabel(t *testing.T) { 28 | got := createFilterFromLabel("k1", "v1") 29 | expected := `(eq(key, "k1") AND eq(value, "v1"))` 30 | utils.Equals(t, expected, got) 31 | } 32 | 33 | // TestCreateFilterFromListOfLabels ... 34 | func TestCreateFilterFromListOfLabels(t *testing.T) { 35 | labels := make(map[string][]string) 36 | labels["k1"] = []string{"v1"} 37 | got := CreateFilterFromListOfLabels(labels) 38 | expected := `(eq(key, "k1") AND eq(value, "v1"))` 39 | utils.Equals(t, expected, got) 40 | 41 | labels["k2"] = []string{"v2"} 42 | got2 := CreateFilterFromListOfLabels(labels) 43 | expected1 := `(eq(key, "k2") AND eq(value, "v2")) OR (eq(key, "k1") AND eq(value, "v1"))` 44 | expected2 := `(eq(key, "k1") AND eq(value, "v1")) OR (eq(key, "k2") AND eq(value, "v2"))` 45 | utils.Assert(t, (got2 == expected1) || (got2 == expected2), "label filter didn't match") 46 | } 47 | -------------------------------------------------------------------------------- /pkg/pricing/cloud.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pricing 19 | 20 | import ( 21 | "github.com/Sirupsen/logrus" 22 | "github.com/vmware/purser/pkg/controller/dgraph/models" 23 | "github.com/vmware/purser/pkg/pricing/aws" 24 | "k8s.io/client-go/kubernetes" 25 | ) 26 | 27 | // Cloud structure used for pricing 28 | type Cloud struct { 29 | CloudProvider string 30 | Region string 31 | Kubeclient *kubernetes.Clientset 32 | } 33 | 34 | // GetClusterProviderAndRegion returns cluster provider(ex: aws) and region(ex: us-east-1) 35 | func GetClusterProviderAndRegion() (string, string) { 36 | // TODO: https://github.com/vmware/purser/issues/143 37 | cloudProvider := models.AWS 38 | region := "us-east-1" 39 | logrus.Infof("CloudProvider: %s, Region: %s", cloudProvider, region) 40 | return cloudProvider, region 41 | } 42 | 43 | // PopulateRateCard given a cloud (cloudProvider and region) it populates corresponding rate card in dgraph 44 | func (c *Cloud) PopulateRateCard() { 45 | switch c.CloudProvider { 46 | case models.AWS: 47 | rateCard := aws.GetRateCardForAWS(c.Region) 48 | models.StoreRateCard(rateCard) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture of Purser 2 | 3 | The following diagram represents the architecture of Purser. 4 | 5 | ![Architecture](/docs/img/architecture.png) 6 | 7 | The following are the main componenets installed in Kubernetes for Purser. 8 | 9 | 1. **Kubernetes API Server** 10 | 11 | All the Purser `kubectl` commands hit the API server extension. These APIs understand the input command, compute and return the required output. 12 | 13 | 2. **Custom Controller** 14 | 15 | The custom controller watches for changes in state of pods, nodes, persistent volumes, etc. and update the inventory in CRDs. 16 | 17 | 3. **Custom Resource Definitions(CRDs)** 18 | 19 | Custom Resource Definitions are like any other resource(Pod, Node, etc.) and store the config data like `Group Definitions` and inventory. 20 | 21 | 4. **Metric Store** 22 | 23 | Metric store is used to store the utilization, allocation metrics of inventory and also calculated costs. 24 | 25 | 5. **CRON Job** 26 | 27 | CRON Job collects the stats of inventory and calculates the cost periodically and stores in Metric Store. 28 | 29 | ## Work Flow 30 | 31 | 1. Purser installation steps create Custom Controller, CRON Job and CRDs in Kubernetes. 32 | 33 | 2. Once installed the custom controller collects all the inventory(pods, nodes, pv, etc.) and stores in CRDs, later it watches for any changes in inventory and stores the changes in CRDs. 34 | 35 | 3. CRON Job kicks in periodically and collect the stats and stores the stats in metric store. CRON Job also calculates the Costs in the same cycle and stores them in the metric store. 36 | 37 | 4. Any `kubectl` command invocations are received by Kubernetes API server extension. APIs then process the required output based on the configurations(for groups), inventory, costs metrics and returns to the user. 38 | -------------------------------------------------------------------------------- /pkg/controller/controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package controller 19 | 20 | import ( 21 | "os" 22 | "os/signal" 23 | "syscall" 24 | "testing" 25 | 26 | log "github.com/Sirupsen/logrus" 27 | "github.com/vmware/purser/pkg/client" 28 | 29 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | 31 | subscriber_v1 "github.com/vmware/purser/pkg/client/clientset/typed/subscriber/v1" 32 | ) 33 | 34 | // TestCrdFlow executes the CRD flow. 35 | func TestCrdFlow(t *testing.T) { 36 | clientset, clusterConfig := client.GetAPIExtensionClient("") 37 | subcrdclient := subscriber_v1.NewSubscriberClient(clientset, clusterConfig) 38 | ListSubscriberCrdInstances(subcrdclient) 39 | 40 | sigterm := make(chan os.Signal, 1) 41 | signal.Notify(sigterm, syscall.SIGTERM) 42 | signal.Notify(sigterm, syscall.SIGINT) 43 | <-sigterm 44 | } 45 | 46 | // ListSubscriberCrdInstances fetches list of subscriber CRD instances. 47 | func ListSubscriberCrdInstances(crdclient *subscriber_v1.SubscriberClient) { 48 | items, err := crdclient.List(meta_v1.ListOptions{}) 49 | if err != nil { 50 | panic(err) 51 | } 52 | log.Printf("List:\n%v\n", items) 53 | } 54 | -------------------------------------------------------------------------------- /ui/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { RouterModule } from '@angular/router'; 6 | import { ClarityModule } from '@clr/angular'; 7 | import { GoogleChartsModule } from 'angular-google-charts'; 8 | import { CookieService } from 'ngx-cookie-service'; 9 | import { AppComponent } from './app.component'; 10 | import { ROUTING } from "./app.routing"; 11 | import { CapacityGraphModule } from './modules/capacity-graph/capacity-graph.module'; 12 | import { ChangepasswordModule } from './modules/changepassword/changepassword.module'; 13 | import { LogicalGroupModule } from './modules/logical-group/logical-group.module'; 14 | import { LoginModule } from './modules/login/login.module'; 15 | import { LogoutModule } from './modules/logout/logout.module'; 16 | import { OptionsModule } from './modules/options/options.module'; 17 | import { TopoGraphModule } from './modules/topo-graph/modules'; 18 | import { TopologyGraphModule } from './modules/topologyGraph/modules'; 19 | 20 | @NgModule({ 21 | declarations: [ 22 | AppComponent, 23 | ], 24 | imports: [ 25 | BrowserModule, 26 | ClarityModule, 27 | BrowserAnimationsModule, 28 | RouterModule, 29 | HttpClientModule, 30 | ROUTING, 31 | CapacityGraphModule, 32 | TopologyGraphModule, 33 | TopoGraphModule, 34 | LoginModule, 35 | LogoutModule, 36 | LogicalGroupModule, 37 | ChangepasswordModule, 38 | OptionsModule, 39 | GoogleChartsModule.forRoot() 40 | ], 41 | providers: [CookieService], 42 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 43 | bootstrap: [AppComponent] 44 | }) 45 | export class AppModule { } 46 | -------------------------------------------------------------------------------- /test/utils/checkUtil.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "fmt" 22 | "path/filepath" 23 | "reflect" 24 | "runtime" 25 | "testing" 26 | ) 27 | 28 | // Assert fails the test if the condition is false. 29 | func Assert(tb testing.TB, condition bool, msg string, v ...interface{}) { 30 | if !condition { 31 | _, file, line, _ := runtime.Caller(1) 32 | fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) 33 | tb.FailNow() 34 | } 35 | } 36 | 37 | // Ok fails the test if an err is not nil. 38 | func Ok(tb testing.TB, err error) { 39 | if err != nil { 40 | _, file, line, _ := runtime.Caller(1) 41 | fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) 42 | tb.FailNow() 43 | } 44 | } 45 | 46 | // Equals fails the test if exp is not equal to act. 47 | func Equals(tb testing.TB, exp, act interface{}) { 48 | if !reflect.DeepEqual(exp, act) { 49 | _, file, line, _ := runtime.Caller(1) 50 | fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) 51 | tb.FailNow() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/controller/dgraph/login.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package dgraph 19 | 20 | import ( 21 | "github.com/Sirupsen/logrus" 22 | "golang.org/x/crypto/bcrypt" 23 | ) 24 | 25 | // Login structure 26 | type Login struct { 27 | ID 28 | IsLogin bool `json:"isLogin,omitempty"` 29 | Username string `json:"username,omitempty"` 30 | Password string `json:"password,omitempty"` 31 | } 32 | 33 | // Login constants 34 | const ( 35 | DefaultUsername = "admin" 36 | DefaultPassword = "purser!123" 37 | DefaultLoginXID = "purser-login-xid" 38 | IsLogin = "isLogin" 39 | ) 40 | 41 | // StoreLogin ... 42 | func StoreLogin() { 43 | uid := GetUID(DefaultLoginXID, IsLogin) 44 | if uid == "" { 45 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(DefaultPassword), bcrypt.MinCost) 46 | if err != nil { 47 | logrus.Errorf("error while hashing login information") 48 | } 49 | login := Login{ 50 | ID: ID{Xid: DefaultLoginXID}, 51 | IsLogin: true, 52 | Username: DefaultUsername, 53 | Password: string(hashedPassword), 54 | } 55 | _, err = MutateNode(login, CREATE) 56 | if err != nil { 57 | logrus.Errorf("error while storing login information") 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-dapp", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "startdev": "ng serve --proxy-config proxy.conf.json", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular-devkit/build-angular": "~0.13.9", 16 | "@angular/animations": "^6.1.0", 17 | "@angular/common": "^6.1.0", 18 | "@angular/compiler": "^6.1.0", 19 | "@angular/core": "^6.1.0", 20 | "@angular/forms": "^6.1.0", 21 | "@angular/http": "^6.1.0", 22 | "@angular/platform-browser": "^6.1.0", 23 | "@angular/platform-browser-dynamic": "^6.1.0", 24 | "@angular/router": "^6.1.0", 25 | "@clr/angular": "^0.13.1-patch.1", 26 | "@clr/icons": "^0.13.1-patch.1", 27 | "@clr/ui": "^0.13.1-patch.1", 28 | "@types/vis": "^4.21.8", 29 | "@webcomponents/custom-elements": "^1.0.0", 30 | "angular-google-charts": "^0.1.0", 31 | "core-js": "^2.5.4", 32 | "ngx-cookie-service": "^2.1.0", 33 | "rxjs": "~6.2.0", 34 | "vis": "^4.21.0", 35 | "zone.js": "~0.8.26" 36 | }, 37 | "devDependencies": { 38 | "@angular/cli": "~6.2.1", 39 | "@angular/compiler-cli": "^6.1.0", 40 | "@angular/language-service": "^6.1.0", 41 | "@types/jasmine": "~2.8.8", 42 | "@types/jasminewd2": "~2.0.3", 43 | "@types/node": "~8.9.4", 44 | "codelyzer": "~4.3.0", 45 | "jasmine-core": "~2.99.1", 46 | "jasmine-spec-reporter": "~4.2.1", 47 | "karma": "~3.0.0", 48 | "karma-chrome-launcher": "~2.2.0", 49 | "karma-coverage-istanbul-reporter": "~2.0.1", 50 | "karma-jasmine": "~1.1.2", 51 | "karma-jasmine-html-reporter": "^0.2.2", 52 | "protractor": "~5.4.0", 53 | "ts-node": "~7.0.0", 54 | "tslint": "~5.11.0", 55 | "typescript": "~2.9.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/apis/groups/v1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package v1 19 | 20 | import ( 21 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | ) 25 | 26 | // SchemeBuilder parameters 27 | var ( 28 | SchemeBuilder = runtime.NewSchemeBuilder(AddKnownTypes) 29 | AddToScheme = SchemeBuilder.AddToScheme 30 | ) 31 | 32 | // SchemeGroupVersion is group version used to register these objects 33 | var SchemeGroupVersion = schema.GroupVersion{Group: CRDGroup, Version: CRDVersion} 34 | 35 | // Kind takes an unqualified kind and returns a Group qualified GroupKind 36 | func Kind(kind string) schema.GroupKind { 37 | return SchemeGroupVersion.WithKind(kind).GroupKind() 38 | } 39 | 40 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 41 | func Resource(resource string) schema.GroupResource { 42 | return SchemeGroupVersion.WithResource(resource).GroupResource() 43 | } 44 | 45 | // AddKnownTypes ... 46 | func AddKnownTypes(scheme *runtime.Scheme) error { 47 | scheme.AddKnownTypes(SchemeGroupVersion, 48 | &Group{}, 49 | &GroupList{}, 50 | ) 51 | meta_v1.AddToGroupVersion(scheme, SchemeGroupVersion) 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/apis/subscriber/v1/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package v1 19 | 20 | import meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | // CRD Subscriber attributes 23 | const ( 24 | SubscriberPlural string = "subscribers" 25 | SubscriberGroup string = "vmware.purser.com" 26 | SubscriberVersion string = "v1" 27 | SubscriberFullName string = SubscriberPlural + "." + SubscriberGroup 28 | ) 29 | 30 | // Subscriber information 31 | type Subscriber struct { 32 | meta_v1.TypeMeta `json:",inline"` 33 | meta_v1.ObjectMeta `json:"metadata"` 34 | Spec SubscriberSpec `json:"spec"` 35 | Status SubscriberStatus `json:"status,omitempty"` 36 | } 37 | 38 | // SubscriberSpec definition details 39 | type SubscriberSpec struct { 40 | Name string `json:"name"` 41 | Headers map[string]string `json:"headers"` 42 | URL string `json:"url"` 43 | } 44 | 45 | // SubscriberStatus definition 46 | type SubscriberStatus struct { 47 | State string `json:"state,omitempty"` 48 | Message string `json:"message,omitempty"` 49 | } 50 | 51 | // SubscriberList type 52 | type SubscriberList struct { 53 | meta_v1.TypeMeta `json:",inline"` 54 | meta_v1.ListMeta `json:"metadata"` 55 | Items []Subscriber `json:"items"` 56 | } 57 | -------------------------------------------------------------------------------- /pkg/controller/utils/unitConversions_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "testing" 22 | 23 | "github.com/vmware/purser/test/utils" 24 | "k8s.io/apimachinery/pkg/api/resource" 25 | ) 26 | 27 | func TestBytesToGB(t *testing.T) { 28 | act := BytesToGB(124235312345978) 29 | exp := 115703.15095221438 30 | utils.Equals(t, exp, act) 31 | } 32 | 33 | func TestConvertToFloat64GB(t *testing.T) { 34 | quantities := getTestQuantities() 35 | exp := [3]float64{0.011175870895385742, 0.01171875, 0.011175870895385742} 36 | for index, quantity := range quantities { 37 | act := ConvertToFloat64GB(&quantity) 38 | utils.Equals(t, exp[index], act) 39 | } 40 | } 41 | 42 | func TestConvertToFloat64CPU(t *testing.T) { 43 | quantities := getTestQuantities() 44 | exp := [3]float64{1.2e+07, 1.2582912e+07, 1.2e+07} 45 | for index, quantity := range quantities { 46 | act := ConvertToFloat64CPU(&quantity) 47 | utils.Equals(t, exp[index], act) 48 | } 49 | } 50 | 51 | func getTestQuantities() [3]resource.Quantity { 52 | var quantities [3]resource.Quantity 53 | quantities[0], _ = resource.ParseQuantity("12e6") 54 | quantities[1], _ = resource.ParseQuantity("12Mi") 55 | quantities[2], _ = resource.ParseQuantity("12M") 56 | 57 | return quantities 58 | } 59 | -------------------------------------------------------------------------------- /pkg/apis/subscriber/v1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package v1 19 | 20 | import ( 21 | meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | ) 25 | 26 | // SchemeBuilder parameters 27 | var ( 28 | SchemeBuilder = runtime.NewSchemeBuilder(AddKnownTypes) 29 | AddToScheme = SchemeBuilder.AddToScheme 30 | ) 31 | 32 | // SubscriberGroupVersion is group version used to register these objects 33 | var SubscriberGroupVersion = schema.GroupVersion{Group: SubscriberGroup, Version: SubscriberVersion} 34 | 35 | // Kind takes an unqualified kind and returns a Group qualified GroupKind 36 | func Kind(kind string) schema.GroupKind { 37 | return SubscriberGroupVersion.WithKind(kind).GroupKind() 38 | } 39 | 40 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 41 | func Resource(resource string) schema.GroupResource { 42 | return SubscriberGroupVersion.WithResource(resource).GroupResource() 43 | } 44 | 45 | // AddKnownTypes ... 46 | func AddKnownTypes(scheme *runtime.Scheme) error { 47 | scheme.AddKnownTypes(SubscriberGroupVersion, 48 | &Subscriber{}, 49 | &SubscriberList{}, 50 | ) 51 | meta_v1.AddToGroupVersion(scheme, SubscriberGroupVersion) 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /cluster/helm/chart/purser/README.md: -------------------------------------------------------------------------------- 1 | # [Purser](https://github.com/vmware/purser) 2 | 3 | Purser is an extension to Kubernetes tasked at providing an insight into cluster topology, costing, capacity allocations and resource interactions along with the provision of logical grouping of resources for Kubernetes based cloud native applications in a cloud neutral manner, with the focus on catering to a multitude of users ranging from Sys Admins, to DevOps to Developers. 4 | 5 | It comprises of three components: a controller, a plugin and a UI dashboard. 6 | 7 | The controller component deployed inside the cluster watches for K8s native and custom resources associated with the application, thereby, periodically building not just an inventory but also performing discovery by generating and storing the interactions among the resources such as containers, pods and services. 8 | 9 | The plugin component is a CLI tool interfacing with the kubectl that helps query costs, savings defined at a level of control of the application level components rather than at the infrastructure level. 10 | 11 | The UI dashboard is a robust application that renders the Purser UI for providing visual representation to the complete cluster metrics in a single pane of glass. 12 | 13 | > Taken from main [README](https://github.com/vmware/purser/blob/master/README.md) 14 | 15 | > [Plugin installation guide](https://github.com/vmware/purser/blob/master/README.md#purser-plugin-setup) 16 | 17 | ## Chart Configuration 18 | 19 | *See `values.yaml` for configuration notes. Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, 20 | 21 | ```console 22 | $ helm install --name purser \ 23 | --set database.storage=10Gi \ 24 | purser 25 | ``` 26 | 27 | Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example, 28 | 29 | ```console 30 | $ helm install --name purser -f values.yaml 31 | ``` 32 | 33 | > **Tip**: You can use the default [values.yaml](values.yaml) -------------------------------------------------------------------------------- /ui/src/app/modules/topologyGraph/components/topologyGraph.component.html: -------------------------------------------------------------------------------- 1 |

Interactions

2 |
3 | 4 | 5 | 9 | 10 |
11 | 12 | 13 |
14 |
15 |
16 | Scroll in to cluster. Press on the cluster to open up. 17 |
18 |
19 |
20 |
21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 | 40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /ui/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{messages && messages.common.appHeader}} 5 |
6 |
7 |
8 | 47 |
48 |
-------------------------------------------------------------------------------- /ui/src/app/modules/changepassword/components/changepassword.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cluster/helm/chart/purser/templates/purser-ui-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "purser.fullname" . }}-ui 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "purser.name" . }}-ui 8 | helm.sh/chart: {{ include "purser.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | spec: 12 | replicas: {{ .Values.ui.replicaCount }} 13 | selector: 14 | matchLabels: 15 | app.kubernetes.io/name: {{ include "purser.name" . }}-ui 16 | app.kubernetes.io/instance: {{ .Release.Name }} 17 | template: 18 | metadata: 19 | labels: 20 | app.kubernetes.io/name: {{ include "purser.name" . }}-ui 21 | app.kubernetes.io/instance: {{ .Release.Name }} 22 | spec: 23 | volumes: 24 | - configMap: 25 | defaultMode: 420 26 | name: {{ include "purser.fullname" . }}-ui 27 | name: nginx 28 | containers: 29 | - name: {{ .Chart.Name }} 30 | image: "{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag }}" 31 | imagePullPolicy: {{ .Values.ui.image.pullPolicy }} 32 | ports: 33 | - name: http 34 | containerPort: 4200 35 | protocol: TCP 36 | volumeMounts: 37 | - mountPath: /etc/nginx/conf.d 38 | name: nginx 39 | livenessProbe: 40 | httpGet: 41 | path: / 42 | port: http 43 | readinessProbe: 44 | httpGet: 45 | path: / 46 | port: http 47 | resources: 48 | {{- toYaml .Values.ui.resources | nindent 12 }} 49 | {{- with .Values.ui.nodeSelector }} 50 | nodeSelector: 51 | {{- toYaml . | nindent 8 }} 52 | {{- end }} 53 | {{- with .Values.ui.affinity }} 54 | affinity: 55 | {{- toYaml . | nindent 8 }} 56 | {{- end }} 57 | {{- with .Values.ui.tolerations }} 58 | tolerations: 59 | {{- toYaml . | nindent 8 }} 60 | {{- end }} 61 | -------------------------------------------------------------------------------- /pkg/controller/utils/unitConversions.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "strconv" 22 | 23 | log "github.com/Sirupsen/logrus" 24 | "k8s.io/apimachinery/pkg/api/resource" 25 | ) 26 | 27 | // BytesToGB converts from bytes(int64) to GB(float64) 28 | func BytesToGB(val int64) float64 { 29 | return float64BytesToFloat64GB(float64(val)) 30 | } 31 | 32 | // ConvertToFloat64GB quantity to float64 GB 33 | func ConvertToFloat64GB(quantity *resource.Quantity) float64 { 34 | return float64BytesToFloat64GB(resourceToFloat64(quantity)) 35 | } 36 | 37 | // ConvertToFloat64CPU quantity to float64 vCPU 38 | func ConvertToFloat64CPU(quantity *resource.Quantity) float64 { 39 | return resourceToFloat64(quantity) 40 | } 41 | 42 | // AddResourceAToResourceB ... 43 | func AddResourceAToResourceB(resA, resB *resource.Quantity) { 44 | if resA != nil { 45 | resB.Add(*resA) 46 | } 47 | } 48 | 49 | // float64BytesToFloat64GB from bytes (float64) to GB(float64) 50 | func float64BytesToFloat64GB(val float64) float64 { 51 | return val / (1024.0 * 1024.0 * 1024.0) 52 | } 53 | 54 | // resourceToFloat64 ... 55 | func resourceToFloat64(quantity *resource.Quantity) float64 { 56 | decVal := quantity.AsDec() 57 | decValueFloat, err := strconv.ParseFloat(decVal.String(), 64) 58 | if err != nil { 59 | log.Errorf("error while converting into string: (%s) to float\n", decVal.String()) 60 | } 61 | return decValueFloat // 0 if not isSuccess 62 | } 63 | -------------------------------------------------------------------------------- /cluster/minimal/purser-controller-setup.yaml: -------------------------------------------------------------------------------- 1 | # Service account 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: purser-service-account 6 | --- 7 | # RBAC 8 | apiVersion: rbac.authorization.k8s.io/v1beta1 9 | kind: ClusterRole 10 | metadata: 11 | name: purser-permissions 12 | rules: 13 | - apiGroups: ["apiextensions.k8s.io"] 14 | resources: ["customresourcedefinitions"] 15 | verbs: ["get", "watch", "list", "update", "create", "delete"] 16 | - apiGroups: ["vmware.purser.com"] 17 | resources: ["groups", "subscribers"] 18 | verbs: ["get", "watch", "list", "update", "create", "delete"] 19 | - apiGroups: ["*"] 20 | resources: ["*"] 21 | verbs: ["get", "watch", "list"] 22 | # Uncomment next three lines to enable interactions feature. 23 | # - apiGroups: ["*"] 24 | # resources: ["pods/exec"] 25 | # verbs: ["create"] 26 | --- 27 | # ClusterRoleBinding 28 | apiVersion: rbac.authorization.k8s.io/v1beta1 29 | kind: ClusterRoleBinding 30 | metadata: 31 | name: purser-cluster-role 32 | roleRef: 33 | apiGroup: rbac.authorization.k8s.io 34 | kind: ClusterRole 35 | name: purser-permissions 36 | subjects: 37 | - kind: ServiceAccount 38 | name: purser-service-account 39 | namespace: purser 40 | --- 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: purser 45 | spec: 46 | selector: 47 | app: purser 48 | ports: 49 | - protocol: TCP 50 | port: 3030 51 | targetPort: http 52 | --- 53 | apiVersion: apps/v1 54 | kind: Deployment 55 | metadata: 56 | name: purser 57 | spec: 58 | selector: 59 | matchLabels: 60 | app: purser 61 | replicas: 1 62 | template: 63 | metadata: 64 | labels: 65 | app: purser 66 | spec: 67 | serviceAccountName: purser-service-account 68 | containers: 69 | - name: purser-controller 70 | image: kreddyj/purser:controller-1.0.2 71 | imagePullPolicy: Always 72 | ports: 73 | - name: http 74 | containerPort: 3030 75 | command: ["/controller"] 76 | args: ["--log=info", "--interactions=disable", "--dgraphURL=purser-db", "--dgraphPort=9080"] 77 | -------------------------------------------------------------------------------- /pkg/controller/dgraph/models/query/subscriber_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package query 19 | 20 | import ( 21 | "fmt" 22 | "testing" 23 | 24 | "github.com/vmware/purser/pkg/controller/dgraph/models" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func mockDgraphForSubscriberQueries(queryType string) { 30 | executeQuery = func(query string, root interface{}) error { 31 | dummySubscriberList, ok := root.(*subscriberRoot) 32 | if !ok { 33 | return fmt.Errorf("wrong root received") 34 | } 35 | 36 | if queryType == testRetrieveSubscribers { 37 | dummySubscriber := models.SubscriberCRD{ 38 | Name: "subscriber-purser", 39 | Spec: models.SubscriberSpec{ 40 | URL: "http://purser.com", 41 | }, 42 | } 43 | dummySubscriberList.Subscribers = []models.SubscriberCRD{dummySubscriber} 44 | return nil 45 | } 46 | 47 | return fmt.Errorf("no data found") 48 | } 49 | } 50 | 51 | // TestRetrieveSubscribersWithDgraphError ... 52 | func TestRetrieveSubscribersWithDgraphError(t *testing.T) { 53 | mockDgraphForSubscriberQueries(testWrongQuery) 54 | _, err := RetrieveSubscribers() 55 | assert.Error(t, err) 56 | } 57 | 58 | // TestRetrieveSubscribers ... 59 | func TestRetrieveSubscribers(t *testing.T) { 60 | mockDgraphForSubscriberQueries(testRetrieveSubscribers) 61 | got, err := RetrieveSubscribers() 62 | expected := []models.SubscriberCRD{{ 63 | Name: "subscriber-purser", 64 | Spec: models.SubscriberSpec{ 65 | URL: "http://purser.com", 66 | }, 67 | }} 68 | assert.Equal(t, expected, got) 69 | assert.NoError(t, err) 70 | } 71 | -------------------------------------------------------------------------------- /ui/src/app/modules/topo-graph/components/topo-graph.component.scss: -------------------------------------------------------------------------------- 1 | .graphCardBlock{ 2 | ::ng-deep .googleChart{ 3 | display: block; 4 | margin: 0 auto; 5 | } 6 | ::ng-deep .customNode{ 7 | border: 1px solid #2B7CE9; 8 | border-radius: 5%; 9 | background-color: whitesmoke; 10 | font-size: 14px; 11 | font-weight: 800; 12 | } 13 | .headerBlock{ 14 | display: flex; 15 | .headerText{ 16 | font-size: 18px; 17 | } 18 | .card-title{ 19 | flex: 1; 20 | } 21 | .filterDiv{ 22 | label{ 23 | padding-right: 10px; 24 | } 25 | padding-right: 60px; 26 | } 27 | .toggleDiv{ 28 | .viewSwitchLeftLabel{ 29 | padding-right: 5px; 30 | } 31 | } 32 | } 33 | .card-text{ 34 | text-align: center; 35 | overflow-x: auto; 36 | .legendDiv{ 37 | display: flex; 38 | .legend{ 39 | display: flex; 40 | align-items: center; 41 | padding: 5px; 42 | .fakeLegend{ 43 | width: 10px; 44 | height: 10px; 45 | border-radius: 50%; 46 | } 47 | .fakeLegendText{ 48 | padding-left: 5px; 49 | } 50 | } 51 | } 52 | } 53 | ::ng-deep .namespace{ 54 | color: red; 55 | } 56 | ::ng-deep .service{ 57 | color: yellow; 58 | } 59 | ::ng-deep .pod{ 60 | color: green; 61 | } 62 | ::ng-deep .container{ 63 | color: blue 64 | } 65 | ::ng-deep .process{ 66 | color: orange; 67 | } 68 | ::ng-deep .cluster{ 69 | color: orangered; 70 | } 71 | ::ng-deep .deployment{ 72 | color: purple; 73 | } 74 | ::ng-deep .replicaset{ 75 | color: palevioletred; 76 | } 77 | ::ng-deep .node{ 78 | color: royalblue; 79 | } 80 | ::ng-deep .daemonset{ 81 | color: brown; 82 | } 83 | ::ng-deep .job{ 84 | color: black; 85 | } 86 | ::ng-deep .statefulset{ 87 | color: goldenrod; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /pkg/controller/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package controller 19 | 20 | import ( 21 | groups_v1 "github.com/vmware/purser/pkg/client/clientset/typed/groups/v1" 22 | subscriber_v1 "github.com/vmware/purser/pkg/client/clientset/typed/subscriber/v1" 23 | "github.com/vmware/purser/pkg/controller/buffering" 24 | "k8s.io/client-go/kubernetes" 25 | "k8s.io/client-go/rest" 26 | ) 27 | 28 | // These are the event types supported for controllers 29 | const ( 30 | Create = "create" 31 | Delete = "delete" 32 | Update = "update" 33 | ) 34 | 35 | // Resource contains resource configuration 36 | type Resource struct { 37 | Pod bool `json:"po"` 38 | Node bool `json:"node"` 39 | PersistentVolume bool `json:"pv"` 40 | PersistentVolumeClaim bool `json:"pvc"` 41 | Service bool `json:"service"` 42 | ReplicaSet bool `json:"replicaset"` 43 | StatefulSet bool `json:"statefulset"` 44 | Deployment bool `json:"deployment"` 45 | Job bool `json:"job"` 46 | DaemonSet bool `json:"daemonset"` 47 | Namespace bool `json:"namespace"` 48 | Group bool `json:"groups.vmware.purser.com"` 49 | Subscriber bool `json:"subscribers.vmware.purser.com"` 50 | } 51 | 52 | // Config contains config objects 53 | type Config struct { 54 | KubeConfig *rest.Config 55 | Resource Resource `json:"resource"` 56 | RingBuffer *buffering.RingBuffer 57 | Groupcrdclient *groups_v1.GroupClient 58 | Subscriberclient *subscriber_v1.SubscriberClient 59 | Kubeclient *kubernetes.Clientset 60 | } 61 | -------------------------------------------------------------------------------- /cluster/purser-controller-setup.yaml: -------------------------------------------------------------------------------- 1 | # Service account 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: purser-service-account 6 | --- 7 | # RBAC 8 | apiVersion: rbac.authorization.k8s.io/v1beta1 9 | kind: ClusterRole 10 | metadata: 11 | name: purser-permissions 12 | rules: 13 | - apiGroups: ["apiextensions.k8s.io"] 14 | resources: ["customresourcedefinitions"] 15 | verbs: ["get", "watch", "list", "update", "create", "delete"] 16 | - apiGroups: ["vmware.purser.com"] 17 | resources: ["groups", "subscribers"] 18 | verbs: ["get", "watch", "list", "update", "create", "delete"] 19 | - apiGroups: ["*"] 20 | resources: ["*"] 21 | verbs: ["get", "watch", "list"] 22 | # Uncomment next three lines to enable interactions feature. 23 | # - apiGroups: ["*"] 24 | # resources: ["pods/exec"] 25 | # verbs: ["create"] 26 | --- 27 | # ClusterRoleBinding 28 | apiVersion: rbac.authorization.k8s.io/v1beta1 29 | kind: ClusterRoleBinding 30 | metadata: 31 | name: purser-cluster-role 32 | roleRef: 33 | apiGroup: rbac.authorization.k8s.io 34 | kind: ClusterRole 35 | name: purser-permissions 36 | subjects: 37 | - kind: ServiceAccount 38 | name: purser-service-account 39 | namespace: purser 40 | --- 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: purser 45 | spec: 46 | selector: 47 | app: purser 48 | ports: 49 | - protocol: TCP 50 | port: 3030 51 | targetPort: http 52 | --- 53 | apiVersion: apps/v1 54 | kind: Deployment 55 | metadata: 56 | name: purser 57 | spec: 58 | selector: 59 | matchLabels: 60 | app: purser 61 | replicas: 1 62 | template: 63 | metadata: 64 | labels: 65 | app: purser 66 | spec: 67 | serviceAccountName: purser-service-account 68 | containers: 69 | - name: purser-controller 70 | image: kreddyj/purser:controller-1.0.2 71 | imagePullPolicy: Always 72 | resources: 73 | limits: 74 | memory: 1000Mi 75 | cpu: 300m 76 | requests: 77 | memory: 1000Mi 78 | cpu: 300m 79 | ports: 80 | - name: http 81 | containerPort: 3030 82 | command: ["/controller"] 83 | args: ["--log=info", "--interactions=disable", "--dgraphURL=purser-db", "--dgraphPort=9080"] 84 | -------------------------------------------------------------------------------- /cmd/controller/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package config 19 | 20 | import ( 21 | "sync" 22 | 23 | log "github.com/Sirupsen/logrus" 24 | 25 | "github.com/vmware/purser/pkg/client" 26 | group_client "github.com/vmware/purser/pkg/client/clientset/typed/groups/v1" 27 | subscriber_client "github.com/vmware/purser/pkg/client/clientset/typed/subscriber/v1" 28 | "github.com/vmware/purser/pkg/controller" 29 | "github.com/vmware/purser/pkg/controller/buffering" 30 | "github.com/vmware/purser/pkg/utils" 31 | ) 32 | 33 | // Setup initialzes the controller configuration 34 | func Setup(conf *controller.Config, kubeconfig string) { 35 | var err error 36 | *conf = controller.Config{} 37 | conf.KubeConfig, err = utils.GetKubeconfig(kubeconfig) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | conf.Kubeclient = utils.GetKubeclient(conf.KubeConfig) 42 | conf.Resource = controller.Resource{ 43 | Pod: true, 44 | Node: true, 45 | PersistentVolume: true, 46 | PersistentVolumeClaim: true, 47 | ReplicaSet: true, 48 | Deployment: true, 49 | StatefulSet: true, 50 | DaemonSet: true, 51 | Job: true, 52 | Service: true, 53 | Namespace: true, 54 | Group: true, 55 | Subscriber: true, 56 | } 57 | conf.RingBuffer = &buffering.RingBuffer{Size: buffering.BufferSize, Mutex: &sync.Mutex{}} 58 | clientset, clusterConfig := client.GetAPIExtensionClient(kubeconfig) 59 | conf.Groupcrdclient = group_client.NewGroupClient(clientset, clusterConfig) 60 | conf.Subscriberclient = subscriber_client.NewSubscriberClient(clientset, clusterConfig) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/controller/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package metrics 19 | 20 | import ( 21 | log "github.com/Sirupsen/logrus" 22 | api_v1 "k8s.io/api/core/v1" 23 | "k8s.io/apimachinery/pkg/api/resource" 24 | ) 25 | 26 | // Metrics types 27 | type Metrics struct { 28 | CPULimit *resource.Quantity 29 | MemoryLimit *resource.Quantity 30 | CPURequest *resource.Quantity 31 | MemoryRequest *resource.Quantity 32 | } 33 | 34 | // CalculatePodStatsFromContainers returns the cumulative metrics from the containers. 35 | func CalculatePodStatsFromContainers(pod *api_v1.Pod) *Metrics { 36 | cpuLimit := &resource.Quantity{} 37 | memoryLimit := &resource.Quantity{} 38 | cpuRequest := &resource.Quantity{} 39 | memoryRequest := &resource.Quantity{} 40 | for _, c := range pod.Spec.Containers { 41 | limits := c.Resources.Limits 42 | if limits != nil { 43 | cpuLimit.Add(*limits.Cpu()) 44 | memoryLimit.Add(*limits.Memory()) 45 | } 46 | 47 | requests := c.Resources.Requests 48 | if requests != nil { 49 | cpuRequest.Add(*requests.Cpu()) 50 | memoryRequest.Add(*requests.Memory()) 51 | } 52 | } 53 | return &Metrics{ 54 | CPULimit: cpuLimit, 55 | MemoryLimit: memoryLimit, 56 | CPURequest: cpuRequest, 57 | MemoryRequest: memoryRequest, 58 | } 59 | } 60 | 61 | // PrintPodStats displays the pod stats. 62 | func PrintPodStats(pod *api_v1.Pod, metrics *Metrics) { 63 | log.Printf("Pod:\t%s\n", pod.Name) 64 | log.Printf("\tCPU Limit = %s\n", metrics.CPULimit.String()) 65 | log.Printf("\tMemory Limit = %s\n", metrics.MemoryLimit.String()) 66 | log.Printf("\tCPU Request = %s\n", metrics.CPURequest.String()) 67 | log.Printf("\tMemory Request = %s\n", metrics.MemoryRequest.String()) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/controller/dgraph/models/label.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package models 19 | 20 | import ( 21 | "github.com/Sirupsen/logrus" 22 | "github.com/vmware/purser/pkg/controller/dgraph" 23 | ) 24 | 25 | // Dgraph Model Constants 26 | const ( 27 | Islabel = "isLabel" 28 | ) 29 | 30 | // Label structure for Key:Value 31 | type Label struct { 32 | dgraph.ID 33 | IsLabel bool `json:"isLabel,omitempty"` 34 | Key string `json:"key,omitempty"` 35 | Value string `json:"value,omitempty"` 36 | } 37 | 38 | // GetLabel if label is not in dgraph it creates and returns Label object 39 | func GetLabel(key, value string) *Label { 40 | xid := getXIDOfLabel(key, value) 41 | uid := CreateOrGetLabelByID(key, value) 42 | return &Label{ 43 | ID: dgraph.ID{Xid: xid, UID: uid}, 44 | } 45 | } 46 | 47 | // CreateOrGetLabelByID if label is not in dgraph it creates and returns uid of label 48 | func CreateOrGetLabelByID(key, value string) string { 49 | xid := getXIDOfLabel(key, value) 50 | uid := dgraph.GetUID(xid, Islabel) 51 | if uid == "" { 52 | // create new label and get its uid 53 | uid = createLabelObject(key, value) 54 | } 55 | return uid 56 | } 57 | 58 | func getXIDOfLabel(key, value string) string { 59 | return "label-" + key + "-" + value 60 | } 61 | 62 | func createLabelObject(key, value string) string { 63 | xid := getXIDOfLabel(key, value) 64 | newLabel := Label{ 65 | ID: dgraph.ID{Xid: xid}, 66 | IsLabel: true, 67 | Key: key, 68 | Value: value, 69 | } 70 | assigned, err := dgraph.MutateNode(newLabel, dgraph.CREATE) 71 | if err != nil { 72 | logrus.Fatal(err) 73 | return "" 74 | } 75 | logrus.Debugf("created label in dgraph key: (%v), value: (%v)", newLabel.Key, newLabel.Value) 76 | return assigned.Uids["blank-0"] 77 | } 78 | -------------------------------------------------------------------------------- /cluster/helm/chart/purser/templates/purser-controller-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "purser.fullname" . }}-controller 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "purser.name" . }}-controller 8 | helm.sh/chart: {{ include "purser.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | spec: 12 | replicas: {{ .Values.controller.replicaCount }} 13 | selector: 14 | matchLabels: 15 | app.kubernetes.io/name: {{ include "purser.name" . }}-controller 16 | app.kubernetes.io/instance: {{ .Release.Name }} 17 | template: 18 | metadata: 19 | labels: 20 | app.kubernetes.io/name: {{ include "purser.name" . }}-controller 21 | app.kubernetes.io/instance: {{ .Release.Name }} 22 | spec: 23 | serviceAccountName: {{ include "purser.fullname" . }} 24 | containers: 25 | - name: {{ .Chart.Name }} 26 | image: "{{ .Values.controller.image.repository }}:{{ .Values.controller.image.tag }}" 27 | imagePullPolicy: {{ .Values.controller.image.pullPolicy }} 28 | command: 29 | - "/controller" 30 | args: 31 | - "--cookieKey=purser-super-secret-key" 32 | - "--cookieName=purser-session-token" 33 | - "--log=info" 34 | {{- if .Values.controller.interactions }} 35 | - "--interactions=enable" 36 | {{- else }} 37 | - "--interactions=disable" 38 | {{- end }} 39 | - "--dgraphURL={{ include "purser.fullname" . }}-database" 40 | - "--dgraphPort=9080" 41 | ports: 42 | - name: http 43 | containerPort: 3030 44 | protocol: TCP 45 | resources: 46 | {{- toYaml .Values.controller.resources | nindent 12 }} 47 | initContainers: 48 | - name: init-sleep 49 | image: "{{ .Values.controller.image.repository }}:{{ .Values.controller.image.tag }}" 50 | command: ["/usr/bin/bash", "-c", "sleep 60"] 51 | {{- with .Values.controller.nodeSelector }} 52 | nodeSelector: 53 | {{- toYaml . | nindent 8 }} 54 | {{- end }} 55 | {{- with .Values.controller.affinity }} 56 | affinity: 57 | {{- toYaml . | nindent 8 }} 58 | {{- end }} 59 | {{- with .Values.controller.tolerations }} 60 | tolerations: 61 | {{- toYaml . | nindent 8 }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /cluster/minimal/purser-database-setup.yaml: -------------------------------------------------------------------------------- 1 | # Service Dgraph - This is the service that should be used by the clients of Dgraph to talk to the server. 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: purser-db 6 | labels: 7 | app: purser-db 8 | spec: 9 | type: ClusterIP 10 | ports: 11 | - port: 5080 12 | targetPort: 5080 13 | name: zero-grpc 14 | - port: 6080 15 | targetPort: 6080 16 | name: zero-http 17 | - port: 8080 18 | targetPort: 8080 19 | name: server-http 20 | - port: 9080 21 | targetPort: 9080 22 | name: server-grpc 23 | selector: 24 | app: purser-db 25 | --- 26 | # Dgraph StatefulSet runs 1 pod with one Zero and one Server containers. 27 | apiVersion: apps/v1 28 | kind: StatefulSet 29 | metadata: 30 | name: purser-dgraph 31 | spec: 32 | serviceName: "dgraph" 33 | replicas: 1 34 | selector: 35 | matchLabels: 36 | app: purser-db 37 | template: 38 | metadata: 39 | labels: 40 | app: purser-db 41 | spec: 42 | containers: 43 | - name: zero 44 | image: dgraph/dgraph:v1.0.9 45 | imagePullPolicy: IfNotPresent 46 | ports: 47 | - containerPort: 5080 48 | name: zero-grpc 49 | - containerPort: 6080 50 | name: zero-http 51 | volumeMounts: 52 | - name: datadir 53 | mountPath: /dgraph 54 | command: 55 | - bash 56 | - "-c" 57 | - | 58 | set -ex 59 | dgraph zero --my=$(hostname -f):5080 60 | - name: server 61 | image: dgraph/dgraph:v1.0.9 62 | imagePullPolicy: IfNotPresent 63 | ports: 64 | - containerPort: 8080 65 | name: server-http 66 | - containerPort: 9080 67 | name: server-grpc 68 | volumeMounts: 69 | - name: datadir 70 | mountPath: /dgraph 71 | command: 72 | - bash 73 | - "-c" 74 | - | 75 | set -ex 76 | dgraph server --my=$(hostname -f):7080 --lru_mb 2048 --zero $(hostname -f):5080 77 | terminationGracePeriodSeconds: 60 78 | volumes: 79 | - name: datadir 80 | persistentVolumeClaim: 81 | claimName: datadir 82 | updateStrategy: 83 | type: RollingUpdate 84 | volumeClaimTemplates: 85 | - metadata: 86 | name: datadir 87 | annotations: 88 | volume.alpha.kubernetes.io/storage-class: anything 89 | spec: 90 | accessModes: 91 | - "ReadWriteOnce" 92 | resources: 93 | requests: 94 | storage: 5Gi 95 | -------------------------------------------------------------------------------- /pkg/pricing/aws/aws.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package aws 19 | 20 | import ( 21 | "net/http" 22 | "time" 23 | 24 | "github.com/Sirupsen/logrus" 25 | "github.com/vmware/purser/pkg/controller/utils" 26 | ) 27 | 28 | const ( 29 | httpTimeout = 100 * time.Second 30 | ) 31 | 32 | // Pricing structure 33 | type Pricing struct { 34 | Products map[string]Product 35 | Terms PlanList 36 | } 37 | 38 | // PlanList structure 39 | type PlanList struct { 40 | OnDemand map[string]map[string]TermAttributes 41 | } 42 | 43 | // TermAttributes structure 44 | type TermAttributes struct { 45 | PriceDimensions map[string]PricingData 46 | } 47 | 48 | // PricingData structure 49 | type PricingData struct { 50 | Unit string 51 | PricePerUnit map[string]string 52 | } 53 | 54 | // Product structure 55 | type Product struct { 56 | Sku string 57 | ProductFamily string 58 | Attributes ProductAttributes 59 | } 60 | 61 | // ProductAttributes structure 62 | type ProductAttributes struct { 63 | InstanceType string 64 | InstanceFamily string 65 | OperatingSystem string 66 | PreInstalledSW string 67 | VolumeType string 68 | UsageType string 69 | Vcpu string 70 | Memory string 71 | } 72 | 73 | // GetAWSPricing function details 74 | // input: region 75 | // retrieves data from http get to the corresponding url for that region 76 | func GetAWSPricing(region string) (*Pricing, error) { 77 | var myClient = &http.Client{Timeout: httpTimeout} 78 | rateCard := Pricing{} 79 | err := utils.GetJSONResponse(myClient, getURLForRegion(region), &rateCard) 80 | if err != nil { 81 | logrus.Errorf("Unable to get aws pricing. Reason: %v", err) 82 | return nil, err 83 | } 84 | return &rateCard, nil 85 | } 86 | 87 | func getURLForRegion(region string) string { 88 | return "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/" + region + "/index.json" 89 | } 90 | -------------------------------------------------------------------------------- /cmd/controller/api/apiHandlers/helpers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package apiHandlers 19 | 20 | import ( 21 | "encoding/json" 22 | "github.com/Sirupsen/logrus" 23 | "io" 24 | "io/ioutil" 25 | "k8s.io/apimachinery/pkg/util/yaml" 26 | "net/http" 27 | "github.com/vmware/purser/pkg/controller" 28 | "github.com/vmware/purser/pkg/client/clientset/typed/groups/v1" 29 | "k8s.io/client-go/kubernetes" 30 | ) 31 | 32 | var groupClient *v1.GroupClient 33 | var kubeClient *kubernetes.Clientset 34 | 35 | func addHeaders(w *http.ResponseWriter, r *http.Request) { 36 | addAccessControlHeaders(w, r) 37 | (*w).Header().Set("Content-Type", "application/json; charset=UTF-8") 38 | (*w).WriteHeader(http.StatusOK) 39 | } 40 | 41 | func addAccessControlHeaders(w *http.ResponseWriter, r *http.Request) { 42 | (*w).Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) 43 | (*w).Header().Set("Access-Control-Allow-Credentials", "true") 44 | } 45 | 46 | func writeBytes(w io.Writer, data []byte) { 47 | _, err := w.Write(data) 48 | if err != nil { 49 | logrus.Errorf("Unable to encode to json: (%v)", err) 50 | } 51 | } 52 | 53 | func encodeAndWrite(w io.Writer, obj interface{}) { 54 | err := json.NewEncoder(w).Encode(obj) 55 | if err != nil { 56 | logrus.Errorf("Unable to encode to json: (%v)", err) 57 | } 58 | } 59 | 60 | func convertRequestBodyToJSON(r *http.Request) ([]byte, error) { 61 | requestData, err := ioutil.ReadAll(r.Body) 62 | if err != nil { 63 | return nil, err 64 | } 65 | groupData, err := yaml.ToJSON(requestData) 66 | return groupData, err 67 | } 68 | 69 | // SetKubeClientAndGroupClient sets groupcrd client 70 | func SetKubeClientAndGroupClient(conf controller.Config) { 71 | groupClient = conf.Groupcrdclient 72 | kubeClient = conf.Kubeclient 73 | } 74 | 75 | func getGroupClient() *v1.GroupClient { 76 | return groupClient 77 | } 78 | 79 | func getKubeClient() *kubernetes.Clientset { 80 | return kubeClient 81 | } 82 | -------------------------------------------------------------------------------- /pkg/controller/discovery/executer/exec.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package executer 19 | 20 | import ( 21 | "bytes" 22 | "fmt" 23 | "io" 24 | "strings" 25 | 26 | log "github.com/Sirupsen/logrus" 27 | "github.com/vmware/purser/pkg/controller" 28 | 29 | corev1 "k8s.io/api/core/v1" 30 | "k8s.io/apimachinery/pkg/runtime" 31 | "k8s.io/client-go/tools/remotecommand" 32 | ) 33 | 34 | // ExecToPodThroughAPI uninteractively exec to the pod with the command specified. 35 | func ExecToPodThroughAPI(conf controller.Config, pod corev1.Pod, command, containerName string, stdin io.Reader) (string, string, error) { 36 | // Prepare the API URL used to execute another process within the Pod. In this case, 37 | // we'll run a remote shell. 38 | req := conf.Kubeclient.CoreV1().RESTClient().Post(). 39 | Resource("pods"). 40 | Name(pod.Name). 41 | Namespace(pod.Namespace). 42 | SubResource("exec") 43 | 44 | scheme := runtime.NewScheme() 45 | if err := corev1.AddToScheme(scheme); err != nil { 46 | return "", "", fmt.Errorf("error adding to scheme: %v", err) 47 | } 48 | 49 | parameterCodec := runtime.NewParameterCodec(scheme) 50 | req.VersionedParams(&corev1.PodExecOptions{ 51 | Command: strings.Fields(command), 52 | Container: containerName, 53 | Stdin: stdin != nil, 54 | Stdout: true, 55 | Stderr: true, 56 | TTY: false, 57 | }, parameterCodec) 58 | 59 | log.Debug("Request URL:", req.URL().String()) 60 | 61 | exec, err := remotecommand.NewSPDYExecutor(conf.KubeConfig, "POST", req.URL()) 62 | if err != nil { 63 | return "", "", fmt.Errorf("error while creating Executor: %v", err) 64 | } 65 | 66 | // Connect this process' std{in,out,err} to the remote shell process. 67 | var stdout, stderr bytes.Buffer 68 | err = exec.Stream(remotecommand.StreamOptions{ 69 | Stdin: stdin, 70 | Stdout: &stdout, 71 | Stderr: &stderr, 72 | Tty: false, 73 | }) 74 | if err != nil { 75 | return "", "", fmt.Errorf("error in Stream: %v", err) 76 | } 77 | return stdout.String(), stderr.String(), nil 78 | } 79 | -------------------------------------------------------------------------------- /pkg/controller/dgraph/models/query/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package query 19 | 20 | // Constants used in query parameters 21 | const ( 22 | All = "" 23 | Name = "name" 24 | Orphan = "orphan" 25 | View = "view" 26 | Physical = "physical" 27 | Logical = "logical" 28 | False = "false" 29 | ) 30 | 31 | // Children structure 32 | type Children struct { 33 | Name string `json:"name,omitempty"` 34 | Type string `json:"type,omitempty"` 35 | CPU float64 `json:"cpu,omitempty"` 36 | Memory float64 `json:"memory,omitempty"` 37 | Storage float64 `json:"storage,omitempty"` 38 | CPUCost float64 `json:"cpuCost,omitempty"` 39 | MemoryCost float64 `json:"memoryCost,omitempty"` 40 | StorageCost float64 `json:"storageCost,omitempty"` 41 | } 42 | 43 | // ParentWrapper structure 44 | type ParentWrapper struct { 45 | Name string `json:"name,omitempty"` 46 | Type string `json:"type,omitempty"` 47 | Children []Children `json:"children,omitempty"` 48 | Parent []ParentWrapper `json:"parent,omitempty"` 49 | CPU float64 `json:"cpu,omitempty"` 50 | Memory float64 `json:"memory,omitempty"` 51 | Storage float64 `json:"storage,omitempty"` 52 | CPUCost float64 `json:"cpuCost,omitempty"` 53 | MemoryCost float64 `json:"memoryCost,omitempty"` 54 | StorageCost float64 `json:"storageCost,omitempty"` 55 | CPUAllocated float64 `json:"cpuAllocated,omitempty"` 56 | MemoryAllocated float64 `json:"memoryAllocated,omitempty"` 57 | StorageAllocated float64 `json:"storageAllocated,omitempty"` 58 | CPUCapacity float64 `json:"cpuCapacity,omitempty"` 59 | MemoryCapacity float64 `json:"memoryCapacity,omitempty"` 60 | StorageCapacity float64 `json:"storageCapacity,omitempty"` 61 | } 62 | 63 | // JSONDataWrapper structure 64 | type JSONDataWrapper struct { 65 | Data ParentWrapper `json:"data,omitempty"` 66 | } 67 | -------------------------------------------------------------------------------- /pkg/controller/discovery/processor/svc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package processor 19 | 20 | import ( 21 | "github.com/vmware/purser/pkg/controller/utils" 22 | "sync" 23 | 24 | log "github.com/Sirupsen/logrus" 25 | 26 | "github.com/vmware/purser/pkg/controller" 27 | "github.com/vmware/purser/pkg/controller/discovery/linker" 28 | 29 | corev1 "k8s.io/api/core/v1" 30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 | "k8s.io/apimachinery/pkg/labels" 32 | "k8s.io/client-go/kubernetes" 33 | ) 34 | 35 | var svcwg sync.WaitGroup 36 | 37 | // ProcessServiceInteractions parses through the list of services and it's associated pods to 38 | // generate a 1:1 mapping between the communicating services. 39 | func ProcessServiceInteractions(conf controller.Config) { 40 | services := utils.RetrieveServiceList(conf.Kubeclient, metav1.ListOptions{}) 41 | if services == nil { 42 | log.Info("No services retrieved from cluster") 43 | return 44 | } 45 | 46 | processServiceDetails(conf.Kubeclient, services) 47 | linker.GenerateAndStoreSvcInteractions() 48 | 49 | log.Infof("Successfully generated Service To Service mapping.") 50 | } 51 | 52 | func processServiceDetails(client *kubernetes.Clientset, services *corev1.ServiceList) { 53 | svcCount := len(services.Items) 54 | log.Infof("Processing total of (%d) Services.", svcCount) 55 | 56 | svcwg.Add(svcCount) 57 | { 58 | for index, svc := range services.Items { 59 | log.Debugf("Processing Service (%d/%d): %s ", index+1, svcCount, svc.GetName()) 60 | 61 | go func(svc corev1.Service, index int) { 62 | defer svcwg.Done() 63 | 64 | selectorSet := labels.Set(svc.Spec.Selector) 65 | if selectorSet != nil { 66 | options := metav1.ListOptions{ 67 | LabelSelector: selectorSet.AsSelector().String(), 68 | } 69 | pods := utils.RetrievePodList(client, options) 70 | if pods != nil { 71 | linker.PopulatePodToServiceTable(svc, pods) 72 | } 73 | } 74 | 75 | log.Debugf("Finished processing Service (%d/%d)", index+1, svcCount) 76 | }(svc, index) 77 | } 78 | } 79 | svcwg.Wait() 80 | } 81 | -------------------------------------------------------------------------------- /docs/plugin-usage.md: -------------------------------------------------------------------------------- 1 | # Purser Plugin Usage 2 | 3 | Once installed, Purser is ready for use right away. You can query using native Kubernetes grouping artifacts. 4 | 5 | Purser supports the following list of commands. 6 | 7 | ``` bash 8 | # query cluster visibility in terms of savings and summary for the application. 9 | kubectl plugin purser get [summary|savings] 10 | 11 | # query resources filtered by associated namespace, labels and groups. 12 | kubectl plugin purser get resources group 13 | 14 | # query cost filtered by associated labels, pods and node. 15 | kubectl plugin purser get cost label 16 | kubectl plugin purser get cost pod 17 | kubectl plugin purser get cost node all 18 | 19 | # configure user-costs for the choice of deployment. 20 | kubectl plugin purser [set|get] user-costs 21 | ``` 22 | 23 | _Use flag `--kubeconfig=` if your cluster configuration is not at the [default location](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#the-kubeconfig-environment-variable)._ 24 | 25 | ## Examples 26 | 27 | 1. Get Cluster Summary 28 | 29 | ``` bash 30 | $ kubectl plugin purser get summary 31 | Cluster Summary 32 | Compute: 33 | Node count: 57 34 | Cost: 3015.48$ 35 | Total Capacity: 36 | Cpu(vCPU): 456 37 | Memory(GB): 1770.50 38 | Provisioned Resources: 39 | Cpu Request(vCPU): 319 40 | Memory Request(GB): 1032.67 41 | Storage: 42 | Persistent Volume count: 151 43 | Capacity(GB): 9297.00 44 | Cost: 4124.79$ 45 | PV Claim count: 108 46 | PV Claim Capacity(GB): 8867.00 47 | Cost: 48 | Compute cost: 3015.48$ 49 | Storage cost: 4124.79$ 50 | Total cost: 7140.27$ 51 | ``` 52 | 53 | 54 | 2. Get Cost Of All Nodes 55 | 56 | ``` bash 57 | kubectl purser get cost node all 58 | ``` 59 | 60 | 3. Get Savings 61 | 62 | ``` bash 63 | $ kubectl plugin purser get savings 64 | Savings Summary 65 | Storage: 66 | Unused Volumes: 43 67 | Unused Capacity(GB): 430.00 68 | Month To Date Savings: 186.33$ 69 | Projected Monthly Savings: 1066.40$ 70 | ``` 71 | 72 | Next, define higher level groupings to define your business, logical or application constructs. 73 | 74 | ## Defining Custom Groups 75 | 76 | Refer [doc](./custom-group-installation-and-usage.md) for custom group installation and usage. -------------------------------------------------------------------------------- /pkg/controller/discovery/linker/processlinks.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package linker 19 | 20 | import ( 21 | "time" 22 | 23 | log "github.com/Sirupsen/logrus" 24 | "github.com/vmware/purser/pkg/controller/dgraph/models" 25 | ) 26 | 27 | // Process holds the details for the executing processes inside the container 28 | type Process struct { 29 | ID, Name string 30 | } 31 | 32 | // StoreProcessInteractions stores process, container to process edge, process to pods edge 33 | func StoreProcessInteractions(containerProcessInteraction map[string][]string, processPodInteraction map[string](map[string]bool), creationTime time.Time) { 34 | for containerXID, procsXIDs := range containerProcessInteraction { 35 | for _, procXID := range procsXIDs { 36 | podsXIDs := []string{} 37 | for podXID := range processPodInteraction[procXID] { 38 | podsXIDs = append(podsXIDs, podXID) 39 | } 40 | 41 | err := models.StoreProcess(procXID, containerXID, podsXIDs, creationTime) 42 | if err != nil { 43 | log.Errorf("failed to store process details: %s, err: (%v)", procXID, err) 44 | } 45 | } 46 | err := models.StoreContainerProcessEdge(containerXID, procsXIDs) 47 | if err != nil { 48 | log.Errorf("failed to store edge from container: %s to procs, err: (%v)", containerXID, err) 49 | } 50 | } 51 | } 52 | 53 | func populateContainerProcessTable(containerXID, procXID string, interactions *InteractionsWrapper) { 54 | if _, isPresent := interactions.ContainerProcessInteraction[containerXID]; !isPresent { 55 | interactions.ContainerProcessInteraction[containerXID] = []string{} 56 | } 57 | interactions.ContainerProcessInteraction[containerXID] = append(interactions.ContainerProcessInteraction[containerXID], procXID) 58 | } 59 | 60 | func updatePodProcessInteractions(procXID, dstName string, interactions *InteractionsWrapper) { 61 | if dstName != "" { 62 | if _, isPresent := interactions.ProcessToPodInteraction[procXID]; !isPresent { 63 | interactions.ProcessToPodInteraction[procXID] = make(map[string]bool) 64 | } 65 | interactions.ProcessToPodInteraction[procXID][dstName] = true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/controller/utils/purge.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package utils 19 | 20 | import ( 21 | "encoding/hex" 22 | "fmt" 23 | "strings" 24 | 25 | "github.com/Sirupsen/logrus" 26 | ) 27 | 28 | // PurgeTCPData handles IP conversion from Hex to Dec and cleans up data to contain only 29 | // inter pod address information. 30 | func PurgeTCPData(data string) []string { 31 | var tcpDump []string 32 | 33 | tcpDumpHex := getTCPDumpHexFromData(data) 34 | for _, address := range tcpDumpHex { 35 | localIP, localPort := hexToDecIP(address[6:14]), address[15:19] 36 | remoteIP, remotePort := hexToDecIP(address[20:28]), address[29:33] 37 | 38 | if isLocalHost(localIP, remoteIP) { 39 | continue 40 | } 41 | 42 | addressMapping := localIP + ":" + localPort + ":" + remoteIP + ":" + remotePort 43 | tcpDump = append(tcpDump, addressMapping) 44 | } 45 | return tcpDump 46 | } 47 | 48 | // PurgeTCP6Data handles IP conversion from Hex to Dec and cleans up data to contain only 49 | // inter pod address information. 50 | func PurgeTCP6Data(data string) []string { 51 | var tcpDump []string 52 | 53 | tcpDumpHex := getTCPDumpHexFromData(data) 54 | for _, address := range tcpDumpHex { 55 | localIP, localPort := hexToDecIP(address[30:38]), address[39:43] 56 | remoteIP, remotePort := hexToDecIP(address[68:76]), address[77:81] 57 | 58 | if isLocalHost(localIP, remoteIP) { 59 | continue 60 | } 61 | 62 | addressMapping := localIP + ":" + localPort + ":" + remoteIP + ":" + remotePort 63 | tcpDump = append(tcpDump, addressMapping) 64 | } 65 | return tcpDump 66 | } 67 | 68 | func getTCPDumpHexFromData(data string) []string { 69 | tcpDumpHex := strings.Split(data, "\n") 70 | if len(tcpDumpHex) <= 1 { 71 | return nil 72 | } 73 | 74 | // ignore title and last one as it is empty 75 | tcpDumpHex = tcpDumpHex[1 : len(tcpDumpHex)-1] 76 | return tcpDumpHex 77 | } 78 | 79 | func hexToDecIP(hexIP string) string { 80 | decBytes, err := hex.DecodeString(hexIP) 81 | if err != nil { 82 | logrus.Warnf("failed to decode string to hex %v", err) 83 | } 84 | return fmt.Sprintf("%v.%v.%v.%v", decBytes[3], decBytes[2], decBytes[1], decBytes[0]) 85 | } 86 | 87 | func isLocalHost(localIP, remoteIP string) bool { 88 | return strings.Compare(localIP, "0.0.0.0") == 0 || strings.Compare(localIP, "127.0.0.1") == 0 || strings.Compare(remoteIP, "0.0.0.0") == 0 89 | } 90 | -------------------------------------------------------------------------------- /cluster/purser-database-setup.yaml: -------------------------------------------------------------------------------- 1 | # Service Dgraph - This is the service that should be used by the clients of Dgraph to talk to the server. 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: purser-db 6 | labels: 7 | app: purser-db 8 | spec: 9 | type: ClusterIP 10 | ports: 11 | - port: 5080 12 | targetPort: 5080 13 | name: zero-grpc 14 | - port: 6080 15 | targetPort: 6080 16 | name: zero-http 17 | - port: 8080 18 | targetPort: 8080 19 | name: server-http 20 | - port: 9080 21 | targetPort: 9080 22 | name: server-grpc 23 | selector: 24 | app: purser-db 25 | --- 26 | # Dgraph StatefulSet runs 1 pod with one Zero and one Server containers. 27 | apiVersion: apps/v1 28 | kind: StatefulSet 29 | metadata: 30 | name: purser-dgraph 31 | spec: 32 | serviceName: "dgraph" 33 | replicas: 1 34 | selector: 35 | matchLabels: 36 | app: purser-db 37 | template: 38 | metadata: 39 | labels: 40 | app: purser-db 41 | spec: 42 | containers: 43 | - name: zero 44 | image: dgraph/dgraph:v1.0.9 45 | imagePullPolicy: IfNotPresent 46 | resources: 47 | limits: 48 | memory: 1000Mi 49 | cpu: 300m 50 | requests: 51 | memory: 1000Mi 52 | cpu: 300m 53 | ports: 54 | - containerPort: 5080 55 | name: zero-grpc 56 | - containerPort: 6080 57 | name: zero-http 58 | volumeMounts: 59 | - name: datadir 60 | mountPath: /dgraph 61 | command: 62 | - bash 63 | - "-c" 64 | - | 65 | set -ex 66 | dgraph zero --my=$(hostname -f):5080 67 | - name: server 68 | image: dgraph/dgraph:v1.0.9 69 | imagePullPolicy: IfNotPresent 70 | resources: 71 | limits: 72 | memory: 1500Mi 73 | cpu: 500m 74 | requests: 75 | memory: 1500Mi 76 | cpu: 500m 77 | ports: 78 | - containerPort: 8080 79 | name: server-http 80 | - containerPort: 9080 81 | name: server-grpc 82 | volumeMounts: 83 | - name: datadir 84 | mountPath: /dgraph 85 | command: 86 | - bash 87 | - "-c" 88 | - | 89 | set -ex 90 | dgraph server --my=$(hostname -f):7080 --lru_mb 2048 --zero $(hostname -f):5080 91 | terminationGracePeriodSeconds: 60 92 | volumes: 93 | - name: datadir 94 | persistentVolumeClaim: 95 | claimName: datadir 96 | updateStrategy: 97 | type: RollingUpdate 98 | volumeClaimTemplates: 99 | - metadata: 100 | name: datadir 101 | annotations: 102 | volume.alpha.kubernetes.io/storage-class: anything 103 | spec: 104 | accessModes: 105 | - "ReadWriteOnce" 106 | resources: 107 | requests: 108 | storage: 10Gi 109 | -------------------------------------------------------------------------------- /pkg/controller/dgraph/models/query/login.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package query 19 | 20 | import ( 21 | "github.com/Sirupsen/logrus" 22 | "github.com/vmware/purser/pkg/controller/dgraph" 23 | 24 | "golang.org/x/crypto/bcrypt" 25 | ) 26 | 27 | // Authenticate performs user authentication for service access 28 | func Authenticate(username, inputPassword string) bool { 29 | if !validateUsername(username) { 30 | return false 31 | } 32 | login, err := getLoginCredentials(username) 33 | if err != nil { 34 | logrus.Error(err) 35 | return false 36 | } 37 | return comparePasswords(login.Password, []byte(inputPassword)) 38 | } 39 | 40 | // UpdatePassword updates stored password with new one for the given username in Dgraph 41 | func UpdatePassword(username, oldPassword, newPassword string) bool { 42 | if Authenticate(username, oldPassword) { 43 | login, err := getLoginCredentials(username) 44 | if err != nil { 45 | logrus.Error(err) 46 | return false 47 | } 48 | if err = hashAndUpdatePassword(&login, newPassword); err == nil { 49 | return true 50 | } 51 | logrus.Error(err) 52 | } 53 | return false 54 | } 55 | 56 | func hashAndUpdatePassword(login *dgraph.Login, newPassword string) error { 57 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost) 58 | if err != nil { 59 | return err 60 | } 61 | login.Password = string(hashedPassword) 62 | _, err = dgraph.MutateNode(login, dgraph.UPDATE) 63 | return err 64 | } 65 | 66 | // getLoginCredentials returns a struct of hashed password and username. 67 | func getLoginCredentials(username string) (dgraph.Login, error) { 68 | q := `query { 69 | login(func: has(isLogin)) @filter(eq(username, ` + username + `)) { 70 | uid 71 | username 72 | password 73 | } 74 | }` 75 | type root struct { 76 | LoginList []dgraph.Login `json:"login"` 77 | } 78 | newRoot := root{} 79 | if err := executeQuery(q, &newRoot); err != nil || newRoot.LoginList == nil { 80 | return dgraph.Login{}, err 81 | } 82 | return newRoot.LoginList[0], nil 83 | } 84 | 85 | func validateUsername(username string) bool { 86 | return username == "admin" 87 | } 88 | 89 | func comparePasswords(hashedPwd string, plainPwd []byte) bool { 90 | byteHash := []byte(hashedPwd) 91 | if err := bcrypt.CompareHashAndPassword(byteHash, plainPwd); err != nil { 92 | logrus.Error(err) 93 | return false 94 | } 95 | return true 96 | } 97 | -------------------------------------------------------------------------------- /pkg/plugin/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package plugin 19 | 20 | import ( 21 | "time" 22 | 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | ) 25 | 26 | // getCurrentTime returns the current time as k8s apimachinery Time object 27 | func getCurrentTime() metav1.Time { 28 | return metav1.Now() 29 | } 30 | 31 | // getCurrentMonthStartTime returns month start time as k8s apimachinery Time object 32 | func getCurrentMonthStartTime() metav1.Time { 33 | now := time.Now() 34 | monthStart := metav1.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.Local) 35 | return monthStart 36 | } 37 | 38 | /* 39 | currentMonthActiveTimeInHours returns active time (endTime - startTime) in the current month. 40 | 1. If startTime is before month start then it is set as month start 41 | 2. If endTime is not set(isZero) then it is set as current time 42 | These two conditions ensures that the active time we compute is within the current month. 43 | */ 44 | func currentMonthActiveTimeInHours(startTime, endTime metav1.Time) float64 { 45 | currentTime := getCurrentTime() 46 | monthStart := getCurrentMonthStartTime() 47 | return currentMonthActiveTimeInHoursMulti(startTime, endTime, currentTime, monthStart) 48 | } 49 | 50 | /* 51 | currentMonthActiveTimeInHoursMulti is same as currentMonthActiveTimeInHours but it needs extra inputs: 52 | currentTime and monthStart. 53 | Use this method(currentMonthActiveTimeInHoursMulti) if you want to caclculate active time multiple times (ex: inside a loop). 54 | */ 55 | func currentMonthActiveTimeInHoursMulti(startTime, endTime, currentTime, monthStart metav1.Time) float64 { 56 | if startTime.Time.Before(monthStart.Time) { 57 | startTime = monthStart 58 | } 59 | 60 | if endTime.IsZero() { 61 | endTime = currentTime 62 | } 63 | 64 | duration := endTime.Time.Sub(startTime.Time) 65 | durationInHours := duration.Hours() 66 | return durationInHours 67 | } 68 | 69 | // totalHoursTillNow return number of hours from month start to current time. 70 | func totalHoursTillNow() float64 { 71 | monthStart := getCurrentMonthStartTime() 72 | currentTime := getCurrentTime() 73 | return currentMonthActiveTimeInHours(monthStart, currentTime) 74 | } 75 | 76 | func projectToMonth(val float64) float64 { 77 | // TODO: enhance this. 78 | return (val * 31 * 24) / totalHoursTillNow() 79 | } 80 | 81 | func bytesToGB(val int64) float64 { 82 | return float64(val) / (1024.0 * 1024.0 * 1024.0) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/controller/dgraph/purge.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package dgraph 19 | 20 | import ( 21 | "github.com/vmware/purser/pkg/controller/utils" 22 | 23 | log "github.com/Sirupsen/logrus" 24 | "time" 25 | ) 26 | 27 | type resource struct { 28 | ID 29 | } 30 | 31 | // RemoveResourcesInactive deletes all resources which have their deletion time stamp before 32 | // the start of current month. 33 | func RemoveResourcesInactive() { 34 | err := removeOldDeletedResources() 35 | if err != nil { 36 | log.Println(err) 37 | } 38 | 39 | err = removeOldDeletedPods() 40 | if err != nil { 41 | log.Error(err) 42 | } 43 | } 44 | 45 | func removeOldDeletedResources() error { 46 | uids, err := retrieveResourcesWithEndTimeBeforeCurrentMonthStart() 47 | if err != nil { 48 | return err 49 | } 50 | if len(uids) == 0 { 51 | log.Println("No old deleted resources are present in dgraph") 52 | return nil 53 | } 54 | 55 | _, err = MutateNode(uids, DELETE) 56 | return err 57 | } 58 | 59 | func removeOldDeletedPods() error { 60 | uids, err := retrievePodsWithEndTimeBeforeThreeMonths() 61 | if err != nil { 62 | return err 63 | } 64 | if len(uids) == 0 { 65 | log.Println("No old deleted pods are present in dgraph") 66 | return nil 67 | } 68 | 69 | _, err = MutateNode(uids, DELETE) 70 | return err 71 | } 72 | 73 | func retrieveResourcesWithEndTimeBeforeCurrentMonthStart() ([]resource, error) { 74 | q := `query { 75 | resources(func: le(endTime, "` + utils.ConverTimeToRFC3339(utils.GetCurrentMonthStartTime()) + `")) @filter(NOT(has(isPod))) { 76 | uid 77 | } 78 | }` 79 | 80 | type root struct { 81 | Resources []resource `json:"resources"` 82 | } 83 | newRoot := root{} 84 | err := ExecuteQuery(q, &newRoot) 85 | if err != nil { 86 | return nil, err 87 | } 88 | return newRoot.Resources, nil 89 | } 90 | 91 | func retrievePodsWithEndTimeBeforeThreeMonths() ([]resource, error) { 92 | q := `query { 93 | resources(func: le(endTime, "` + utils.ConverTimeToRFC3339(utils.GetCurrentMonthStartTime().Add(-time.Hour*24*30*2)) + `")) @filter(has(isPod)) { 94 | uid 95 | } 96 | }` 97 | 98 | type root struct { 99 | Resources []resource `json:"resources"` 100 | } 101 | newRoot := root{} 102 | err := ExecuteQuery(q, &newRoot) 103 | if err != nil { 104 | return nil, err 105 | } 106 | return newRoot.Resources, nil 107 | } 108 | -------------------------------------------------------------------------------- /ui/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .main-container{ 2 | .appHeader{ 3 | font-size: 20px; 4 | align-items: center; 5 | } 6 | } 7 | .content-container { 8 | position: relative; 9 | height: 100%; 10 | display: flex; 11 | display: -webkit-flex; 12 | display: -moz-flex; 13 | display: -ms-flex; 14 | flex-direction: column; 15 | -webkit-box-direction: normal; 16 | -webkit-box-orient: vertical; 17 | .header { 18 | -webkit-box-flex: 0; 19 | box-flex: 0; 20 | flex: 0 0 60px; 21 | display: flex; 22 | } 23 | .webpageSpinner { 24 | position: absolute; 25 | top: 0; 26 | bottom: 0; 27 | right: 0; 28 | left: 0; 29 | z-index: 100; 30 | background: white; 31 | .spinner { 32 | position: absolute; 33 | margin: auto; 34 | top: 0; 35 | bottom: 0; 36 | right: 0; 37 | left: 0; 38 | } 39 | } 40 | .main-body { 41 | display: flex; 42 | display: -webkit-flex; 43 | display: -moz-flex; 44 | display: -ms-flex; 45 | overflow-x: hidden; 46 | -webkit-box-flex: 1; 47 | -ms-flex: 1 1 auto; 48 | flex: 1 1 auto; 49 | .navigation-area { 50 | /* -webkit-box-flex: 0; 51 | -ms-flex: 0 0 auto; 52 | flex: 0 0 auto; 53 | -webkit-box-ordinal-group: 0; 54 | order: -1; 55 | overflow: hidden; 56 | display: flex; 57 | -webkit-box-orient: vertical; 58 | -webkit-box-direction: normal; 59 | flex-direction: column;*/ 60 | background-color: #eee; 61 | } 62 | .content-area { 63 | background-color: #FAFAFA; 64 | display: flex; 65 | display: -webkit-flex; 66 | display: -moz-flex; 67 | display: -ms-flex; 68 | -webkit-box-flex: 1; 69 | -ms-flex: 1 1 auto; 70 | flex: 1 1 auto; 71 | -webkit-flex-direction: column; 72 | flex-direction: column; 73 | overflow-x: hidden; 74 | padding: 20px 24px 80px 24px; 75 | .bread-crumb { 76 | border-style: solid; 77 | border-width: 0px; 78 | border-color: grey; 79 | max-height: 40px; 80 | font-size: 12px; 81 | z-index: 10; 82 | .synctime-div { 83 | float: right; 84 | font-size: 12px; 85 | } 86 | } 87 | .page-area { 88 | flex: auto; 89 | position: relative; 90 | } 91 | .app-loader { 92 | height: 100%; 93 | display: flex; 94 | justify-content: center; 95 | align-items: center; 96 | flex-direction: column; 97 | } 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /docs/custom-group-installation-and-usage.md: -------------------------------------------------------------------------------- 1 | # Custom Group Installation and Usage 2 | 3 | To get resource and cost visibility for a particular set of pods Purser allows user to create custom logical group. 4 | User can define the label filter logic(`AND of ORs`: Conjunctive normal form) while creating the logical group i.e, pods satisfying these conditions will belong to this custom group. 5 | 6 | ## Installing logical group definition and an example logical group 7 | 8 | To install the logical group definition into your cluster, 9 | download [purser-group-crd.yaml](../cluster/artifacts/purser-group-crd.yaml) yaml i.e, 10 | ```yaml 11 | apiVersion: apiextensions.k8s.io/v1beta1 12 | kind: CustomResourceDefinition 13 | metadata: 14 | name: groups.vmware.purser.com 15 | spec: 16 | group: vmware.purser.com 17 | names: 18 | kind: Group 19 | listKind: GroupList 20 | plural: groups 21 | singular: group 22 | scope: Namespaced 23 | version: v1 24 | status: 25 | acceptedNames: 26 | kind: Group 27 | listKind: GroupList 28 | plural: groups 29 | singular: group 30 | ``` 31 | and use kubectl to install this definition 32 | ```bash 33 | kubectl create -f purser-group-crd.yaml 34 | ``` 35 | _**NOTE:** This installation is needed only once per cluster_ 36 | 37 | **Installing an example logical group** 38 | 39 | Download [example-group.yaml](../cluster/artifacts/example-group.yaml) yaml i.e, 40 | ```yaml 41 | apiVersion: vmware.purser.com/v1 42 | kind: Group 43 | metadata: 44 | name: example-group 45 | spec: 46 | name: example-group 47 | labels: 48 | expr1: 49 | app: 50 | - sample-app 51 | - sample-app2 52 | env: 53 | - dev 54 | expr2: 55 | namespace: 56 | - ns1 57 | - ns2 58 | expr3: 59 | key1: 60 | - val1 61 | key2: 62 | - val2 63 | ``` 64 | and use kubectl to create this logical group 65 | ```bash 66 | kubectl create -f example-group.yaml 67 | kubectl get groups.vmware.purser.com 68 | ``` 69 | 70 | This will create a custom logical group with name `example-group` of type `groups.vmware.purser.com`. 71 | The label filter (used to fetch pods belonging to this group) for `example-group` will be 72 | ```yaml 73 | (app=sampl-app OR app=sample-app2 OR env=dev) AND (namespace=ns1 OR namespace=ns2) AND (key1=val1 OR key2=val2) 74 | ``` 75 | 76 | In general the syntax purser supports is: 77 | 78 | ``` 79 | expr1 AND expr2 AND expr3 AND ... 80 | where each expr is of form key1:value1 OR key2:value2 OR key1:value3 OR ... 81 | ``` 82 | 83 | ## Usage 84 | For resource and cost visibility into this newly created logical group run the following command 85 | ```bash 86 | kubectl plugin purser get resources group example-group 87 | ``` 88 | _Refer [purser installation](../README.md#installation) to install purser controller and plugin_ 89 | 90 | ## Uninstalling purser custom group 91 | To uninstall purser custom group run the following command 92 | ```bash 93 | kubectl delete -f purser-group-crd.yaml 94 | ``` 95 | where [purser-group-crd.yaml](../cluster/artifacts/purser-group-crd.yaml) is same file that you downloaded during installation. -------------------------------------------------------------------------------- /pkg/plugin/volume.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package plugin 19 | 20 | import ( 21 | "fmt" 22 | 23 | "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/api/errors" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | ) 27 | 28 | // PersistentVolumeClaim details 29 | type PersistentVolumeClaim struct { 30 | name string 31 | volumeName string 32 | requestSizeInGB float64 33 | capacityAllotedInGB float64 34 | storageClass *string 35 | } 36 | 37 | // GetClusterVolumes returns list of persistent volumes for the cluster. 38 | func GetClusterVolumes() []v1.PersistentVolume { 39 | pvs, err := ClientSetInstance.CoreV1().PersistentVolumes().List(metav1.ListOptions{}) 40 | if err != nil { 41 | panic(err.Error()) 42 | } 43 | return pvs.Items 44 | } 45 | 46 | // GetClusterPersistentVolumeClaims returns the list of persistent volume claims for the cluster. 47 | func GetClusterPersistentVolumeClaims() []v1.PersistentVolumeClaim { 48 | pvcs, err := ClientSetInstance.CoreV1().PersistentVolumeClaims("").List(metav1.ListOptions{}) 49 | if err != nil { 50 | panic(err.Error()) 51 | } 52 | return pvcs.Items 53 | } 54 | 55 | func collectPersistentVolumeClaims(pvcs map[string]*PersistentVolumeClaim) map[string]*PersistentVolumeClaim { 56 | for key := range pvcs { 57 | pvc := collectPersistentVolumeClaim(key) 58 | pvcs[key] = pvc 59 | } 60 | return pvcs 61 | } 62 | 63 | func collectPersistentVolumeClaim(claimName string) *PersistentVolumeClaim { 64 | pvc, err := ClientSetInstance.CoreV1().PersistentVolumeClaims("default").Get(claimName, metav1.GetOptions{}) 65 | if errors.IsNotFound(err) { 66 | fmt.Printf("Persistent Volume Claim %s not found\n", claimName) 67 | return nil 68 | } else if statusError, isStatus := err.(*errors.StatusError); isStatus { 69 | fmt.Printf("Error getting persistence volume Claim %s : %v\n", claimName, statusError.ErrStatus.Message) 70 | return nil 71 | } else if err != nil { 72 | panic(err.Error()) 73 | } else { 74 | request := pvc.Spec.Resources.Requests["storage"].DeepCopy() 75 | capacity := pvc.Status.Capacity["storage"].DeepCopy() 76 | 77 | return &PersistentVolumeClaim{ 78 | name: pvc.GetObjectMeta().GetName(), 79 | volumeName: pvc.Spec.VolumeName, 80 | storageClass: pvc.Spec.StorageClassName, 81 | requestSizeInGB: bytesToGB(request.Value()), 82 | capacityAllotedInGB: bytesToGB(capacity.Value()), 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/controller/dgraph/models/subscriber.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 VMware Inc. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package models 19 | 20 | import ( 21 | "github.com/Sirupsen/logrus" 22 | "time" 23 | 24 | subscribers_v1 "github.com/vmware/purser/pkg/apis/subscriber/v1" 25 | "github.com/vmware/purser/pkg/controller/dgraph" 26 | ) 27 | 28 | // Dgraph Model Constants 29 | const ( 30 | IsSubscriber = "isSubscriber" 31 | ) 32 | 33 | // SubscriberCRD schema in dgraph 34 | type SubscriberCRD struct { 35 | dgraph.ID 36 | IsSubscriber bool `json:"isSubscriber,omitempty"` 37 | Name string `json:"name,omitempty"` 38 | StartTime string `json:"startTime,omitempty"` 39 | EndTime string `json:"endTime,omitempty"` 40 | Type string `json:"type,omitempty"` 41 | Spec SubscriberSpec `json:"spec"` 42 | } 43 | 44 | // SubscriberSpec definition details 45 | type SubscriberSpec struct { 46 | Name string `json:"name"` 47 | Headers map[string]string `json:"headers"` 48 | URL string `json:"url"` 49 | } 50 | 51 | func createSubscriberCRDObject(subscriber subscribers_v1.Subscriber) SubscriberCRD { 52 | newSubscriber := SubscriberCRD{ 53 | Name: subscriber.Name, 54 | IsSubscriber: true, 55 | Type: subscribers_v1.SubscriberGroup, 56 | ID: dgraph.ID{Xid: "subscriber-" + subscriber.Name}, 57 | StartTime: subscriber.GetCreationTimestamp().Time.Format(time.RFC3339), 58 | Spec: SubscriberSpec{ 59 | Name: subscriber.Spec.Name, 60 | Headers: subscriber.Spec.Headers, 61 | URL: subscriber.Spec.URL, 62 | }, 63 | } 64 | 65 | deletionTimestamp := subscriber.GetDeletionTimestamp() 66 | if !deletionTimestamp.IsZero() { 67 | newSubscriber.EndTime = deletionTimestamp.Time.Format(time.RFC3339) 68 | } 69 | return newSubscriber 70 | } 71 | 72 | // StoreSubscriberCRD create a new subscriber CRD in the Dgraph and updates if already present. 73 | func StoreSubscriberCRD(subscriber subscribers_v1.Subscriber) (string, error) { 74 | xid := "subscriber-" + subscriber.Name 75 | uid := dgraph.GetUID(xid, IsSubscriber) 76 | 77 | if uid != "" { 78 | return uid, nil 79 | } 80 | 81 | newSubscriber := createSubscriberCRDObject(subscriber) 82 | assigned, err := dgraph.MutateNode(newSubscriber, dgraph.CREATE) 83 | if err != nil { 84 | return "", err 85 | } 86 | logrus.Infof("Subscriber: (%v) persisted in dgraph", subscriber.Name) 87 | return assigned.Uids["blank-0"], nil 88 | } 89 | --------------------------------------------------------------------------------