[UI] add tab for monthly costs per cluster (#796)

* add tab for monthly costs per cluster
* sync run_local and update version number
* lowering resources
* some Makefile polishing and updated admin docs on UI
* extend admin docs on UI
* add api-service manifest for operator
* set min limits in UI to default min limits of operator
* reflect new UI helm charts in docs
* make cluster name label configurable
This commit is contained in:
Felix Kunde
2020-02-19 12:58:24 +01:00
committed by GitHub
parent aea9e9bd33
commit d5660f65bb
17 changed files with 257 additions and 86 deletions
+6 -14
View File
@@ -1,17 +1,6 @@
.PHONY: clean test appjs docker push mock
BINARY ?= postgres-operator-ui
BUILD_FLAGS ?= -v
CGO_ENABLED ?= 0
ifeq ($(RACE),1)
BUILD_FLAGS += -race -a
CGO_ENABLED=1
endif
LOCAL_BUILD_FLAGS ?= $(BUILD_FLAGS)
LDFLAGS ?= -X=main.version=$(VERSION)
IMAGE ?= registry.opensource.zalan.do/acid/$(BINARY)
IMAGE ?= registry.opensource.zalan.do/acid/postgres-operator-ui
VERSION ?= $(shell git describe --tags --always --dirty)
TAG ?= $(VERSION)
GITHEAD = $(shell git rev-parse --short HEAD)
@@ -32,8 +21,11 @@ appjs:
docker run $(TTYFLAGS) -u $$(id -u) -v $$(pwd):/workdir -w /workdir/app node:10.1.0-alpine npm run build
docker: appjs
docker build --build-arg "VERSION=$(VERSION)" -t "$(IMAGE):$(TAG)" .
@echo 'Docker image $(IMAGE):$(TAG) can now be used.'
echo `(env)`
echo "Tag ${TAG}"
echo "Version ${VERSION}"
echo "git describe $(shell git describe --tags --always --dirty)"
docker build --rm -t "$(IMAGE):$(TAG)" -f Dockerfile .
push: docker
docker push "$(IMAGE):$(TAG)"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "postgres-operator-ui",
"version": "1.0.0",
"version": "1.3.0",
"description": "PostgreSQL Operator UI",
"main": "src/app.js",
"config": {
+8 -8
View File
@@ -408,7 +408,7 @@ new
ref='cpuLimit'
type='number'
placeholder='{ cpu.state.limit.initialValue }'
min='1'
min='250'
required
value='{ cpu.state.limit.state }'
onchange='{ cpu.state.limit.edit }'
@@ -434,7 +434,7 @@ new
onkeyup='{ memory.state.request.edit }'
)
.input-group-addon
.input-units Gi
.input-units Mi
.input-group
.input-group-addon.resource-type Limit
@@ -442,14 +442,14 @@ new
ref='memoryLimit'
type='number'
placeholder='{ memory.state.limit.initialValue }'
min='1'
min='250'
required
value='{ memory.state.limit.state }'
onchange='{ memory.state.limit.edit }'
onkeyup='{ memory.state.limit.edit }'
)
.input-group-addon
.input-units Gi
.input-units Mi
.col-lg-3
help-general(config='{ opts.config }')
@@ -519,10 +519,10 @@ new
resources:
requests:
cpu: {{ cpu.state.request.state }}m
memory: {{ memory.state.request.state }}Gi
memory: {{ memory.state.request.state }}Mi
limits:
cpu: {{ cpu.state.limit.state }}m
memory: {{ memory.state.limit.state }}Gi{{#if restoring}}
memory: {{ memory.state.limit.state }}Mi{{#if restoring}}
clone:
cluster: "{{ backup.state.name.state }}"
@@ -786,8 +786,8 @@ new
return instance
}
this.cpu = DynamicResource({ request: 100, limit: 1000 })
this.memory = DynamicResource({ request: 1, limit: 1 })
this.cpu = DynamicResource({ request: 100, limit: 500 })
this.memory = DynamicResource({ request: 100, limit: 500 })
this.backup = DynamicSet({
type: () => 'empty',
+3
View File
@@ -76,6 +76,9 @@ postgresql
.alert.alert-danger(if='{ progress.requestStatus !== "OK" }') Create request failed
.alert.alert-success(if='{ progress.requestStatus === "OK" }') Create request successful ({ new Date(progress.createdTimestamp).toLocaleString() })
.alert.alert-info(if='{ !progress.postgresql }') PostgreSQL cluster manifest pending
.alert.alert-success(if='{ progress.postgresql }') PostgreSQL cluster manifest created
.alert.alert-info(if='{ !progress.statefulSet }') StatefulSet pending
.alert.alert-success(if='{ progress.statefulSet }') StatefulSet created
+62 -17
View File
@@ -45,12 +45,14 @@ postgresqls
thead
tr
th(style='width: 120px') Team
th(style='width: 130px') Namespace
th Name
th(style='width: 50px') Pods
th(style='width: 140px') CPU
th(style='width: 130px') Memory
th(style='width: 100px') Size
th(style='width: 130px') Namespace
th Name
th(style='width: 120px') Cost/Month
th(stlye='width: 120px')
tbody
tr(
@@ -58,19 +60,21 @@ postgresqls
hidden='{ !namespaced_name.toLowerCase().includes(filter.state.toLowerCase()) }'
)
td { team }
td { nodes }
td { cpu } / { cpu_limit }
td { memory } / { memory_limit }
td { volume_size }
td(style='white-space: pre')
| { namespace }
td
a(
href='/#/status/{ cluster_path(this) }'
)
| { name }
td { nodes }
td { cpu } / { cpu_limit }
td { memory } / { memory_limit }
td { volume_size }
td { calcCosts(nodes, cpu, memory, volume_size) }$
td
.btn-group.pull-right(
aria-label='Cluster { qname } actions'
@@ -124,12 +128,14 @@ postgresqls
thead
tr
th(style='width: 120px') Team
th(style='width: 130px') Namespace
th Name
th(style='width: 50px') Pods
th(style='width: 140px') CPU
th(style='width: 130px') Memory
th(style='width: 100px') Size
th(style='width: 130px') Namespace
th Name
th(style='width: 120px') Cost/Month
th(stlye='width: 120px')
tbody
tr(
@@ -137,20 +143,20 @@ postgresqls
hidden='{ !namespaced_name.toLowerCase().includes(filter.state.toLowerCase()) }'
)
td { team }
td { nodes }
td { cpu } / { cpu_limit }
td { memory } / { memory_limit }
td { volume_size }
td(style='white-space: pre')
| { namespace }
td
a(
href='/#/status/{ cluster_path(this) }'
)
| { name }
td { nodes }
td { cpu } / { cpu_limit }
td { memory } / { memory_limit }
td { volume_size }
td { calcCosts(nodes, cpu, memory, volume_size) }$
td
.btn-group.pull-right(
aria-label='Cluster { qname } actions'
@@ -223,6 +229,45 @@ postgresqls
+ '/' + encodeURI(cluster.name)
)
const calcCosts = this.calcCosts = (nodes, cpu, memory, disk) => {
costs = nodes * (toCores(cpu) * opts.config.cost_core + toMemory(memory) * opts.config.cost_memory + toDisk(disk) * opts.config.cost_ebs)
return costs.toFixed(2)
}
const toDisk = this.toDisk = value => {
if(value.endsWith("Gi")) {
value = value.substring(0, value.length-2)
value = Number(value)
return value
}
return value
}
const toMemory = this.toMemory = value => {
if (value.endsWith("Mi")) {
value = value.substring(0, value.length-2)
value = Number(value) / 1000.
return value
}
else if(value.endsWith("Gi")) {
value = value.substring(0, value.length-2)
value = Number(value)
return value
}
return value
}
const toCores = this.toCores = value => {
if (value.endsWith("m")) {
value = value.substring(0, value.length-1)
value = Number(value) / 1000.
return value
}
return value
}
this.on('mount', () =>
jQuery
.get('/postgresqls')
+15 -8
View File
@@ -4,23 +4,23 @@ metadata:
name: "postgres-operator-ui"
namespace: "default"
labels:
application: "postgres-operator-ui"
name: "postgres-operator-ui"
team: "acid"
spec:
replicas: 1
selector:
matchLabels:
application: "postgres-operator-ui"
name: "postgres-operator-ui"
template:
metadata:
labels:
application: "postgres-operator-ui"
name: "postgres-operator-ui"
team: "acid"
spec:
serviceAccountName: postgres-operator-ui
containers:
- name: "service"
image: registry.opensource.zalan.do/acid/postgres-operator-ui:v1.2.0
image: registry.opensource.zalan.do/acid/postgres-operator-ui:v1.3.0
ports:
- containerPort: 8081
protocol: "TCP"
@@ -32,8 +32,8 @@ spec:
timeoutSeconds: 1
resources:
limits:
cpu: "300m"
memory: "3000Mi"
cpu: "200m"
memory: "200Mi"
requests:
cpu: "100m"
memory: "100Mi"
@@ -41,7 +41,9 @@ spec:
- name: "APP_URL"
value: "http://localhost:8081"
- name: "OPERATOR_API_URL"
value: "http://localhost:8080"
value: "http://postgres-operator:8080"
- name: "OPERATOR_CLUSTER_NAME_LABEL"
value: "cluster-name"
- name: "TARGET_NAMESPACE"
value: "default"
- name: "TEAMS"
@@ -60,9 +62,14 @@ spec:
"replica_load_balancer_visible": true,
"resources_visible": true,
"users_visible": true,
"cost_ebs": 0.119,
"cost_core": 0.0575,
"cost_memory": 0.014375,
"postgresql_versions": [
"12",
"11",
"10",
"9.6"
"9.6",
"9.5"
]
}
+12
View File
@@ -76,6 +76,7 @@ ACCESS_TOKEN_URL = getenv('ACCESS_TOKEN_URL')
TOKENINFO_URL = getenv('OAUTH2_TOKEN_INFO_URL')
OPERATOR_API_URL = getenv('OPERATOR_API_URL', 'http://postgres-operator')
OPERATOR_CLUSTER_NAME_LABEL = getenv('OPERATOR_CLUSTER_NAME_LABEL', 'cluster-name')
OPERATOR_UI_CONFIG = getenv('OPERATOR_UI_CONFIG', '{}')
OPERATOR_UI_MAINTENANCE_CHECK = getenv('OPERATOR_UI_MAINTENANCE_CHECK', '{}')
READ_ONLY_MODE = getenv('READ_ONLY_MODE', False) in [True, 'true']
@@ -84,6 +85,13 @@ SUPERUSER_TEAM = getenv('SUPERUSER_TEAM', 'acid')
TARGET_NAMESPACE = getenv('TARGET_NAMESPACE')
GOOGLE_ANALYTICS = getenv('GOOGLE_ANALYTICS', False)
# storage pricing, i.e. https://aws.amazon.com/ebs/pricing/
COST_EBS = float(getenv('COST_EBS', 0.119)) # GB per month
# compute costs, i.e. https://www.ec2instances.info/?region=eu-central-1&selected=m5.2xlarge
COST_CORE = 30.5 * 24 * float(getenv('COST_CORE', 0.0575)) # Core per hour m5.2xlarge / 8.
COST_MEMORY = 30.5 * 24 * float(getenv('COST_MEMORY', 0.014375)) # Memory GB m5.2xlarge / 32.
WALE_S3_ENDPOINT = getenv(
'WALE_S3_ENDPOINT',
'https+path://s3-eu-central-1.amazonaws.com:443',
@@ -293,6 +301,9 @@ DEFAULT_UI_CONFIG = {
'dns_format_string': '{0}.{1}.{2}',
'pgui_link': '',
'static_network_whitelist': {},
'cost_ebs': COST_EBS,
'cost_core': COST_CORE,
'cost_memory': COST_MEMORY
}
@@ -1003,6 +1014,7 @@ def main(port, secret_key, debug, clusters: list):
logger.info(f'App URL: {APP_URL}')
logger.info(f'Authorize URL: {AUTHORIZE_URL}')
logger.info(f'Operator API URL: {OPERATOR_API_URL}')
logger.info(f'Operator cluster name label: {OPERATOR_CLUSTER_NAME_LABEL}')
logger.info(f'Readonly mode: {"enabled" if READ_ONLY_MODE else "disabled"}') # noqa
logger.info(f'Spilo S3 backup bucket: {SPILO_S3_BACKUP_BUCKET}')
logger.info(f'Spilo S3 backup prefix: {SPILO_S3_BACKUP_PREFIX}')
+4 -2
View File
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
from furl import furl
from json import dumps
from logging import getLogger
from os import environ
from os import environ, getenv
from requests import Session
from urllib.parse import urljoin
from uuid import UUID
@@ -16,6 +16,8 @@ logger = getLogger(__name__)
session = Session()
OPERATOR_CLUSTER_NAME_LABEL = getenv('OPERATOR_CLUSTER_NAME_LABEL', 'cluster-name')
def request(cluster, path, **kwargs):
if 'timeout' not in kwargs:
@@ -137,7 +139,7 @@ def read_pods(cluster, namespace, spilo_cluster):
cluster=cluster,
resource_type='pods',
namespace=namespace,
label_selector={'version': spilo_cluster},
label_selector={OPERATOR_CLUSTER_NAME_LABEL: spilo_cluster},
)
+7 -7
View File
@@ -1,14 +1,14 @@
Flask-OAuthlib==0.9.5
Flask==1.0.2
backoff==1.5.0
boto3==1.5.14
boto==2.48.0
Flask==1.1.1
backoff==1.8.1
boto3==1.10.4
boto==2.49.0
click==6.7
furl==1.0.1
furl==1.0.2
gevent==1.2.2
jq==0.1.6
json_delta>=2.0
kubernetes==3.0.0
requests==2.20.1
requests==2.22.0
stups-tokens>=1.1.19
wal_e==1.1.0
wal_e==1.1.0
+6 -1
View File
@@ -19,10 +19,15 @@ default_operator_ui_config='{
"nat_gateways_visible": false,
"resources_visible": true,
"users_visible": true,
"cost_ebs": 0.119,
"cost_core": 0.0575,
"cost_memory": 0.014375,
"postgresql_versions": [
"12",
"11",
"10",
"9.6"
"9.6",
"9.5"
],
"static_network_whitelist": {
"localhost": ["172.0.0.1/32"]