1
0
Fork 0
mirror of https://github.com/arangodb/kube-arangodb.git synced 2024-12-14 11:57:37 +00:00

Extensing deployment list

This commit is contained in:
Ewout Prangsma 2018-07-05 11:54:27 +02:00
parent 7c7849ac46
commit 81b98028b3
No known key found for this signature in database
GPG key ID: 4DBAD380D93D0698
10 changed files with 357 additions and 77 deletions

File diff suppressed because one or more lines are too long

View file

@ -4,6 +4,22 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@babel/runtime": {
"version": "7.0.0-beta.51",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-beta.51.tgz",
"integrity": "sha1-SLjtGDBwNMZiD2Q1FGUMoszAFlo=",
"requires": {
"core-js": "^2.5.7",
"regenerator-runtime": "^0.11.1"
},
"dependencies": {
"core-js": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
"integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
}
}
},
"abab": { "abab": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
@ -1334,6 +1350,7 @@
"version": "6.26.0", "version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
"dev": true,
"requires": { "requires": {
"core-js": "^2.4.0", "core-js": "^2.4.0",
"regenerator-runtime": "^0.11.0" "regenerator-runtime": "^0.11.0"
@ -1342,7 +1359,8 @@
"core-js": { "core-js": {
"version": "2.5.7", "version": "2.5.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
"integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==",
"dev": true
} }
} }
}, },
@ -2396,6 +2414,14 @@
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
"dev": true "dev": true
}, },
"copy-to-clipboard": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz",
"integrity": "sha512-c3GdeY8qxCHGezVb1EFQfHYK/8NZRemgcTIzPq7PuxjHAf/raKibn2QdhHPb/y6q74PMgH6yizaDZlRmw6QyKw==",
"requires": {
"toggle-selection": "^1.0.3"
}
},
"core-js": { "core-js": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
@ -7037,6 +7063,11 @@
"integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=", "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=",
"dev": true "dev": true
}, },
"keyboard-key": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/keyboard-key/-/keyboard-key-1.0.1.tgz",
"integrity": "sha512-OAfjaSI917BOonwfH6LQHMZJRv5035jjZvgElouB/DM4I7l5zEjrA15RD80YwIjhN69xqEfWCZIbhBcGpb85Ig=="
},
"killable": { "killable": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz",
@ -9847,6 +9878,15 @@
"prop-types": "^15.6.0" "prop-types": "^15.6.0"
} }
}, },
"react-copy-to-clipboard": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz",
"integrity": "sha512-ELKq31/E3zjFs5rDWNCfFL4NvNFQvGRoJdAKReD/rUPA+xxiLPQmZBZBvy2vgH7V0GE9isIQpT9WXbwIVErYdA==",
"requires": {
"copy-to-clipboard": "^3",
"prop-types": "^15.5.8"
}
},
"react-dev-utils": { "react-dev-utils": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-5.0.1.tgz", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-5.0.1.tgz",
@ -10532,15 +10572,16 @@
} }
}, },
"semantic-ui-react": { "semantic-ui-react": {
"version": "0.78.3", "version": "0.81.3",
"resolved": "https://registry.npmjs.org/semantic-ui-react/-/semantic-ui-react-0.78.3.tgz", "resolved": "https://registry.npmjs.org/semantic-ui-react/-/semantic-ui-react-0.81.3.tgz",
"integrity": "sha512-JRmuqjyigCehHfzrS2ir5nGoytZWCifU8G2T++G/CMdahUJBME7S6E9rU7WW9Qg2Fqn2aJIxfn6Ry/rlOTJDOw==", "integrity": "sha512-AGLKjtWT0HnOyduMJn+6T4JmV5DfEuOfN2iSpBmQ5ZNJxrmdD4hoiskP89MUT7JD4Mno7aH8KkzdIKxavxpJUw==",
"requires": { "requires": {
"babel-runtime": "^6.25.0", "@babel/runtime": "^7.0.0-beta.49",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"fbjs": "^0.8.16", "keyboard-key": "^1.0.1",
"lodash": "^4.17.4", "lodash": "^4.17.10",
"prop-types": "^15.5.10" "prop-types": "^15.6.1",
"shallowequal": "^1.0.2"
} }
}, },
"semver": { "semver": {
@ -10676,6 +10717,11 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
},
"shebang-command": { "shebang-command": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@ -11446,6 +11492,11 @@
"repeat-string": "^1.6.1" "repeat-string": "^1.6.1"
} }
}, },
"toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
},
"toposort": { "toposort": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz",

View file

@ -4,14 +4,15 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"react": "^16.4.1", "react": "^16.4.1",
"react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.4.1", "react-dom": "^16.4.1",
"semantic-ui-less": "^2.2.12", "semantic-ui-less": "^2.2.12",
"semantic-ui-react": "^0.78.2" "semantic-ui-react": "^0.81.3"
}, },
"devDependencies": { "devDependencies": {
"react-scripts": "1.1.4" "react-scripts": "1.1.4"
}, },
"proxy": "https://kube-arangodb-operator:8528", "proxy": "https://192.168.140.208:8528",
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",

View file

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" -->
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">
<!-- <!--
manifest.json provides metadata used when your web app is added to the manifest.json provides metadata used when your web app is added to the

View file

@ -1,7 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { apiGet } from '../api/api.js'; import { apiGet } from '../api/api.js';
import { Icon, Table } from 'semantic-ui-react'; import { Icon, Popup, Table } from 'semantic-ui-react';
import Loading from '../util/Loading.js'; import Loading from '../util/Loading.js';
import CommandInstruction from '../util/CommandInstruction.js';
const HeaderView = () => ( const HeaderView = () => (
<Table.Header> <Table.Header>
@ -9,17 +10,47 @@ const HeaderView = () => (
<Table.HeaderCell>State</Table.HeaderCell> <Table.HeaderCell>State</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell> <Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Mode</Table.HeaderCell> <Table.HeaderCell>Mode</Table.HeaderCell>
<Table.HeaderCell>Pods</Table.HeaderCell> <Table.HeaderCell>Version</Table.HeaderCell>
<Table.HeaderCell><Popup trigger={<span>Pods</span>}>Ready / Total</Popup></Table.HeaderCell>
<Table.HeaderCell><Popup trigger={<span>Volumes</span>}>Bound / Total</Popup></Table.HeaderCell>
<Table.HeaderCell>StorageClass</Table.HeaderCell>
<Table.HeaderCell></Table.HeaderCell>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
); );
const RowView = ({name, mode, ready_pod_count, pod_count}) => ( const DatabaseLinkView = ({name, url}) => (
<a href={url} target={name}>
<Popup trigger={<Icon link name="database"/>}>
Go the the web-UI of the database.
</Popup>
</a>
);
const NoDatabaseLinkView = () => (
<Popup trigger={<Icon link name="database"/>}>
This deployment is not reachable outside the Kubernetes cluster.
</Popup>
);
const RowView = ({name, mode, version, license, ready_pod_count, pod_count, ready_volume_count, volume_count, storage_classes, database_url, delete_command}) => (
<Table.Row> <Table.Row>
<Table.Cell><Icon name="bell" color="red"/></Table.Cell> <Table.Cell><Icon name="bell" color="red"/></Table.Cell>
<Table.Cell>{name}</Table.Cell> <Table.Cell>{name}</Table.Cell>
<Table.Cell>{mode}</Table.Cell> <Table.Cell>{mode}</Table.Cell>
<Table.Cell>{version} {(license) ? `(${license})` : "" }</Table.Cell>
<Table.Cell>{ready_pod_count} / {pod_count}</Table.Cell> <Table.Cell>{ready_pod_count} / {pod_count}</Table.Cell>
<Table.Cell>{ready_volume_count} / {volume_count}</Table.Cell>
<Table.Cell>{storage_classes.map((item) => (item === "") ? "<default>" : item)}</Table.Cell>
<Table.Cell>
{ database_url ? <DatabaseLinkView name={name} url={database_url}/> : <NoDatabaseLinkView/>}
<CommandInstruction
trigger={<Icon floated="right" name="trash alternate"/>}
command={delete_command}
title="Delete deployment"
description="To delete this deployment, run:"
/>
</Table.Cell>
</Table.Row> </Table.Row>
); );
@ -32,9 +63,17 @@ const ListView = ({items}) => (
<RowView <RowView
key={item.name} key={item.name}
name={item.name} name={item.name}
namespace={item.namespace}
mode={item.mode} mode={item.mode}
version={item.database_version}
license={item.database_license}
ready_pod_count={item.ready_pod_count} ready_pod_count={item.ready_pod_count}
pod_count={item.pod_count} pod_count={item.pod_count}
ready_volume_count={item.ready_volume_count}
volume_count={item.volume_count}
storage_classes={item.storage_classes}
database_url={item.database_url}
delete_command={createDeleteCommand(item.name, item.namespace)}
/>) : <p>No items</p> />) : <p>No items</p>
} }
</Table.Body> </Table.Body>
@ -43,6 +82,10 @@ const ListView = ({items}) => (
const EmptyView = () => (<div>No deployments</div>); const EmptyView = () => (<div>No deployments</div>);
function createDeleteCommand(name, namespace) {
return `kubectl delete ArangoDeployment -n ${namespace} ${name}`;
}
class DeploymentList extends Component { class DeploymentList extends Component {
state = {}; state = {};

View file

@ -0,0 +1,40 @@
import React, { Component } from 'react';
import { Button, Modal, Segment } from 'semantic-ui-react';
import {CopyToClipboard} from 'react-copy-to-clipboard';
class CommandInstruction extends Component {
state = {};
close = () => { this.setState({open:false}); }
open = () => { this.setState({open:true}); }
render() {
return (
<Modal trigger={this.props.trigger} onClose={this.close} onOpen={this.open} open={this.state.open}>
<Modal.Header>{this.props.title}</Modal.Header>
<Modal.Content>
<Modal.Description>
<p>
{this.props.description}
</p>
<Segment clearing>
<code>{this.props.command}</code>
</Segment>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<CopyToClipboard text={this.props.command} onCopy={this.close}>
<Button
positive
icon='copy'
labelPosition='right'
content="Copy"
/>
</CopyToClipboard>
</Modal.Actions>
</Modal>
);
}
}
export default CommandInstruction;

View file

@ -6,7 +6,7 @@ const Loading = ({message}) => (
<Dimmer inverted active> <Dimmer inverted active>
<Loader inverted>{message || "Loading..."}</Loader> <Loader inverted>{message || "Loading..."}</Loader>
</Dimmer> </Dimmer>
<div style={{"min-height":"3em"}}/> <div style={{minHeight:"3em"}}/>
</Segment> </Segment>
); );

View file

@ -29,7 +29,7 @@ rules:
verbs: ["*"] verbs: ["*"]
- apiGroups: [""] - apiGroups: [""]
resources: ["pods"] resources: ["pods"]
verbs: ["get"] verbs: ["get", "update"]
- apiGroups: ["apps"] - apiGroups: ["apps"]
resources: ["daemonsets"] resources: ["daemonsets"]
verbs: ["*"] verbs: ["*"]

View file

@ -23,7 +23,16 @@
package deployment package deployment
import ( import (
"fmt"
"net"
"sort"
"strconv"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1alpha" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1alpha"
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
) )
// Name returns the name of the deployment. // Name returns the name of the deployment.
@ -31,6 +40,11 @@ func (d *Deployment) Name() string {
return d.apiObject.Name return d.apiObject.Name
} }
// Namespace returns the namespace that contains the deployment.
func (d *Deployment) Namespace() string {
return d.apiObject.Namespace
}
// Mode returns the mode of the deployment. // Mode returns the mode of the deployment.
func (d *Deployment) Mode() api.DeploymentMode { func (d *Deployment) Mode() api.DeploymentMode {
return d.GetSpec().GetMode() return d.GetSpec().GetMode()
@ -68,3 +82,113 @@ func (d *Deployment) ReadyPodCount() int {
}) })
return count return count
} }
// VolumeCount returns the number of volumes for the deployment
func (d *Deployment) VolumeCount() int {
count := 0
status, _ := d.GetStatus()
status.Members.ForeachServerGroup(func(group api.ServerGroup, list api.MemberStatusList) error {
for _, m := range list {
if m.PersistentVolumeClaimName != "" {
count++
}
}
return nil
})
return count
}
// ReadyVolumeCount returns the number of volumes for the deployment that are in ready state
func (d *Deployment) ReadyVolumeCount() int {
count := 0
status, _ := d.GetStatus()
pvcs, _ := d.GetOwnedPVCs() // Ignore errors on purpose
status.Members.ForeachServerGroup(func(group api.ServerGroup, list api.MemberStatusList) error {
for _, m := range list {
if m.PersistentVolumeClaimName == "" {
continue
}
// Find status
for _, pvc := range pvcs {
if pvc.Name == m.PersistentVolumeClaimName {
if pvc.Status.Phase == v1.ClaimBound {
count++
}
}
}
}
return nil
})
return count
}
// StorageClasses returns the names of the StorageClasses used by this deployment.
func (d *Deployment) StorageClasses() []string {
scNames := make(map[string]struct{})
spec := d.GetSpec()
mode := spec.GetMode()
if mode.HasAgents() {
scNames[spec.Agents.GetStorageClassName()] = struct{}{}
}
if mode.HasDBServers() {
scNames[spec.DBServers.GetStorageClassName()] = struct{}{}
}
if mode.HasSingleServers() {
scNames[spec.Single.GetStorageClassName()] = struct{}{}
}
result := make([]string, 0, len(scNames))
for k := range scNames {
result = append(result, k)
}
sort.Strings(result)
return result
}
// DatabaseURL returns an URL to reach the database from outside the Kubernetes cluster
// Empty string means that the database is not reachable outside the Kubernetes cluster.
func (d *Deployment) DatabaseURL() string {
eaSvcName := k8sutil.CreateDatabaseExternalAccessServiceName(d.Name())
ns := d.apiObject.Namespace
svc, err := d.deps.KubeCli.CoreV1().Services(ns).Get(eaSvcName, metav1.GetOptions{})
if err != nil {
return ""
}
host := ""
switch svc.Spec.Type {
case v1.ServiceTypeLoadBalancer:
for _, i := range svc.Status.LoadBalancer.Ingress {
if i.Hostname != "" {
host = i.Hostname
} else {
host = i.IP
}
break
}
case v1.ServiceTypeNodePort:
// TODO
}
if host == "" {
return ""
}
scheme := "https"
if !d.GetSpec().IsSecure() {
scheme = "http"
}
return fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(host, strconv.Itoa(k8sutil.ArangoPort)))
}
// DatabaseVersion returns the version used by the deployment
// Returns versionNumber, licenseType
func (d *Deployment) DatabaseVersion() (string, string) {
image := d.GetSpec().GetImage()
status, _ := d.GetStatus()
info, found := status.Images.GetByImage(image)
if !found {
return "", ""
}
license := "community"
if info.Enterprise {
license = "enterprise"
}
return string(info.ArangoDBVersion), license
}

View file

@ -33,9 +33,15 @@ import (
// Deployment is the API implemented by an ArangoDeployment. // Deployment is the API implemented by an ArangoDeployment.
type Deployment interface { type Deployment interface {
Name() string Name() string
Namespace() string
Mode() api.DeploymentMode Mode() api.DeploymentMode
PodCount() int PodCount() int
ReadyPodCount() int ReadyPodCount() int
VolumeCount() int
ReadyVolumeCount() int
StorageClasses() []string
DatabaseURL() string
DatabaseVersion() (string, string)
} }
// DeploymentOperator is the API implemented by the deployment operator. // DeploymentOperator is the API implemented by the deployment operator.
@ -46,9 +52,16 @@ type DeploymentOperator interface {
// DeploymentInfo is the information returned per deployment. // DeploymentInfo is the information returned per deployment.
type DeploymentInfo struct { type DeploymentInfo struct {
Name string `json:"name"` Name string `json:"name"`
Namespace string `json:"namespace"`
Mode api.DeploymentMode `json:"mode"` Mode api.DeploymentMode `json:"mode"`
PodCount int `json:"pod_count"` PodCount int `json:"pod_count"`
ReadyPodCount int `json:"ready_pod_count"` ReadyPodCount int `json:"ready_pod_count"`
VolumeCount int `json:"volume_count"`
ReadyVolumeCount int `json:"ready_volume_count"`
StorageClasses []string `json:"storage_classes"`
DatabaseURL string `json:"database_url"`
DatabaseVersion string `json:"database_version"`
DatabaseLicense string `json:"database_license"`
} }
// Handle a GET /api/deployment request // Handle a GET /api/deployment request
@ -61,11 +74,19 @@ func (s *Server) handleGetDeployments(c *gin.Context) {
} else { } else {
result := make([]DeploymentInfo, len(depls)) result := make([]DeploymentInfo, len(depls))
for i, d := range depls { for i, d := range depls {
version, license := d.DatabaseVersion()
result[i] = DeploymentInfo{ result[i] = DeploymentInfo{
Name: d.Name(), Name: d.Name(),
Namespace: d.Namespace(),
Mode: d.Mode(), Mode: d.Mode(),
PodCount: d.PodCount(), PodCount: d.PodCount(),
ReadyPodCount: d.ReadyPodCount(), ReadyPodCount: d.ReadyPodCount(),
VolumeCount: d.VolumeCount(),
ReadyVolumeCount: d.ReadyVolumeCount(),
StorageClasses: d.StorageClasses(),
DatabaseURL: d.DatabaseURL(),
DatabaseVersion: version,
DatabaseLicense: license,
} }
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{