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