[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:
		
							parent
							
								
									aea9e9bd33
								
							
						
					
					
						commit
						d5660f65bb
					
				|  | @ -1,7 +1,7 @@ | ||||||
| apiVersion: v1 | apiVersion: v1 | ||||||
| name: postgres-operator-ui | name: postgres-operator-ui | ||||||
| version: 0.1.0 | version: 0.1.0 | ||||||
| appVersion: 1.2.0 | appVersion: 1.3.0 | ||||||
| home: https://github.com/zalando/postgres-operator | home: https://github.com/zalando/postgres-operator | ||||||
| description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience | description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience | ||||||
| keywords: | keywords: | ||||||
|  | @ -12,6 +12,8 @@ keywords: | ||||||
| - patroni | - patroni | ||||||
| - spilo | - spilo | ||||||
| maintainers: | maintainers: | ||||||
|  | - name: Zalando | ||||||
|  |   email: opensource@zalando.de | ||||||
| - name: siku4 | - name: siku4 | ||||||
|   email: sk@sik-net.de |   email: sk@sik-net.de | ||||||
| sources: | sources: | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ metadata: | ||||||
|     app.kubernetes.io/instance: {{ .Release.Name }} |     app.kubernetes.io/instance: {{ .Release.Name }} | ||||||
|   name: {{ template "postgres-operator.fullname" . }} |   name: {{ template "postgres-operator.fullname" . }} | ||||||
| spec: | spec: | ||||||
|  |   type: ClusterIP | ||||||
|   ports: |   ports: | ||||||
|   - port: 8080 |   - port: 8080 | ||||||
|     protocol: TCP |     protocol: TCP | ||||||
|  | @ -15,7 +16,3 @@ spec: | ||||||
|   selector: |   selector: | ||||||
|     app.kubernetes.io/instance: {{ .Release.Name }} |     app.kubernetes.io/instance: {{ .Release.Name }} | ||||||
|     app.kubernetes.io/name: {{ template "postgres-operator.name" . }} |     app.kubernetes.io/name: {{ template "postgres-operator.name" . }} | ||||||
|   sessionAffinity: None |  | ||||||
|   type: ClusterIP |  | ||||||
| status: |  | ||||||
|   loadBalancer: {} |  | ||||||
|  | @ -480,37 +480,71 @@ A secret can be pre-provisioned in different ways: | ||||||
| 
 | 
 | ||||||
| ## Setting up the Postgres Operator UI | ## Setting up the Postgres Operator UI | ||||||
| 
 | 
 | ||||||
| With the v1.2 release the Postgres Operator is shipped with a browser-based | Since the v1.2 release the Postgres Operator is shipped with a browser-based | ||||||
| configuration user interface (UI) that simplifies managing Postgres clusters | configuration user interface (UI) that simplifies managing Postgres clusters | ||||||
| with the operator. The UI runs with Node.js and comes with it's own Docker | with the operator. | ||||||
| image. |  | ||||||
| 
 | 
 | ||||||
| Run NPM to continuously compile `tags/js` code. Basically, it creates an | ### Building the UI image | ||||||
| `app.js` file in: `static/build/app.js` |  | ||||||
| 
 | 
 | ||||||
| ``` | The UI runs with Node.js and comes with it's own Docker | ||||||
| (cd ui/app && npm start) | image. However, installing Node.js to build the operator UI is not required. It | ||||||
| ``` | is handled via Docker containers when running: | ||||||
| 
 |  | ||||||
| To build the Docker image open a shell and change to the `ui` folder. Then run: |  | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
| docker build -t registry.opensource.zalan.do/acid/postgres-operator-ui:v1.2.0 . | make docker | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Apply all manifests for the `ui/manifests` folder to deploy the Postgres | ### Configure endpoints and options | ||||||
| Operator UI on K8s. For local tests you don't need the Ingress resource. | 
 | ||||||
|  | The UI talks to the K8s API server as well as the Postgres Operator [REST API](developer.md#debugging-the-operator). | ||||||
|  | K8s API server URLs are loaded from the machine's kubeconfig environment by | ||||||
|  | default. Alternatively, a list can also be passed when starting the Python | ||||||
|  | application with the `--cluster` option. | ||||||
|  | 
 | ||||||
|  | The Operator API endpoint can be configured via the `OPERATOR_API_URL` | ||||||
|  | environment variables in the [deployment manifest](../ui/manifests/deployment.yaml#L40). | ||||||
|  | You can also expose the operator API through a [service](../manifests/api-service.yaml). | ||||||
|  | Some displayed options can be disabled from UI using simple flags under the | ||||||
|  | `OPERATOR_UI_CONFIG` field in the deployment. | ||||||
|  | 
 | ||||||
|  | ### Deploy the UI on K8s | ||||||
|  | 
 | ||||||
|  | Now, apply all manifests from the `ui/manifests` folder to deploy the Postgres | ||||||
|  | Operator UI on K8s. Replace the image tag in the deployment manifest if you | ||||||
|  | want to test the image you've built with `make docker`. Make sure the pods for | ||||||
|  | the operator and the UI are both running. | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
| kubectl apply -f ui/manifests | sed -e "s/\(image\:.*\:\).*$/\1$TAG/" manifests/deployment.yaml | kubectl apply -f manifests/ | ||||||
|  | kubectl get all -l application=postgres-operator-ui | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Make sure the pods for the operator and the UI are both running. For local | ### Local testing | ||||||
| testing you need to apply proxying and port forwarding so that the UI can talk | 
 | ||||||
| to the K8s and Postgres Operator REST API. You can use the provided | For local testing you need to apply K8s proxying and operator pod port | ||||||
| `run_local.sh` script for this. Make sure it uses the correct URL to your K8s | forwarding so that the UI can talk to the K8s and Postgres Operator REST API. | ||||||
| API server, e.g. for minikube it would be `https://192.168.99.100:8443`. | The Ingress resource is not needed. You can use the provided `run_local.sh` | ||||||
|  | script for this. Make sure that: | ||||||
|  | 
 | ||||||
|  | * Python dependencies are installed on your machine | ||||||
|  | * the K8s API server URL is set for kubectl commands, e.g. for minikube it would usually be `https://192.168.99.100:8443`. | ||||||
|  | * the pod label selectors for port forwarding are correct | ||||||
|  | 
 | ||||||
|  | When testing with minikube you have to build the image in its docker environment | ||||||
|  | (running `make docker` doesn't do it for you). From the `ui` directory execute: | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
|  | # compile and build operator UI | ||||||
|  | make docker | ||||||
|  | 
 | ||||||
|  | # build in image in minikube docker env | ||||||
|  | eval $(minikube docker-env) | ||||||
|  | docker build -t registry.opensource.zalan.do/acid/postgres-operator-ui:v1.3.0 . | ||||||
|  | 
 | ||||||
|  | # apply UI manifests next to a running Postgres Operator | ||||||
|  | kubectl apply -f manifests/ | ||||||
|  | 
 | ||||||
|  | # install python dependencies to run UI locally | ||||||
|  | pip3 install -r requirements | ||||||
| ./run_local.sh | ./run_local.sh | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | @ -31,9 +31,13 @@ status page. | ||||||
|  |  | ||||||
| 
 | 
 | ||||||
| Usually, the startup should only take up to 1 minute. If you feel the process | Usually, the startup should only take up to 1 minute. If you feel the process | ||||||
| got stuck click on the "Logs" button to inspect the operator logs. From the | got stuck click on the "Logs" button to inspect the operator logs. If the logs | ||||||
| "Status" field in the top menu you can also retrieve the logs and queue of each | look fine, but the UI seems to got stuck, check if you are have configured the | ||||||
| worker the operator is using. The number of concurrent workers can be | same [cluster name label](../ui/manifests/deployment.yaml#L45) like for the | ||||||
|  | [operator](../manifests/configmap.yaml#L13). | ||||||
|  | 
 | ||||||
|  | From the "Status" field in the top menu you can also retrieve the logs and queue | ||||||
|  | of each worker the operator is using. The number of concurrent workers can be | ||||||
| [configured](reference/operator_parameters.md#general). | [configured](reference/operator_parameters.md#general). | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @ -52,6 +52,7 @@ cd postgres-operator | ||||||
| kubectl create -f manifests/configmap.yaml  # configuration | kubectl create -f manifests/configmap.yaml  # configuration | ||||||
| kubectl create -f manifests/operator-service-account-rbac.yaml  # identity and permissions | kubectl create -f manifests/operator-service-account-rbac.yaml  # identity and permissions | ||||||
| kubectl create -f manifests/postgres-operator.yaml  # deployment | kubectl create -f manifests/postgres-operator.yaml  # deployment | ||||||
|  | kubectl create -f manifests/api-service.yaml  # operator API to be used by UI | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| There is a [Kustomization](https://github.com/kubernetes-sigs/kustomize) | There is a [Kustomization](https://github.com/kubernetes-sigs/kustomize) | ||||||
|  | @ -104,7 +105,7 @@ kubectl create -f https://operatorhub.io/install/postgres-operator.yaml | ||||||
| This installs the operator in the `operators` namespace. More information can be | This installs the operator in the `operators` namespace. More information can be | ||||||
| found on [operatorhub.io](https://operatorhub.io/operator/postgres-operator). | found on [operatorhub.io](https://operatorhub.io/operator/postgres-operator). | ||||||
| 
 | 
 | ||||||
| ## Create a Postgres cluster | ## Check if Postgres Operator is running | ||||||
| 
 | 
 | ||||||
| Starting the operator may take a few seconds. Check if the operator pod is | Starting the operator may take a few seconds. Check if the operator pod is | ||||||
| running before applying a Postgres cluster manifest. | running before applying a Postgres cluster manifest. | ||||||
|  | @ -115,7 +116,61 @@ kubectl get pod -l name=postgres-operator | ||||||
| 
 | 
 | ||||||
| # if you've created the operator using helm chart | # if you've created the operator using helm chart | ||||||
| kubectl get pod -l app.kubernetes.io/name=postgres-operator | kubectl get pod -l app.kubernetes.io/name=postgres-operator | ||||||
|  | ``` | ||||||
| 
 | 
 | ||||||
|  | If the operator doesn't get into `Running` state, either check the latest K8s | ||||||
|  | events of the deployment or pod with `kubectl describe` or inspect the operator | ||||||
|  | logs: | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | kubectl logs "$(kubectl get pod -l name=postgres-operator --output='name')" | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Deploy the operator UI | ||||||
|  | 
 | ||||||
|  | In the following paragraphs we describe how to access and manage PostgreSQL | ||||||
|  | clusters from the command line with kubectl. But it can also be done from the | ||||||
|  | browser-based [Postgres Operator UI](operator-ui.md). Before deploying the UI | ||||||
|  | make sure the operator is running and its REST API is reachable through a | ||||||
|  | [K8s service](../manifests/api-service.yaml). The URL to this API must be | ||||||
|  | configured in the [deployment manifest](../ui/manifests/deployment.yaml#L43) | ||||||
|  | of the UI. | ||||||
|  | 
 | ||||||
|  | To deploy the UI simply apply all its manifests files or use the UI helm chart: | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | # manual deployment | ||||||
|  | kubectl apply -f ui/manifests/ | ||||||
|  | 
 | ||||||
|  | # or helm chart | ||||||
|  | helm install postgres-operator-ui ./charts/postgres-operator-ui | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Like with the operator, check if the UI pod gets into `Running` state: | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | # if you've created the operator using yaml manifests | ||||||
|  | kubectl get pod -l name=postgres-operator-ui | ||||||
|  | 
 | ||||||
|  | # if you've created the operator using helm chart | ||||||
|  | kubectl get pod -l app.kubernetes.io/name=postgres-operator-ui | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | You can now access the web interface by port forwarding the UI pod (mind the | ||||||
|  | label selector) and enter `localhost:8081` in your browser: | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | kubectl port-forward "$(kubectl get pod -l name=postgres-operator-ui --output='name')" 8081 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Available option are explained in detail in the [UI docs](operator-ui.md). | ||||||
|  | 
 | ||||||
|  | ## Create a Postgres cluster | ||||||
|  | 
 | ||||||
|  | If the operator pod is running it listens to new events regarding `postgresql` | ||||||
|  | resources. Now, it's time to submit your first Postgres cluster manifest. | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
| # create a Postgres cluster | # create a Postgres cluster | ||||||
| kubectl create -f manifests/minimal-postgres-manifest.yaml | kubectl create -f manifests/minimal-postgres-manifest.yaml | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | @ -0,0 +1,12 @@ | ||||||
|  | apiVersion: v1 | ||||||
|  | kind: Service | ||||||
|  | metadata: | ||||||
|  |   name: postgres-operator | ||||||
|  | spec: | ||||||
|  |   type: ClusterIP | ||||||
|  |   ports: | ||||||
|  |   - port: 8080 | ||||||
|  |     protocol: TCP | ||||||
|  |     targetPort: 8080 | ||||||
|  |   selector: | ||||||
|  |     name: postgres-operator | ||||||
|  | @ -4,3 +4,4 @@ resources: | ||||||
| - configmap.yaml | - configmap.yaml | ||||||
| - operator-service-account-rbac.yaml | - operator-service-account-rbac.yaml | ||||||
| - postgres-operator.yaml | - postgres-operator.yaml | ||||||
|  | - api-service.yaml | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								ui/Makefile
								
								
								
								
							
							
						
						
									
										20
									
								
								ui/Makefile
								
								
								
								
							|  | @ -1,17 +1,6 @@ | ||||||
| .PHONY: clean test appjs docker push mock | .PHONY: clean test appjs docker push mock | ||||||
| 
 | 
 | ||||||
| BINARY ?= postgres-operator-ui | IMAGE            ?= registry.opensource.zalan.do/acid/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) |  | ||||||
| VERSION          ?= $(shell git describe --tags --always --dirty) | VERSION          ?= $(shell git describe --tags --always --dirty) | ||||||
| TAG              ?= $(VERSION) | TAG              ?= $(VERSION) | ||||||
| GITHEAD          = $(shell git rev-parse --short HEAD) | 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 run $(TTYFLAGS) -u $$(id -u) -v $$(pwd):/workdir -w /workdir/app node:10.1.0-alpine npm run build | ||||||
| 
 | 
 | ||||||
| docker: appjs | docker: appjs | ||||||
| 	docker build --build-arg "VERSION=$(VERSION)" -t "$(IMAGE):$(TAG)" . | 	echo `(env)` | ||||||
| 	@echo 'Docker image $(IMAGE):$(TAG) can now be used.' | 	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 | push: docker | ||||||
| 	docker push "$(IMAGE):$(TAG)" | 	docker push "$(IMAGE):$(TAG)" | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|   "name": "postgres-operator-ui", |   "name": "postgres-operator-ui", | ||||||
|   "version": "1.0.0", |   "version": "1.3.0", | ||||||
|   "description": "PostgreSQL Operator UI", |   "description": "PostgreSQL Operator UI", | ||||||
|   "main": "src/app.js", |   "main": "src/app.js", | ||||||
|   "config": { |   "config": { | ||||||
|  |  | ||||||
|  | @ -408,7 +408,7 @@ new | ||||||
|                           ref='cpuLimit' |                           ref='cpuLimit' | ||||||
|                           type='number' |                           type='number' | ||||||
|                           placeholder='{ cpu.state.limit.initialValue }' |                           placeholder='{ cpu.state.limit.initialValue }' | ||||||
|                           min='1' |                           min='250' | ||||||
|                           required |                           required | ||||||
|                           value='{ cpu.state.limit.state }' |                           value='{ cpu.state.limit.state }' | ||||||
|                           onchange='{ cpu.state.limit.edit }' |                           onchange='{ cpu.state.limit.edit }' | ||||||
|  | @ -434,7 +434,7 @@ new | ||||||
|                           onkeyup='{ memory.state.request.edit }' |                           onkeyup='{ memory.state.request.edit }' | ||||||
|                         ) |                         ) | ||||||
|                         .input-group-addon |                         .input-group-addon | ||||||
|                           .input-units Gi |                           .input-units Mi | ||||||
| 
 | 
 | ||||||
|                       .input-group |                       .input-group | ||||||
|                         .input-group-addon.resource-type Limit |                         .input-group-addon.resource-type Limit | ||||||
|  | @ -442,14 +442,14 @@ new | ||||||
|                           ref='memoryLimit' |                           ref='memoryLimit' | ||||||
|                           type='number' |                           type='number' | ||||||
|                           placeholder='{ memory.state.limit.initialValue }' |                           placeholder='{ memory.state.limit.initialValue }' | ||||||
|                           min='1' |                           min='250' | ||||||
|                           required |                           required | ||||||
|                           value='{ memory.state.limit.state }' |                           value='{ memory.state.limit.state }' | ||||||
|                           onchange='{ memory.state.limit.edit }' |                           onchange='{ memory.state.limit.edit }' | ||||||
|                           onkeyup='{ memory.state.limit.edit }' |                           onkeyup='{ memory.state.limit.edit }' | ||||||
|                         ) |                         ) | ||||||
|                         .input-group-addon |                         .input-group-addon | ||||||
|                           .input-units Gi |                           .input-units Mi | ||||||
| 
 | 
 | ||||||
|       .col-lg-3 |       .col-lg-3 | ||||||
|         help-general(config='{ opts.config }') |         help-general(config='{ opts.config }') | ||||||
|  | @ -519,10 +519,10 @@ new | ||||||
|         resources: |         resources: | ||||||
|           requests: |           requests: | ||||||
|             cpu: {{ cpu.state.request.state }}m |             cpu: {{ cpu.state.request.state }}m | ||||||
|             memory: {{ memory.state.request.state }}Gi |             memory: {{ memory.state.request.state }}Mi | ||||||
|           limits: |           limits: | ||||||
|             cpu: {{ cpu.state.limit.state }}m |             cpu: {{ cpu.state.limit.state }}m | ||||||
|             memory: {{ memory.state.limit.state }}Gi{{#if restoring}} |             memory: {{ memory.state.limit.state }}Mi{{#if restoring}} | ||||||
| 
 | 
 | ||||||
|         clone: |         clone: | ||||||
|           cluster: "{{ backup.state.name.state }}" |           cluster: "{{ backup.state.name.state }}" | ||||||
|  | @ -786,8 +786,8 @@ new | ||||||
|       return instance |       return instance | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.cpu = DynamicResource({ request: 100, limit: 1000 }) |     this.cpu = DynamicResource({ request: 100, limit: 500 }) | ||||||
|     this.memory = DynamicResource({ request: 1, limit: 1 }) |     this.memory = DynamicResource({ request: 100, limit: 500 }) | ||||||
| 
 | 
 | ||||||
|     this.backup = DynamicSet({ |     this.backup = DynamicSet({ | ||||||
|       type: () => 'empty', |       type: () => 'empty', | ||||||
|  |  | ||||||
|  | @ -76,6 +76,9 @@ postgresql | ||||||
|         .alert.alert-danger(if='{ progress.requestStatus !== "OK" }') Create request failed |         .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-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-info(if='{ !progress.statefulSet }') StatefulSet pending | ||||||
|         .alert.alert-success(if='{ progress.statefulSet }') StatefulSet created |         .alert.alert-success(if='{ progress.statefulSet }') StatefulSet created | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -45,12 +45,14 @@ postgresqls | ||||||
|         thead |         thead | ||||||
|           tr |           tr | ||||||
|             th(style='width: 120px') Team |             th(style='width: 120px') Team | ||||||
|  |             th(style='width: 130px') Namespace | ||||||
|  |             th Name | ||||||
|             th(style='width: 50px') Pods |             th(style='width: 50px') Pods | ||||||
|             th(style='width: 140px') CPU |             th(style='width: 140px') CPU | ||||||
|             th(style='width: 130px') Memory |             th(style='width: 130px') Memory | ||||||
|             th(style='width: 100px') Size |             th(style='width: 100px') Size | ||||||
|             th(style='width: 130px') Namespace |             th(style='width: 120px') Cost/Month | ||||||
|             th Name |             th(stlye='width: 120px') | ||||||
| 
 | 
 | ||||||
|         tbody |         tbody | ||||||
|           tr( |           tr( | ||||||
|  | @ -58,19 +60,21 @@ postgresqls | ||||||
|             hidden='{ !namespaced_name.toLowerCase().includes(filter.state.toLowerCase()) }' |             hidden='{ !namespaced_name.toLowerCase().includes(filter.state.toLowerCase()) }' | ||||||
|           ) |           ) | ||||||
|             td { team } |             td { team } | ||||||
|             td { nodes } |  | ||||||
|             td { cpu } / { cpu_limit } |  | ||||||
|             td { memory } / { memory_limit } |  | ||||||
|             td { volume_size } |  | ||||||
| 
 |  | ||||||
|             td(style='white-space: pre') |             td(style='white-space: pre') | ||||||
|               | { namespace } |               | { namespace } | ||||||
| 
 |  | ||||||
|             td |             td | ||||||
|               a( |               a( | ||||||
|                 href='/#/status/{ cluster_path(this) }' |                 href='/#/status/{ cluster_path(this) }' | ||||||
|               ) |               ) | ||||||
|                 | { name } |                 | { 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( |               .btn-group.pull-right( | ||||||
|                 aria-label='Cluster { qname } actions' |                 aria-label='Cluster { qname } actions' | ||||||
|  | @ -124,12 +128,14 @@ postgresqls | ||||||
|         thead |         thead | ||||||
|           tr |           tr | ||||||
|             th(style='width: 120px') Team |             th(style='width: 120px') Team | ||||||
|  |             th(style='width: 130px') Namespace | ||||||
|  |             th Name | ||||||
|             th(style='width: 50px') Pods |             th(style='width: 50px') Pods | ||||||
|             th(style='width: 140px') CPU |             th(style='width: 140px') CPU | ||||||
|             th(style='width: 130px') Memory |             th(style='width: 130px') Memory | ||||||
|             th(style='width: 100px') Size |             th(style='width: 100px') Size | ||||||
|             th(style='width: 130px') Namespace |             th(style='width: 120px') Cost/Month | ||||||
|             th Name |             th(stlye='width: 120px') | ||||||
| 
 | 
 | ||||||
|         tbody |         tbody | ||||||
|           tr( |           tr( | ||||||
|  | @ -137,20 +143,20 @@ postgresqls | ||||||
|             hidden='{ !namespaced_name.toLowerCase().includes(filter.state.toLowerCase()) }' |             hidden='{ !namespaced_name.toLowerCase().includes(filter.state.toLowerCase()) }' | ||||||
|           ) |           ) | ||||||
|             td { team } |             td { team } | ||||||
|             td { nodes } |  | ||||||
|             td { cpu } / { cpu_limit } |  | ||||||
|             td { memory } / { memory_limit } |  | ||||||
|             td { volume_size } |  | ||||||
| 
 |  | ||||||
|             td(style='white-space: pre') |             td(style='white-space: pre') | ||||||
|               | { namespace } |               | { namespace } | ||||||
| 
 |  | ||||||
|             td |             td | ||||||
| 
 |  | ||||||
|               a( |               a( | ||||||
|                 href='/#/status/{ cluster_path(this) }' |                 href='/#/status/{ cluster_path(this) }' | ||||||
|               ) |               ) | ||||||
|                 | { name } |                 | { 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( |               .btn-group.pull-right( | ||||||
|                 aria-label='Cluster { qname } actions' |                 aria-label='Cluster { qname } actions' | ||||||
|  | @ -223,6 +229,45 @@ postgresqls | ||||||
|       + '/' + encodeURI(cluster.name) |       + '/' + 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', () => |     this.on('mount', () => | ||||||
|       jQuery |       jQuery | ||||||
|       .get('/postgresqls') |       .get('/postgresqls') | ||||||
|  |  | ||||||
|  | @ -4,23 +4,23 @@ metadata: | ||||||
|   name: "postgres-operator-ui" |   name: "postgres-operator-ui" | ||||||
|   namespace: "default" |   namespace: "default" | ||||||
|   labels: |   labels: | ||||||
|     application: "postgres-operator-ui" |     name: "postgres-operator-ui" | ||||||
|     team: "acid" |     team: "acid" | ||||||
| spec: | spec: | ||||||
|   replicas: 1 |   replicas: 1 | ||||||
|   selector: |   selector: | ||||||
|     matchLabels: |     matchLabels: | ||||||
|       application: "postgres-operator-ui" |       name: "postgres-operator-ui" | ||||||
|   template: |   template: | ||||||
|     metadata: |     metadata: | ||||||
|       labels: |       labels: | ||||||
|         application: "postgres-operator-ui" |         name: "postgres-operator-ui" | ||||||
|         team: "acid" |         team: "acid" | ||||||
|     spec: |     spec: | ||||||
|       serviceAccountName: postgres-operator-ui |       serviceAccountName: postgres-operator-ui | ||||||
|       containers: |       containers: | ||||||
|         - name: "service" |         - 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: |           ports: | ||||||
|             - containerPort: 8081 |             - containerPort: 8081 | ||||||
|               protocol: "TCP" |               protocol: "TCP" | ||||||
|  | @ -32,8 +32,8 @@ spec: | ||||||
|             timeoutSeconds: 1 |             timeoutSeconds: 1 | ||||||
|           resources: |           resources: | ||||||
|             limits: |             limits: | ||||||
|               cpu: "300m" |               cpu: "200m" | ||||||
|               memory: "3000Mi" |               memory: "200Mi" | ||||||
|             requests: |             requests: | ||||||
|               cpu: "100m" |               cpu: "100m" | ||||||
|               memory: "100Mi" |               memory: "100Mi" | ||||||
|  | @ -41,7 +41,9 @@ spec: | ||||||
|             - name: "APP_URL" |             - name: "APP_URL" | ||||||
|               value: "http://localhost:8081" |               value: "http://localhost:8081" | ||||||
|             - name: "OPERATOR_API_URL" |             - name: "OPERATOR_API_URL" | ||||||
|               value: "http://localhost:8080" |               value: "http://postgres-operator:8080" | ||||||
|  |             - name: "OPERATOR_CLUSTER_NAME_LABEL" | ||||||
|  |               value: "cluster-name" | ||||||
|             - name: "TARGET_NAMESPACE" |             - name: "TARGET_NAMESPACE" | ||||||
|               value: "default" |               value: "default" | ||||||
|             - name: "TEAMS" |             - name: "TEAMS" | ||||||
|  | @ -60,9 +62,14 @@ spec: | ||||||
|                   "replica_load_balancer_visible": true, |                   "replica_load_balancer_visible": true, | ||||||
|                   "resources_visible": true, |                   "resources_visible": true, | ||||||
|                   "users_visible": true, |                   "users_visible": true, | ||||||
|  |                   "cost_ebs": 0.119, | ||||||
|  |                   "cost_core": 0.0575, | ||||||
|  |                   "cost_memory": 0.014375, | ||||||
|                   "postgresql_versions": [ |                   "postgresql_versions": [ | ||||||
|  |                     "12", | ||||||
|                     "11", |                     "11", | ||||||
|                     "10", |                     "10", | ||||||
|                     "9.6" |                     "9.6", | ||||||
|  |                     "9.5" | ||||||
|                   ] |                   ] | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  | @ -76,6 +76,7 @@ ACCESS_TOKEN_URL = getenv('ACCESS_TOKEN_URL') | ||||||
| TOKENINFO_URL = getenv('OAUTH2_TOKEN_INFO_URL') | TOKENINFO_URL = getenv('OAUTH2_TOKEN_INFO_URL') | ||||||
| 
 | 
 | ||||||
| OPERATOR_API_URL = getenv('OPERATOR_API_URL', 'http://postgres-operator') | 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_CONFIG = getenv('OPERATOR_UI_CONFIG', '{}') | ||||||
| OPERATOR_UI_MAINTENANCE_CHECK = getenv('OPERATOR_UI_MAINTENANCE_CHECK', '{}') | OPERATOR_UI_MAINTENANCE_CHECK = getenv('OPERATOR_UI_MAINTENANCE_CHECK', '{}') | ||||||
| READ_ONLY_MODE = getenv('READ_ONLY_MODE', False) in [True, 'true'] | 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') | TARGET_NAMESPACE = getenv('TARGET_NAMESPACE') | ||||||
| GOOGLE_ANALYTICS = getenv('GOOGLE_ANALYTICS', False) | 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 = getenv( | ||||||
|     'WALE_S3_ENDPOINT', |     'WALE_S3_ENDPOINT', | ||||||
|     'https+path://s3-eu-central-1.amazonaws.com:443', |     'https+path://s3-eu-central-1.amazonaws.com:443', | ||||||
|  | @ -293,6 +301,9 @@ DEFAULT_UI_CONFIG = { | ||||||
|     'dns_format_string': '{0}.{1}.{2}', |     'dns_format_string': '{0}.{1}.{2}', | ||||||
|     'pgui_link': '', |     'pgui_link': '', | ||||||
|     'static_network_whitelist': {}, |     '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'App URL: {APP_URL}') | ||||||
|     logger.info(f'Authorize URL: {AUTHORIZE_URL}') |     logger.info(f'Authorize URL: {AUTHORIZE_URL}') | ||||||
|     logger.info(f'Operator API URL: {OPERATOR_API_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'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 bucket: {SPILO_S3_BACKUP_BUCKET}') | ||||||
|     logger.info(f'Spilo S3 backup prefix: {SPILO_S3_BACKUP_PREFIX}') |     logger.info(f'Spilo S3 backup prefix: {SPILO_S3_BACKUP_PREFIX}') | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ from datetime import datetime, timezone | ||||||
| from furl import furl | from furl import furl | ||||||
| from json import dumps | from json import dumps | ||||||
| from logging import getLogger | from logging import getLogger | ||||||
| from os import environ | from os import environ, getenv | ||||||
| from requests import Session | from requests import Session | ||||||
| from urllib.parse import urljoin | from urllib.parse import urljoin | ||||||
| from uuid import UUID | from uuid import UUID | ||||||
|  | @ -16,6 +16,8 @@ logger = getLogger(__name__) | ||||||
| 
 | 
 | ||||||
| session = Session() | session = Session() | ||||||
| 
 | 
 | ||||||
|  | OPERATOR_CLUSTER_NAME_LABEL = getenv('OPERATOR_CLUSTER_NAME_LABEL', 'cluster-name') | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def request(cluster, path, **kwargs): | def request(cluster, path, **kwargs): | ||||||
|     if 'timeout' not in kwargs: |     if 'timeout' not in kwargs: | ||||||
|  | @ -137,7 +139,7 @@ def read_pods(cluster, namespace, spilo_cluster): | ||||||
|         cluster=cluster, |         cluster=cluster, | ||||||
|         resource_type='pods', |         resource_type='pods', | ||||||
|         namespace=namespace, |         namespace=namespace, | ||||||
|         label_selector={'version': spilo_cluster}, |         label_selector={OPERATOR_CLUSTER_NAME_LABEL: spilo_cluster}, | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,14 +1,14 @@ | ||||||
| Flask-OAuthlib==0.9.5 | Flask-OAuthlib==0.9.5 | ||||||
| Flask==1.0.2 | Flask==1.1.1 | ||||||
| backoff==1.5.0 | backoff==1.8.1 | ||||||
| boto3==1.5.14 | boto3==1.10.4 | ||||||
| boto==2.48.0 | boto==2.49.0 | ||||||
| click==6.7 | click==6.7 | ||||||
| furl==1.0.1 | furl==1.0.2 | ||||||
| gevent==1.2.2 | gevent==1.2.2 | ||||||
| jq==0.1.6 | jq==0.1.6 | ||||||
| json_delta>=2.0 | json_delta>=2.0 | ||||||
| kubernetes==3.0.0 | kubernetes==3.0.0 | ||||||
| requests==2.20.1 | requests==2.22.0 | ||||||
| stups-tokens>=1.1.19 | stups-tokens>=1.1.19 | ||||||
| wal_e==1.1.0 | wal_e==1.1.0 | ||||||
|  | @ -19,10 +19,15 @@ default_operator_ui_config='{ | ||||||
|   "nat_gateways_visible": false, |   "nat_gateways_visible": false, | ||||||
|   "resources_visible": true, |   "resources_visible": true, | ||||||
|   "users_visible": true, |   "users_visible": true, | ||||||
|  |   "cost_ebs": 0.119, | ||||||
|  |   "cost_core": 0.0575, | ||||||
|  |   "cost_memory": 0.014375, | ||||||
|   "postgresql_versions": [ |   "postgresql_versions": [ | ||||||
|  |     "12", | ||||||
|     "11", |     "11", | ||||||
|     "10", |     "10", | ||||||
|     "9.6" |     "9.6", | ||||||
|  |     "9.5" | ||||||
|   ], |   ], | ||||||
|   "static_network_whitelist": { |   "static_network_whitelist": { | ||||||
|     "localhost": ["172.0.0.1/32"] |     "localhost": ["172.0.0.1/32"] | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue