├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .make ├── Makefile.deploy.controller └── Makefile.deploy.purser ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile.in ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── build ├── build.sh ├── purser-binary-install.sh ├── purser-binary-uninstall.sh ├── purser-minimal-setup.sh └── purser-setup.sh ├── cluster ├── artifacts │ ├── example-group.yaml │ ├── example-subscriber.yaml │ ├── group-template.json │ ├── purser-group-crd.yaml │ └── purser-subscriber-crd.yaml ├── helm │ └── chart │ │ └── purser │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── README.md │ │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── purser-controller-deployment.yaml │ │ ├── purser-controller-rbac.yaml │ │ ├── purser-controller-serviceaccount.yaml │ │ ├── purser-controller-svc.yaml │ │ ├── purser-database-statefulset.yaml │ │ ├── purser-database-svc.yaml │ │ ├── purser-ui-configmap.yaml │ │ ├── purser-ui-deployment.yaml │ │ ├── purser-ui-ingress.yaml │ │ └── purser-ui-svc.yaml │ │ └── values.yaml ├── minimal │ ├── purser-controller-setup.yaml │ ├── purser-database-setup.yaml │ └── purser-ui-setup.yaml ├── purser-controller-setup.yaml ├── purser-database-setup.yaml └── purser-ui-setup.yaml ├── cmd ├── controller │ ├── api │ │ ├── api.go │ │ ├── apiHandlers │ │ │ ├── authenticationHandlers.go │ │ │ ├── customGroupHandlers.go │ │ │ ├── helpers.go │ │ │ └── hierarchyAndMetricAPIHandlers.go │ │ ├── logger.go │ │ ├── router.go │ │ └── routes.go │ ├── config │ │ └── config.go │ └── purserctrl.go └── plugin │ ├── purser.go │ └── types.go ├── docs ├── architecture.md ├── custom-group-installation-and-usage.md ├── design │ └── pricing.md ├── developers-guide.md ├── img │ ├── architecture.png │ ├── architecture01.png │ └── purser-cli.gif ├── manual-installation.md ├── plugin-installation.md ├── plugin-usage.md ├── purser-deployment.md └── sourcecode-installation.md ├── openapi.yaml ├── pkg ├── apis │ ├── groups │ │ └── v1 │ │ │ ├── deepcopy.go │ │ │ ├── docs.go │ │ │ ├── register.go │ │ │ └── types.go │ └── subscriber │ │ └── v1 │ │ ├── deepcopy.go │ │ ├── docs.go │ │ ├── register.go │ │ └── types.go ├── client │ ├── clientset.go │ └── clientset │ │ └── typed │ │ ├── groups │ │ └── v1 │ │ │ ├── group.go │ │ │ └── group_client.go │ │ └── subscriber │ │ └── v1 │ │ ├── subsciber_client.go │ │ └── subscriber.go ├── controller │ ├── buffering │ │ └── ring_buffer.go │ ├── controller.go │ ├── controller_test.go │ ├── dgraph │ │ ├── dgraph.go │ │ ├── login.go │ │ ├── models │ │ │ ├── constants.go │ │ │ ├── container.go │ │ │ ├── daemonset.go │ │ │ ├── deployment.go │ │ │ ├── group.go │ │ │ ├── job.go │ │ │ ├── label.go │ │ │ ├── namespace.go │ │ │ ├── node.go │ │ │ ├── pod.go │ │ │ ├── pod_test.go │ │ │ ├── process.go │ │ │ ├── pv.go │ │ │ ├── pvc.go │ │ │ ├── query │ │ │ │ ├── cluster.go │ │ │ │ ├── cluster_test.go │ │ │ │ ├── constants_test.go │ │ │ │ ├── group.go │ │ │ │ ├── group_test.go │ │ │ │ ├── helpers.go │ │ │ │ ├── helpers_test.go │ │ │ │ ├── label.go │ │ │ │ ├── label_test.go │ │ │ │ ├── login.go │ │ │ │ ├── pod.go │ │ │ │ ├── pod_test.go │ │ │ │ ├── queries.go │ │ │ │ ├── resource.go │ │ │ │ ├── resource_test.go │ │ │ │ ├── subscriber.go │ │ │ │ ├── subscriber_test.go │ │ │ │ └── types.go │ │ │ ├── rateCard.go │ │ │ ├── replicaset.go │ │ │ ├── service.go │ │ │ ├── statefulset.go │ │ │ └── subscriber.go │ │ └── purge.go │ ├── discovery │ │ ├── executer │ │ │ └── exec.go │ │ ├── generator │ │ │ └── graph.go │ │ ├── linker │ │ │ ├── podlinks.go │ │ │ ├── processlinks.go │ │ │ └── servicelinks.go │ │ └── processor │ │ │ ├── container.go │ │ │ ├── pod.go │ │ │ └── svc.go │ ├── eventprocessor │ │ ├── notifier.go │ │ ├── processor.go │ │ ├── sync.go │ │ └── updater.go │ ├── metrics │ │ └── metrics.go │ ├── payload.go │ ├── persistentVolume.go │ ├── types.go │ └── utils │ │ ├── jsonutils.go │ │ ├── k8sUtils.go │ │ ├── purge.go │ │ ├── purge_test.go │ │ ├── timeUtils.go │ │ ├── unitConversions.go │ │ └── unitConversions_test.go ├── plugin │ ├── costing.go │ ├── grouping.go │ ├── metrics │ │ └── metrics.go │ ├── node.go │ ├── pod.go │ ├── pricing.go │ ├── utils.go │ └── volume.go ├── pricing │ ├── aws │ │ ├── aws.go │ │ └── convert.go │ └── cloud.go └── utils │ ├── fileutils.go │ ├── k8sutil.go │ └── logutil.go ├── plugin.yaml ├── test ├── controller │ └── buffering │ │ └── ring_buffer_test.go ├── pricing │ └── pricing_aws_test.go └── utils │ └── checkUtil.go └── ui ├── Dockerfile.deploy.purser ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── nginx.conf ├── package.json ├── proxy.conf.json ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.constants.ts │ ├── app.module.ts │ ├── app.routing.ts │ ├── common │ │ └── messages │ │ │ ├── common.messages.ts │ │ │ └── left-navigation.messages.ts │ └── modules │ │ ├── capacity-graph │ │ ├── capacity-graph.module.ts │ │ ├── components │ │ │ ├── capactiy-graph.component.html │ │ │ ├── capactiy-graph.component.scss │ │ │ ├── capactiy-graph.component.spec.ts │ │ │ └── capactiy-graph.component.ts │ │ └── services │ │ │ └── capacity-graph.service.ts │ │ ├── changepassword │ │ ├── changepassword.module.ts │ │ ├── components │ │ │ ├── changepassword.component.html │ │ │ ├── changepassword.component.scss │ │ │ ├── changepassword.component.spec.ts │ │ │ └── changepassword.component.ts │ │ └── services │ │ │ └── changepassword.service.ts │ │ ├── logical-group │ │ ├── components │ │ │ ├── logical-group.component.css │ │ │ ├── logical-group.component.html │ │ │ ├── logical-group.component.spec.ts │ │ │ └── logical-group.component.ts │ │ ├── logical-group.module.ts │ │ └── services │ │ │ └── logical-group.service.ts │ │ ├── login │ │ ├── components │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ ├── login.component.spec.ts │ │ │ └── login.component.ts │ │ ├── login.module.ts │ │ └── services │ │ │ └── login.service.ts │ │ ├── logout │ │ ├── components │ │ │ ├── logout.component.html │ │ │ ├── logout.component.scss │ │ │ ├── logout.component.spec.ts │ │ │ └── logout.component.ts │ │ └── logout.module.ts │ │ ├── options │ │ ├── components │ │ │ ├── options.component.html │ │ │ ├── options.component.scss │ │ │ ├── options.component.spec.ts │ │ │ └── options.component.ts │ │ └── options.module.ts │ │ ├── topo-graph │ │ ├── components │ │ │ ├── topo-graph.component.html │ │ │ ├── topo-graph.component.scss │ │ │ ├── topo-graph.component.spec.ts │ │ │ └── topo-graph.component.ts │ │ ├── modules.ts │ │ └── services │ │ │ └── topo-graph.service.ts │ │ └── topologyGraph │ │ ├── components │ │ ├── index.ts │ │ ├── topologyGraph.component.html │ │ ├── topologyGraph.component.scss │ │ └── topologyGraph.component.ts │ │ ├── modules.ts │ │ └── services │ │ └── topologyGraph.service.ts ├── assets │ ├── .gitkeep │ └── images │ │ └── GitHub-Mark-32px.png ├── browserslist ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── json │ └── logicalGroup.json ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/ -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/fe465499996964cf7fdb64f8045724f7433e9617/LICENSE -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /build/build.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 VMware Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | set -o errexit 18 | set -o nounset 19 | 20 | if [ -z "${PKG}" ]; then 21 | echo "PKG must be set" 22 | exit 1 23 | fi 24 | if [ -z "${ARCH}" ]; then 25 | echo "ARCH must be set" 26 | exit 1 27 | fi 28 | if [ -z "${VERSION}" ]; then 29 | echo "VERSION must be set" 30 | exit 1 31 | fi 32 | 33 | export CGO_ENABLED=0 34 | export GOARCH="${ARCH}" 35 | 36 | go install \ 37 | -installsuffix "static" \ 38 | -ldflags "-X ${PKG}/version.VERSION=${VERSION}" \ 39 | ./... 40 | -------------------------------------------------------------------------------- /build/purser-binary-install.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 VMware Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | # Realease Version 18 | releaseVersion=v1.0.0 19 | 20 | # === Purser Plugin === 21 | 22 | # Detecting os type 23 | unameOut="$(uname -s)" 24 | case "${unameOut}" in 25 | Linux*) machine=Linux;; 26 | Darwin*) machine=Mac;; 27 | CYGWIN*) machine=Cygwin;; 28 | MINGW*) machine=MinGw;; 29 | *) machine="UNKNOWN:${unameOut}" 30 | esac 31 | echo "Detecting your Operating System: ${machine}" 32 | 33 | echo "Downloading files for plugin..." 34 | # Download purser plugin yaml 35 | pluginYamlUrl=https://github.com/vmware/purser/releases/download/$releaseVersion/plugin.yaml 36 | wget -q --show-progress -O plugin.yaml $pluginYamlUrl 37 | 38 | # Downloading purser plugin binary based on os type 39 | if [ $machine = Linux ] 40 | then 41 | pluginUrl=https://github.com/vmware/purser/releases/download/$releaseVersion/purser_plugin_linux_amd64 42 | elif [ $machine = Mac ] 43 | then 44 | pluginUrl=https://github.com/vmware/purser/releases/download/$releaseVersion/purser_plugin_darwin_amd64 45 | else 46 | echo "No match found for your os: $machine" 47 | echo "Install the plugin from source code: https://github.com/vmware/purser/blob/master/README.md" 48 | exit 3 # unsuccessful shell script 49 | fi 50 | wget -q --show-progress -O purser_plugin $pluginUrl 51 | 52 | # Move th plugin yaml to one of the location specified in 53 | # https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/ 54 | if [ ! -d $HOME/.kube/plugins ] 55 | then 56 | mkdir $HOME/.kube/plugins 57 | fi 58 | echo "Moving plugin.yaml to $HOME/.kube/plugins/" 59 | mv plugin.yaml $HOME/.kube/plugins/ 60 | 61 | # Change execution permissions for the binary 62 | chmod +x purser_plugin 63 | 64 | # Move the binary to a location which is in environment PATH variable 65 | echo "Moving the binary to /usr/local/bin" 66 | sudo mv purser_plugin /usr/local/bin 67 | 68 | echo "Purser plugin installation Completed" 69 | 70 | echo "" 71 | 72 | echo "Purser Installation Completed" -------------------------------------------------------------------------------- /build/purser-binary-uninstall.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 VMware Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | echo "Removing plugin.yaml from $HOME/.kube/plugins/" 17 | rm $HOME/.kube/plugins/plugin.yaml 18 | 19 | echo "Removing the binary from /usr/local/bin" 20 | sudo rm /usr/local/bin/purser_plugin 21 | -------------------------------------------------------------------------------- /build/purser-minimal-setup.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 VMware Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | # Realease Version 18 | releaseVersion=1.0.2 19 | 20 | echo "Installing Purser (minimal setup) version: ${releaseVersion}" 21 | 22 | # Namespace setup 23 | echo "Creating namespace purser" 24 | kubectl create ns purser 25 | 26 | # DB setup 27 | echo "Setting up database for Purser" 28 | curl https://raw.githubusercontent.com/vmware/purser/master/cluster/minimal/purser-database-setup.yaml -O 29 | kubectl --namespace=purser create -f purser-database-setup.yaml 30 | echo "Waiting for database containers to be in running state... (30s)" 31 | sleep 30s 32 | 33 | # Purser controller setup 34 | echo "Setting up controller for Purser" 35 | curl https://raw.githubusercontent.com/vmware/purser/master/cluster/minimal/purser-controller-setup.yaml -O 36 | kubectl --namespace=purser create -f purser-controller-setup.yaml 37 | 38 | # Purser UI setup 39 | echo "Setting up UI for Purser" 40 | curl https://raw.githubusercontent.com/vmware/purser/master/cluster/minimal/purser-ui-setup.yaml -O 41 | kubectl --namespace=purser create -f purser-ui-setup.yaml 42 | 43 | echo "Purser setup is completed" 44 | -------------------------------------------------------------------------------- /build/purser-setup.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 VMware Inc. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | # Kubeconfig location 18 | read -p "Location for cluster's configuration (Press 'Enter' to take default $HOME/.kube/config): " readConfig 19 | if [ -z "$readConfig" ]; 20 | then 21 | kubeConfig="$HOME/.kube/config" 22 | else 23 | kubeConfig=$readConfig 24 | fi 25 | 26 | # Realease Version 27 | releaseVersion=1.0.2 28 | echo "Installing Purser version: ${releaseVersion}" 29 | 30 | # Namespace setup 31 | echo "Creating namespace purser" 32 | kubectl --kubeconfig=$kubeConfig create ns purser 33 | 34 | # DB setup 35 | echo "Setting up database for Purser" 36 | curl https://raw.githubusercontent.com/vmware/purser/master/cluster/purser-database-setup.yaml -O 37 | kubectl --kubeconfig=$kubeConfig --namespace=purser create -f purser-database-setup.yaml 38 | echo "Waiting for database containers to be in running state... (1 minute)" 39 | sleep 60s 40 | 41 | # Purser controller setup 42 | echo "Setting up controller for Purser" 43 | curl https://raw.githubusercontent.com/vmware/purser/master/cluster/purser-controller-setup.yaml -O 44 | kubectl --kubeconfig=$kubeConfig --namespace=purser create -f purser-controller-setup.yaml 45 | 46 | # Purser UI setup 47 | echo "Setting up UI for Purser" 48 | curl https://raw.githubusercontent.com/vmware/purser/master/cluster/purser-ui-setup.yaml -O 49 | kubectl --kubeconfig=$kubeConfig --namespace=purser create -f purser-ui-setup.yaml 50 | 51 | echo "Purser setup is completed" 52 | -------------------------------------------------------------------------------- /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/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: -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /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/_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 | -------------------------------------------------------------------------------- /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/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 }} -------------------------------------------------------------------------------- /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 }} -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/fe465499996964cf7fdb64f8045724f7433e9617/docs/img/architecture.png -------------------------------------------------------------------------------- /docs/img/architecture01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/fe465499996964cf7fdb64f8045724f7433e9617/docs/img/architecture01.png -------------------------------------------------------------------------------- /docs/img/purser-cli.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/fe465499996964cf7fdb64f8045724f7433e9617/docs/img/purser-cli.gif -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ui/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:3030/", 4 | "changeOrigin": true, 5 | "secure":false 6 | } 7 | } -------------------------------------------------------------------------------- /ui/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{messages && messages.common.appHeader}} 5 |
6 |
7 |
8 | 47 |
48 |
-------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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/changepassword/components/changepassword.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/app/modules/changepassword/components/changepassword.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/fe465499996964cf7fdb64f8045724f7433e9617/ui/src/app/modules/changepassword/components/changepassword.component.scss -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/app/modules/login/components/login.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/fe465499996964cf7fdb64f8045724f7433e9617/ui/src/app/modules/login/components/login.component.scss -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 { } -------------------------------------------------------------------------------- /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/logout/components/logout.component.html: -------------------------------------------------------------------------------- 1 |

Logging out

-------------------------------------------------------------------------------- /ui/src/app/modules/logout/components/logout.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/fe465499996964cf7fdb64f8045724f7433e9617/ui/src/app/modules/logout/components/logout.component.scss -------------------------------------------------------------------------------- /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/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/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/components/options.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/app/modules/options/components/options.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/fe465499996964cf7fdb64f8045724f7433e9617/ui/src/app/modules/options/components/options.component.scss -------------------------------------------------------------------------------- /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/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/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 { } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | } -------------------------------------------------------------------------------- /ui/src/app/modules/topologyGraph/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './topologyGraph.component'; -------------------------------------------------------------------------------- /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/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/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/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 | } -------------------------------------------------------------------------------- /ui/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/fe465499996964cf7fdb64f8045724f7433e9617/ui/src/assets/.gitkeep -------------------------------------------------------------------------------- /ui/src/assets/images/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/purser/fe465499996964cf7fdb64f8045724f7433e9617/ui/src/assets/images/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /ui/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | --------------------------------------------------------------------------------