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

Adding local storage dashboard

This commit is contained in:
Ewout Prangsma 2018-07-09 11:05:31 +02:00
parent bf29d7a6c0
commit 44cec706b0
No known key found for this signature in database
GPG key ID: 4DBAD380D93D0698
16 changed files with 787 additions and 107 deletions

File diff suppressed because one or more lines are too long

View file

@ -16,7 +16,7 @@
"devDependencies": {
"react-scripts": "1.1.4"
},
"proxy": "https://192.168.140.208:8528",
"proxy": "https://192.168.140.211:8528",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",

View file

@ -1,28 +0,0 @@
.App {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 80px;
}
.App-header {
background-color: #222;
height: 150px;
padding: 20px;
color: white;
}
.App-title {
font-size: 1.5em;
}
.App-intro {
font-size: large;
}
@keyframes App-logo-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}

View file

@ -1,11 +1,11 @@
import React, { Component } from 'react';
import ReactTimeout from 'react-timeout';
import DeploymentOperator from './deployment/DeploymentOperator.js';
import StorageOperator from './storage/StorageOperator.js';
import NoOperator from './NoOperator.js';
import Loading from './util/Loading.js';
import api from './api/api.js';
import { Container, Segment, Message } from 'semantic-ui-react';
import './App.css';
const PodInfoView = ({pod, namespace}) => (
<Segment basic>
@ -16,11 +16,14 @@ const PodInfoView = ({pod, namespace}) => (
</Segment>
);
const OperatorsView = ({error, deployment, pod, namespace}) => {
const OperatorsView = ({error, deployment, storage, pod, namespace}) => {
const podInfoView = (<PodInfoView pod={pod} namespace={namespace}/>);
if (deployment) {
return (<DeploymentOperator podInfoView={podInfoView} error={error}/>);
}
if (storage) {
return (<StorageOperator podInfoView={podInfoView} error={error}/>);
}
return (<NoOperator podInfoView={podInfoView} error={error}/>);
}
@ -55,6 +58,7 @@ class App extends Component {
return <OperatorsView
error={this.state.error}
deployment={this.state.operators.deployment}
storage={this.state.operators.storage}
pod={this.state.operators.pod}
namespace={this.state.operators.namespace}
/>;

View file

@ -1,22 +1,23 @@
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { Message } from 'semantic-ui-react';
import { Container, Message, Modal, Segment } from 'semantic-ui-react';
class NoOperator extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to Kube-ArangoDB</h1>
</header>
<p className="App-intro">
There are no operators available yet.
</p>
{this.props.podInfoView}
{(this.props.error) ? <Message error content={this.props.error}/> : null}
</div>
<Container>
<Modal open>
<Modal.Header>Welcome to Kube-ArangoDB</Modal.Header>
<Modal.Content>
<Segment basic>
<Message color="orange">
There are no operators available yet.
</Message>
</Segment>
{this.props.podInfoView}
{(this.props.error) ? <Message error content={this.props.error}/> : null}
</Modal.Content>
</Modal>
</Container>
);
}
}

View file

@ -0,0 +1,198 @@
import { Accordion, Header, Icon, Loader, Popup, Table } from 'semantic-ui-react';
import api from '../api/api.js';
import CommandInstruction from '../util/CommandInstruction.js';
import VolumeList from './VolumeList.js';
import Loading from '../util/Loading.js';
import React, { Component } from 'react';
import ReactTimeout from 'react-timeout';
import styled from 'react-emotion';
const LoaderBox = styled('span')`
float: right;
width: 0;
padding-right: 1em;
max-width: 0;
display: inline-block;
`;
const HeaderView = ({loading}) => (
<Table.Header>
<Table.Row>
<Table.HeaderCell>State</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Local path(s)</Table.HeaderCell>
<Table.HeaderCell>StorageClass</Table.HeaderCell>
<Table.HeaderCell>
Actions
<LoaderBox><Loader size="mini" active={loading} inline/></LoaderBox>
</Table.HeaderCell>
</Table.Row>
</Table.Header>
);
const RowView = ({name, stateColor,localPaths, storageClass, storageClassIsDefault, deleteCommand, describeCommand, expanded, toggleExpand}) => (
<Table.Row>
<Table.Cell>
<Popup trigger={<Icon name={(stateColor==="green") ? "check" : "bell"} color={stateColor}/>}>
{getStateColorDescription(stateColor)}
</Popup>
</Table.Cell>
<Table.Cell onClick={toggleExpand}>
<Accordion>
<Accordion.Title active={expanded}>
<Icon name='dropdown' />
{name}
</Accordion.Title>
</Accordion>
</Table.Cell>
<Table.Cell>
{localPaths.map((item) => <code>{item}</code>)}
</Table.Cell>
<Table.Cell>
{storageClass}
<span style={{"float":"right"}}>
{storageClassIsDefault && <Popup trigger={<Icon name="exclamation"/>} content="Default storage class"/>}
</span>
</Table.Cell>
<Table.Cell>
<CommandInstruction
trigger={<Icon link name="zoom"/>}
command={describeCommand}
title="Describe local storage"
description="To get more information on the state of this local storage, run:"
/>
<span style={{"float":"right"}}>
<CommandInstruction
trigger={<Icon link name="trash"/>}
command={deleteCommand}
title="Delete local storage"
description="To delete this local storage, run:"
/>
</span>
</Table.Cell>
</Table.Row>
);
const VolumesRowView = ({name}) => (
<Table.Row>
<Table.Cell colspan="5">
<Header sub>Volumes</Header>
<VolumeList storageName={name}/>
</Table.Cell>
</Table.Row>
);
const ListView = ({items, loading}) => (
<Table celled>
<HeaderView loading={loading}/>
<Table.Body>
{
(items) ? items.map((item) =>
<RowComponent
key={item.name}
name={item.name}
localPaths={item.local_paths}
stateColor={item.state_color}
storageClass={item.storage_class}
storageClassIsDefault={item.storage_class_is_default}
deleteCommand={createDeleteCommand(item.name)}
describeCommand={createDescribeCommand(item.name)}
/>
) : <p>No items</p>
}
</Table.Body>
</Table>
);
class RowComponent extends Component {
state = {expanded: true};
onToggleExpand = () => { this.setState({expanded: !this.state.expanded});}
render() {
return [<RowView
key={this.props.name}
name={this.props.name}
localPaths={this.props.localPaths}
stateColor={this.props.stateColor}
storageClass={this.props.storageClass}
storageClassIsDefault={this.props.storageClassIsDefault}
deleteCommand={this.props.deleteCommand}
describeCommand={this.props.describeCommand}
toggleExpand={this.onToggleExpand}
expanded={this.state.expanded}
/>,
this.state.expanded && <VolumesRowView
key={`${this.props.name}-vol`}
name={this.props.name}
expanded={this.state.expanded}
toggleExpand={this.onToggleExpand}
/>
];
}
}
const EmptyView = () => (<div>No local storage resources</div>);
function createDeleteCommand(name) {
return `kubectl delete ArangoLocalStorage ${name}`;
}
function createDescribeCommand(name) {
return `kubectl describe ArangoLocalStorage ${name}`;
}
function getStateColorDescription(stateColor) {
switch (stateColor) {
case "green":
return "Everything is running smooth.";
case "yellow":
return "There is some activity going on, but local storage is available.";
case "orange":
return "There is some activity going on, local storage may be/become unavailable. You should pay attention now!";
case "red":
return "The local storage is in a bad state and manual intervention is likely needed.";
default:
return "State is not known.";
}
}
class StorageList extends Component {
state = {
items: undefined,
error: undefined,
loading: true
};
componentDidMount() {
this.reloadStorages();
}
reloadStorages = async() => {
try {
this.setState({loading: true});
const result = await api.get('/api/storage');
this.setState({
items: result.storages,
loading: false,
error: undefined
});
} catch (e) {
this.setState({error: e.message, loading: false});
}
this.props.setTimeout(this.reloadStorages, 5000);
}
render() {
const items = this.state.items;
if (!items) {
return (<Loading/>);
}
if (items.length === 0) {
return (<EmptyView/>);
}
return (<ListView items={items} loading={this.state.loading}/>);
}
}
export default ReactTimeout(StorageList);

View file

@ -0,0 +1,58 @@
import React, { Component } from 'react';
import LogoutContext from '../auth/LogoutContext.js';
import StorageList from './StorageList.js';
import { Header, Menu, Message, Segment } from 'semantic-ui-react';
import styled from 'react-emotion';
const StyledMenu = styled(Menu)`
width: 15rem !important;
@media (max-width: 768px) {
width: 10rem !important;
}
`;
const StyledContentBox = styled('div')`
margin-left: 15rem;
@media (max-width: 768px) {
margin-left: 10rem;
}
`;
const ListView = () => (
<div>
<Header dividing>
ArangoLocalStorages
</Header>
<StorageList/>
</div>
);
class StorageOperator extends Component {
render() {
return (
<div>
<LogoutContext.Consumer>
{doLogout =>
<StyledMenu fixed="left" vertical>
<Menu.Item>
Local storages
</Menu.Item>
<Menu.Item position="right" onClick={() => doLogout()}>
Logout
</Menu.Item>
</StyledMenu>
}
</LogoutContext.Consumer>
<StyledContentBox>
<Segment basic clearing>
<ListView/>
</Segment>
{this.props.podInfoView}
{(this.props.error) ? <Segment basic><Message error content={this.props.error}/></Segment> : null}
</StyledContentBox>
</div>
);
}
}
export default StorageOperator;

View file

@ -0,0 +1,141 @@
import { Icon, Loader, Popup, Table } from 'semantic-ui-react';
import api from '../api/api.js';
import CommandInstruction from '../util/CommandInstruction.js';
import Loading from '../util/Loading.js';
import React, { Component } from 'react';
import ReactTimeout from 'react-timeout';
import styled from 'react-emotion';
const LoaderBox = styled('span')`
float: right;
width: 0;
padding-right: 1em;
max-width: 0;
display: inline-block;
`;
const HeaderView = ({loading}) => (
<Table.Header>
<Table.Row>
<Table.HeaderCell>State</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>
Actions
<LoaderBox><Loader size="mini" active={loading} inline/></LoaderBox>
</Table.HeaderCell>
</Table.Row>
</Table.Header>
);
const RowView = ({name, stateColor, describeCommand, deleteCommand}) => (
<Table.Row>
<Table.Cell>
<Popup trigger={<Icon name={(stateColor==="green") ? "check" : "bell"} color={stateColor}/>}>
{getStateColorDescription(stateColor)}
</Popup>
</Table.Cell>
<Table.Cell>
{name}
</Table.Cell>
<Table.Cell>
<CommandInstruction
trigger={<Icon link name="zoom"/>}
command={describeCommand}
title="Describe PersistentVolume"
description="To get more information on the state of this PersistentVolume, run:"
/>
<span style={{"float":"right"}}>
<CommandInstruction
trigger={<Icon link name="trash"/>}
command={deleteCommand}
title="Delete PersistentVolume"
description="To delete this PersistentVolume, run:"
/>
</span>
</Table.Cell>
</Table.Row>
);
const ListView = ({items, loading}) => (
<Table celled>
<HeaderView loading={loading}/>
<Table.Body>
{
(items) ? items.map((item) =>
<RowView
key={item.name}
name={item.name}
stateColor={item.state_color}
deleteCommand={createDeleteCommand(item.name)}
describeCommand={createDescribeCommand(item.name)}
/>
) : <p>No items</p>
}
</Table.Body>
</Table>
);
const EmptyView = () => (<div>No PersistentVolumes</div>);
function createDeleteCommand(name) {
return `kubectl delete PersistentVolume ${name}`;
}
function createDescribeCommand(name) {
return `kubectl describe PersistentVolume ${name}`;
}
function getStateColorDescription(stateColor) {
switch (stateColor) {
case "green":
return "Everything is running smooth.";
case "yellow":
return "There is some activity going on, but PersistentVolume is available.";
case "orange":
return "There is some activity going on, PersistentVolume may be/become unavailable. You should pay attention now!";
case "red":
return "The PersistentVolume is in a bad state and manual intervention is likely needed.";
default:
return "State is not known.";
}
}
class VolumeList extends Component {
state = {
items: undefined,
error: undefined,
loading: true
};
componentDidMount() {
this.reloadVolumes();
}
reloadVolumes = async() => {
try {
this.setState({loading: true});
const result = await api.get(`/api/storage/${this.props.storageName}`);
this.setState({
items: result.volumes,
loading: false,
error: undefined
});
} catch (e) {
this.setState({error: e.message, loading: false});
}
this.props.setTimeout(this.reloadVolumes, 5000);
}
render() {
const items = this.state.items;
if (!items) {
return (<Loading/>);
}
if (items.length === 0) {
return (<EmptyView/>);
}
return (<ListView items={items} loading={this.state.loading}/>);
}
}
export default ReactTimeout(VolumeList);

View file

@ -30,6 +30,9 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "update"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
- apiGroups: ["apps"]
resources: ["daemonsets"]
verbs: ["*"]

View file

@ -0,0 +1,19 @@
apiVersion: ""
kind: Service
metadata:
name: {{ .Storage.OperatorDeploymentName }}
namespace: {{ .Storage.Operator.Namespace }}
labels:
name: {{ .Storage.OperatorDeploymentName }}
app: arango-storage-operator
spec:
ports:
- name: server
port: 8528
protocol: TCP
targetPort: 8528
selector:
name: {{ .Storage.OperatorDeploymentName }}
app: arango-storage-operator
role: leader
type: {{ .Storage.Operator.ServiceType }}

View file

@ -60,3 +60,36 @@ func (o *Operator) GetDeployment(name string) (server.Deployment, error) {
}
return nil, maskAny(server.NotFoundError)
}
// StorageOperator provides the local storage operator (if any)
func (o *Operator) StorageOperator() server.StorageOperator {
return o
}
// GetLocalStorages returns basic information for all local storages managed by the operator
func (o *Operator) GetLocalStorages() ([]server.LocalStorage, error) {
o.Dependencies.LivenessProbe.Lock()
defer o.Dependencies.LivenessProbe.Unlock()
result := make([]server.LocalStorage, 0, len(o.localStorages))
for _, ls := range o.localStorages {
result = append(result, ls)
}
sort.Slice(result, func(i, j int) bool {
return result[i].Name() < result[j].Name()
})
return result, nil
}
// GetLocalStorage returns detailed information for a local, managed by the operator, with given name
func (o *Operator) GetLocalStorage(name string) (server.LocalStorage, error) {
o.Dependencies.LivenessProbe.Lock()
defer o.Dependencies.LivenessProbe.Unlock()
for _, ls := range o.localStorages {
if ls.Name() == name {
return ls, nil
}
}
return nil, maskAny(server.NotFoundError)
}

View file

@ -0,0 +1,139 @@
//
// DISCLAIMER
//
// Copyright 2018 ArangoDB GmbH, Cologne, Germany
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//
// Author Ewout Prangsma
//
package server
import (
"net/http"
"github.com/gin-gonic/gin"
)
// LocalStorage is the API implemented by an ArangoLocalStorage.
type LocalStorage interface {
Name() string
LocalPaths() []string
StateColor() StateColor
StorageClass() string
StorageClassIsDefault() bool
Volumes() []Volume
}
// StorageOperator is the API implemented by the storage operator.
type StorageOperator interface {
// GetLocalStorages returns basic information for all local storages managed by the operator
GetLocalStorages() ([]LocalStorage, error)
// GetLocalStorage returns detailed information for a local, managed by the operator, with given name
GetLocalStorage(name string) (LocalStorage, error)
}
// LocalStorageInfo is the information returned per local storage.
type LocalStorageInfo struct {
Name string `json:"name"`
LocalPaths []string `json:"local_paths"`
StateColor StateColor `json:"state_color"`
StorageClass string `json:"storage_class"`
StorageClassIsDefault bool `json:"storage_class_is_default"`
}
// newLocalStorageInfo initializes a LocalStorageInfo for the given LocalStorage.
func newLocalStorageInfo(ls LocalStorage) LocalStorageInfo {
return LocalStorageInfo{
Name: ls.Name(),
LocalPaths: ls.LocalPaths(),
StateColor: ls.StateColor(),
StorageClass: ls.StorageClass(),
StorageClassIsDefault: ls.StorageClassIsDefault(),
}
}
// LocalStorageInfoDetails contains detailed info a local storage
type LocalStorageInfoDetails struct {
LocalStorageInfo
Volumes []VolumeInfo `json:"volumes"`
}
// newLocalStorageInfoDetails creates a LocalStorageInfoDetails for the given local storage
func newLocalStorageInfoDetails(ls LocalStorage) LocalStorageInfoDetails {
vols := ls.Volumes()
result := LocalStorageInfoDetails{
LocalStorageInfo: newLocalStorageInfo(ls),
Volumes: make([]VolumeInfo, 0, len(vols)),
}
for _, v := range vols {
result.Volumes = append(result.Volumes, newVolumeInfo(v))
}
return result
}
// Volume is the API implemented by a volume created in a ArangoLocalStorage.
type Volume interface {
Name() string
StateColor() StateColor
}
// VolumeInfo contained the information returned per volume that is created on behalf of a local storage.
type VolumeInfo struct {
Name string `json:"name"`
StateColor StateColor `json:"state_color"`
}
// newVolumeInfo creates a VolumeInfo for the given volume
func newVolumeInfo(v Volume) VolumeInfo {
return VolumeInfo{
Name: v.Name(),
StateColor: v.StateColor(),
}
}
// Handle a GET /api/storage request
func (s *Server) handleGetLocalStorages(c *gin.Context) {
if o := s.deps.Operators.StorageOperator(); o != nil {
// Fetch local storages
stgs, err := o.GetLocalStorages()
if err != nil {
sendError(c, err)
} else {
result := make([]LocalStorageInfo, len(stgs))
for i, ls := range stgs {
result[i] = newLocalStorageInfo(ls)
}
c.JSON(http.StatusOK, gin.H{
"storages": result,
})
}
}
}
// Handle a GET /api/storage/:name request
func (s *Server) handleGetLocalStorageDetails(c *gin.Context) {
if o := s.deps.Operators.StorageOperator(); o != nil {
// Fetch deployments
ls, err := o.GetLocalStorage(c.Params.ByName("name"))
if err != nil {
sendError(c, err)
} else {
result := newLocalStorageInfoDetails(ls)
c.JSON(http.StatusOK, result)
}
}
}

View file

@ -69,6 +69,8 @@ type Dependencies struct {
type Operators interface {
// Return the deployment operator (if any)
DeploymentOperator() DeploymentOperator
// Return the local storage operator (if any)
StorageOperator() StorageOperator
}
// Server is the HTTPS server for the operator.
@ -153,6 +155,10 @@ func NewServer(cli corev1.CoreV1Interface, cfg Config, deps Dependencies) (*Serv
// Deployment operator
api.GET("/deployment", s.handleGetDeployments)
api.GET("/deployment/:name", s.handleGetDeploymentDetails)
// Local storage operator
api.GET("/storage", s.handleGetLocalStorages)
api.GET("/storage/:name", s.handleGetLocalStorageDetails)
}
// Dashboard
r.GET("/", createAssetFileHandler(dashboard.Assets.Files["index.html"]))

70
pkg/storage/server_api.go Normal file
View file

@ -0,0 +1,70 @@
//
// DISCLAIMER
//
// Copyright 2018 ArangoDB GmbH, Cologne, Germany
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//
// Author Ewout Prangsma
//
package storage
import (
"github.com/arangodb/kube-arangodb/pkg/server"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Name returns the name of the local storage resource
func (ls *LocalStorage) Name() string {
return ls.apiObject.Name
}
// LocalPaths returns the local paths (on nodes) of the local storage resource
func (ls *LocalStorage) LocalPaths() []string {
return ls.apiObject.Spec.LocalPath
}
// StateColor returns a color describing the state of the local storage resource
func (ls *LocalStorage) StateColor() server.StateColor {
// TODO
return server.StateYellow
}
// StorageClass returns the name of the StorageClass specified in the local storage resource
func (ls *LocalStorage) StorageClass() string {
return ls.apiObject.Spec.StorageClass.Name
}
// StorageClassIsDefault returns true if the StorageClass used by this local storage resource is supposed to be default
func (ls *LocalStorage) StorageClassIsDefault() bool {
return ls.apiObject.Spec.StorageClass.IsDefault
}
// Volumes returns all volumes created by the local storage resource
func (ls *LocalStorage) Volumes() []server.Volume {
list, err := ls.deps.KubeCli.CoreV1().PersistentVolumes().List(metav1.ListOptions{})
if err != nil {
ls.deps.Log.Error().Err(err).Msg("Failed to list persistent volumes")
return nil
}
result := make([]server.Volume, 0, len(list.Items))
for _, pv := range list.Items {
if ls.isOwnerOf(&pv) {
result = append(result, serverVolume(pv))
}
}
return result
}

View file

@ -0,0 +1,46 @@
//
// DISCLAIMER
//
// Copyright 2018 ArangoDB GmbH, Cologne, Germany
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//
// Author Ewout Prangsma
//
package storage
import (
"github.com/arangodb/kube-arangodb/pkg/server"
"k8s.io/api/core/v1"
)
type serverVolume v1.PersistentVolume
// Name returns the name of the volume
func (v serverVolume) Name() string {
return v.ObjectMeta.GetName()
}
func (v serverVolume) StateColor() server.StateColor {
switch v.Status.Phase {
default:
return server.StateYellow
case v1.VolumeBound:
return server.StateGreen
case v1.VolumeFailed:
return server.StateRed
}
}

View file

@ -63,6 +63,7 @@ var (
storageTemplateNames = []string{
"rbac.yaml",
"deployment.yaml",
"service.yaml",
}
testTemplateNames = []string{
"rbac.yaml",