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:
parent
bf29d7a6c0
commit
44cec706b0
16 changed files with 787 additions and 107 deletions
File diff suppressed because one or more lines are too long
|
@ -16,7 +16,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"react-scripts": "1.1.4"
|
"react-scripts": "1.1.4"
|
||||||
},
|
},
|
||||||
"proxy": "https://192.168.140.208:8528",
|
"proxy": "https://192.168.140.211:8528",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
|
|
|
@ -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); }
|
|
||||||
}
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import ReactTimeout from 'react-timeout';
|
import ReactTimeout from 'react-timeout';
|
||||||
import DeploymentOperator from './deployment/DeploymentOperator.js';
|
import DeploymentOperator from './deployment/DeploymentOperator.js';
|
||||||
|
import StorageOperator from './storage/StorageOperator.js';
|
||||||
import NoOperator from './NoOperator.js';
|
import NoOperator from './NoOperator.js';
|
||||||
import Loading from './util/Loading.js';
|
import Loading from './util/Loading.js';
|
||||||
import api from './api/api.js';
|
import api from './api/api.js';
|
||||||
import { Container, Segment, Message } from 'semantic-ui-react';
|
import { Container, Segment, Message } from 'semantic-ui-react';
|
||||||
import './App.css';
|
|
||||||
|
|
||||||
const PodInfoView = ({pod, namespace}) => (
|
const PodInfoView = ({pod, namespace}) => (
|
||||||
<Segment basic>
|
<Segment basic>
|
||||||
|
@ -16,11 +16,14 @@ const PodInfoView = ({pod, namespace}) => (
|
||||||
</Segment>
|
</Segment>
|
||||||
);
|
);
|
||||||
|
|
||||||
const OperatorsView = ({error, deployment, pod, namespace}) => {
|
const OperatorsView = ({error, deployment, storage, pod, namespace}) => {
|
||||||
const podInfoView = (<PodInfoView pod={pod} namespace={namespace}/>);
|
const podInfoView = (<PodInfoView pod={pod} namespace={namespace}/>);
|
||||||
if (deployment) {
|
if (deployment) {
|
||||||
return (<DeploymentOperator podInfoView={podInfoView} error={error}/>);
|
return (<DeploymentOperator podInfoView={podInfoView} error={error}/>);
|
||||||
}
|
}
|
||||||
|
if (storage) {
|
||||||
|
return (<StorageOperator podInfoView={podInfoView} error={error}/>);
|
||||||
|
}
|
||||||
return (<NoOperator podInfoView={podInfoView} error={error}/>);
|
return (<NoOperator podInfoView={podInfoView} error={error}/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +58,7 @@ class App extends Component {
|
||||||
return <OperatorsView
|
return <OperatorsView
|
||||||
error={this.state.error}
|
error={this.state.error}
|
||||||
deployment={this.state.operators.deployment}
|
deployment={this.state.operators.deployment}
|
||||||
|
storage={this.state.operators.storage}
|
||||||
pod={this.state.operators.pod}
|
pod={this.state.operators.pod}
|
||||||
namespace={this.state.operators.namespace}
|
namespace={this.state.operators.namespace}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import logo from './logo.svg';
|
import { Container, Message, Modal, Segment } from 'semantic-ui-react';
|
||||||
import './App.css';
|
|
||||||
import { Message } from 'semantic-ui-react';
|
|
||||||
|
|
||||||
class NoOperator extends Component {
|
class NoOperator extends Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<Container>
|
||||||
<header className="App-header">
|
<Modal open>
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
<Modal.Header>Welcome to Kube-ArangoDB</Modal.Header>
|
||||||
<h1 className="App-title">Welcome to Kube-ArangoDB</h1>
|
<Modal.Content>
|
||||||
</header>
|
<Segment basic>
|
||||||
<p className="App-intro">
|
<Message color="orange">
|
||||||
There are no operators available yet.
|
There are no operators available yet.
|
||||||
</p>
|
</Message>
|
||||||
{this.props.podInfoView}
|
</Segment>
|
||||||
{(this.props.error) ? <Message error content={this.props.error}/> : null}
|
{this.props.podInfoView}
|
||||||
</div>
|
{(this.props.error) ? <Message error content={this.props.error}/> : null}
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
198
dashboard/src/storage/StorageList.js
Normal file
198
dashboard/src/storage/StorageList.js
Normal 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);
|
58
dashboard/src/storage/StorageOperator.js
Normal file
58
dashboard/src/storage/StorageOperator.js
Normal 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;
|
141
dashboard/src/storage/VolumeList.js
Normal file
141
dashboard/src/storage/VolumeList.js
Normal 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);
|
|
@ -30,6 +30,9 @@ rules:
|
||||||
- apiGroups: [""]
|
- apiGroups: [""]
|
||||||
resources: ["pods"]
|
resources: ["pods"]
|
||||||
verbs: ["get", "update"]
|
verbs: ["get", "update"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
verbs: ["get"]
|
||||||
- apiGroups: ["apps"]
|
- apiGroups: ["apps"]
|
||||||
resources: ["daemonsets"]
|
resources: ["daemonsets"]
|
||||||
verbs: ["*"]
|
verbs: ["*"]
|
||||||
|
|
19
manifests/templates/storage/service.yaml
Normal file
19
manifests/templates/storage/service.yaml
Normal 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 }}
|
|
@ -60,3 +60,36 @@ func (o *Operator) GetDeployment(name string) (server.Deployment, error) {
|
||||||
}
|
}
|
||||||
return nil, maskAny(server.NotFoundError)
|
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)
|
||||||
|
}
|
||||||
|
|
139
pkg/server/handlers_storage.go
Normal file
139
pkg/server/handlers_storage.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -69,6 +69,8 @@ type Dependencies struct {
|
||||||
type Operators interface {
|
type Operators interface {
|
||||||
// Return the deployment operator (if any)
|
// Return the deployment operator (if any)
|
||||||
DeploymentOperator() DeploymentOperator
|
DeploymentOperator() DeploymentOperator
|
||||||
|
// Return the local storage operator (if any)
|
||||||
|
StorageOperator() StorageOperator
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server is the HTTPS server for the operator.
|
// Server is the HTTPS server for the operator.
|
||||||
|
@ -153,6 +155,10 @@ func NewServer(cli corev1.CoreV1Interface, cfg Config, deps Dependencies) (*Serv
|
||||||
// Deployment operator
|
// Deployment operator
|
||||||
api.GET("/deployment", s.handleGetDeployments)
|
api.GET("/deployment", s.handleGetDeployments)
|
||||||
api.GET("/deployment/:name", s.handleGetDeploymentDetails)
|
api.GET("/deployment/:name", s.handleGetDeploymentDetails)
|
||||||
|
|
||||||
|
// Local storage operator
|
||||||
|
api.GET("/storage", s.handleGetLocalStorages)
|
||||||
|
api.GET("/storage/:name", s.handleGetLocalStorageDetails)
|
||||||
}
|
}
|
||||||
// Dashboard
|
// Dashboard
|
||||||
r.GET("/", createAssetFileHandler(dashboard.Assets.Files["index.html"]))
|
r.GET("/", createAssetFileHandler(dashboard.Assets.Files["index.html"]))
|
||||||
|
|
70
pkg/storage/server_api.go
Normal file
70
pkg/storage/server_api.go
Normal 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
|
||||||
|
}
|
46
pkg/storage/server_volume_api.go
Normal file
46
pkg/storage/server_volume_api.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,6 +63,7 @@ var (
|
||||||
storageTemplateNames = []string{
|
storageTemplateNames = []string{
|
||||||
"rbac.yaml",
|
"rbac.yaml",
|
||||||
"deployment.yaml",
|
"deployment.yaml",
|
||||||
|
"service.yaml",
|
||||||
}
|
}
|
||||||
testTemplateNames = []string{
|
testTemplateNames = []string{
|
||||||
"rbac.yaml",
|
"rbac.yaml",
|
||||||
|
|
Loading…
Reference in a new issue