mirror of
https://github.com/arangodb/kube-arangodb.git
synced 2024-12-14 11:57:37 +00:00
Add links to other operators in dashboard menu
This commit is contained in:
parent
65cc238006
commit
feccd07e49
14 changed files with 361 additions and 90 deletions
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
import { Container, Segment, Message } from 'semantic-ui-react';
|
||||
import { Container, Segment, Menu, Message } from 'semantic-ui-react';
|
||||
import React, { Component } from 'react';
|
||||
import ReactTimeout from 'react-timeout';
|
||||
|
||||
|
@ -19,7 +19,14 @@ const PodInfoView = ({pod, namespace}) => (
|
|||
</Segment>
|
||||
);
|
||||
|
||||
const OperatorsView = ({error, deployment, deploymentReplication, storage, pod, namespace}) => {
|
||||
const OperatorsView = ({error, deployment, deploymentReplication, storage, pod, namespace, otherOperators}) => {
|
||||
let commonMenuItems = otherOperators.map((item) => <Menu.Item><a href={item.url}>{operatorType2Name(item.type)}</a></Menu.Item>);
|
||||
if (commonMenuItems.length > 0) {
|
||||
commonMenuItems = (<Menu.Item>
|
||||
<Menu.Header>Other operators</Menu.Header>
|
||||
<Menu.Menu>{commonMenuItems}</Menu.Menu>
|
||||
</Menu.Item>);
|
||||
}
|
||||
let Operator = NoOperator;
|
||||
if (deployment)
|
||||
Operator = DeploymentOperator;
|
||||
|
@ -30,11 +37,25 @@ const OperatorsView = ({error, deployment, deploymentReplication, storage, pod,
|
|||
return (
|
||||
<Operator
|
||||
podInfoView={<PodInfoView pod={pod} namespace={namespace} />}
|
||||
commonMenuItems={commonMenuItems}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const operatorType2Name = (oType) => {
|
||||
switch (oType) {
|
||||
case "deployment":
|
||||
return "Deployments";
|
||||
case "deployment_replication":
|
||||
return "Deployment replications";
|
||||
case "storage":
|
||||
return "Storage";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const LoadingView = () => (
|
||||
<Container>
|
||||
<Loading/>
|
||||
|
@ -76,6 +97,7 @@ class App extends Component {
|
|||
deployment={this.state.operators.deployment}
|
||||
deploymentReplication={this.state.operators.deployment_replication}
|
||||
storage={this.state.operators.storage}
|
||||
otherOperators={this.state.operators.other}
|
||||
pod={this.state.operators.pod}
|
||||
namespace={this.state.operators.namespace}
|
||||
/>;
|
||||
|
|
|
@ -34,10 +34,16 @@ class DeploymentOperator extends Component {
|
|||
{doLogout =>
|
||||
<StyledMenu fixed="left" vertical>
|
||||
<Menu.Item>
|
||||
<Link to="/">Deployments</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item position="right" onClick={() => doLogout()}>
|
||||
Logout
|
||||
<Menu.Header>Deployment Operator</Menu.Header>
|
||||
<Menu.Menu>
|
||||
<Menu.Item>
|
||||
<Link to="/">Deployments</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item position="right" onClick={() => doLogout()}>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
{this.props.commonMenuItems}
|
||||
</Menu.Item>
|
||||
</StyledMenu>
|
||||
}
|
||||
|
|
|
@ -34,11 +34,17 @@ class DeploymentReplicationOperator extends Component {
|
|||
{doLogout =>
|
||||
<StyledMenu fixed="left" vertical>
|
||||
<Menu.Item>
|
||||
<Link to="/">Deployment replications</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item position="right" onClick={() => doLogout()}>
|
||||
Logout
|
||||
<Menu.Header>Deployment Replication Operator</Menu.Header>
|
||||
<Menu.Menu>
|
||||
<Menu.Item>
|
||||
<Link to="/">Deployment replications</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item position="right" onClick={() => doLogout()}>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
</Menu.Item>
|
||||
{this.props.commonMenuItems}
|
||||
</StyledMenu>
|
||||
}
|
||||
</LogoutContext.Consumer>
|
||||
|
|
|
@ -20,14 +20,20 @@ class StorageOperator extends Component {
|
|||
<div>
|
||||
<LogoutContext.Consumer>
|
||||
{doLogout =>
|
||||
<StyledMenu fixed="left" vertical>
|
||||
<StyledMenu fixed="left" vertical>
|
||||
<Menu.Item>
|
||||
Local storages
|
||||
<Menu.Header>Deployment Operator</Menu.Header>
|
||||
<Menu.Menu>
|
||||
<Menu.Item>
|
||||
Local storages
|
||||
</Menu.Item>
|
||||
<Menu.Item position="right" onClick={() => doLogout()}>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
{this.props.commonMenuItems}
|
||||
</Menu.Item>
|
||||
<Menu.Item position="right" onClick={() => doLogout()}>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</StyledMenu>
|
||||
</StyledMenu>
|
||||
}
|
||||
</LogoutContext.Consumer>
|
||||
<StyledContentBox>
|
||||
|
|
|
@ -30,8 +30,8 @@ rules:
|
|||
resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "events", "secrets"]
|
||||
verbs: ["*"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["get"]
|
||||
resources: ["namespaces", "nodes"]
|
||||
verbs: ["get", "list"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments", "replicasets"]
|
||||
verbs: ["get"]
|
||||
|
|
|
@ -27,8 +27,8 @@ rules:
|
|||
resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "events", "secrets"]
|
||||
verbs: ["*"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["get"]
|
||||
resources: ["namespaces", "nodes"]
|
||||
verbs: ["get", "list"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments", "replicasets"]
|
||||
verbs: ["get"]
|
||||
|
|
|
@ -33,6 +33,9 @@ rules:
|
|||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get"]
|
||||
- apiGroups: [""]
|
||||
resources: ["namespaces", "nodes"]
|
||||
verbs: ["get", "list"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["daemonsets"]
|
||||
verbs: ["*"]
|
||||
|
|
|
@ -23,10 +23,7 @@
|
|||
package deployment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
@ -197,28 +194,25 @@ func (d *Deployment) DatabaseURL() string {
|
|||
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)))
|
||||
nodeFetcher := func() (v1.NodeList, error) {
|
||||
result, err := d.deps.KubeCli.CoreV1().Nodes().List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return v1.NodeList{}, maskAny(err)
|
||||
}
|
||||
return *result, nil
|
||||
}
|
||||
portPredicate := func(p v1.ServicePort) bool {
|
||||
return p.TargetPort.IntValue() == k8sutil.ArangoPort
|
||||
}
|
||||
url, err := k8sutil.CreateServiceURL(*svc, scheme, portPredicate, nodeFetcher)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// DatabaseVersion returns the version used by the deployment
|
||||
|
|
|
@ -36,8 +36,8 @@ var (
|
|||
// The defaultLevels list is used during development to increase the
|
||||
// default level for components that we care a little less about.
|
||||
defaultLevels = map[string]string{
|
||||
"operator": "info",
|
||||
//"something.status": "info",
|
||||
//"operator": "info",
|
||||
//"something.status": "info",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
146
pkg/operator/server_discovery_api.go
Normal file
146
pkg/operator/server_discovery_api.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
//
|
||||
// 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 operator
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/arangodb/kube-arangodb/pkg/server"
|
||||
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
|
||||
)
|
||||
|
||||
const (
|
||||
appKey = "app"
|
||||
roleKey = "role"
|
||||
appDeploymentOperator = "arango-deployment-operator"
|
||||
appDeploymentReplicationOperator = "arango-deployment-replication-operator"
|
||||
appStorageOperator = "arango-storage-operator"
|
||||
roleLeader = "leader"
|
||||
)
|
||||
|
||||
// FindOtherOperators looks up references to other operators in the same Kubernetes cluster.
|
||||
func (o *Operator) FindOtherOperators() []server.OperatorReference {
|
||||
log := o.log
|
||||
var result []server.OperatorReference
|
||||
namespaces, err := o.Dependencies.KubeCli.CoreV1().Namespaces().List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to list namespaces")
|
||||
} else {
|
||||
for _, ns := range namespaces.Items {
|
||||
if ns.Name != o.Config.Namespace {
|
||||
log.Debug().Str("namespace", ns.Name).Msg("inspecting namespace for operators")
|
||||
refs := o.findOtherOperatorsInNamespace(log, ns.Name, func(server.OperatorType) bool { return true })
|
||||
result = append(result, refs...)
|
||||
} else {
|
||||
log.Debug().Str("namespace", ns.Name).Msg("skip inspecting my own namespace for operators")
|
||||
}
|
||||
}
|
||||
}
|
||||
refs := o.findOtherOperatorsInNamespace(log, o.Config.Namespace, func(oType server.OperatorType) bool {
|
||||
// Exclude those operators that I provide myself.
|
||||
switch oType {
|
||||
case server.OperatorTypeDeployment:
|
||||
return !o.Dependencies.DeploymentProbe.IsReady()
|
||||
case server.OperatorTypeDeploymentReplication:
|
||||
return !o.Dependencies.DeploymentReplicationProbe.IsReady()
|
||||
case server.OperatorTypeStorage:
|
||||
return !o.Dependencies.StorageProbe.IsReady()
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
result = append(result, refs...)
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Namespace == result[j].Namespace {
|
||||
return result[i].Type < result[j].Type
|
||||
}
|
||||
return result[i].Namespace < result[j].Namespace
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// findOtherOperatorsInNamespace looks up references to other operators in the given namespace.
|
||||
func (o *Operator) findOtherOperatorsInNamespace(log zerolog.Logger, namespace string, typePred func(server.OperatorType) bool) []server.OperatorReference {
|
||||
log = log.With().Str("namespace", namespace).Logger()
|
||||
var result []server.OperatorReference
|
||||
services, err := o.Dependencies.KubeCli.CoreV1().Services(namespace).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("Failed to list services")
|
||||
return nil
|
||||
}
|
||||
nodeFetcher := func() (v1.NodeList, error) {
|
||||
result, err := o.Dependencies.KubeCli.CoreV1().Nodes().List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return v1.NodeList{}, maskAny(err)
|
||||
}
|
||||
return *result, nil
|
||||
}
|
||||
for _, svc := range services.Items {
|
||||
// Filter out unwanted services
|
||||
selector := svc.Spec.Selector
|
||||
if selector[roleKey] != roleLeader {
|
||||
log.Debug().Str("service", svc.Name).Msg("Service has no leader role selector")
|
||||
continue
|
||||
}
|
||||
var oType server.OperatorType
|
||||
switch selector[appKey] {
|
||||
case appDeploymentOperator:
|
||||
oType = server.OperatorTypeDeployment
|
||||
case appDeploymentReplicationOperator:
|
||||
oType = server.OperatorTypeDeploymentReplication
|
||||
case appStorageOperator:
|
||||
oType = server.OperatorTypeStorage
|
||||
default:
|
||||
log.Debug().Str("service", svc.Name).Msg("Service has no or invalid app selector")
|
||||
continue
|
||||
}
|
||||
if !typePred(oType) {
|
||||
continue
|
||||
}
|
||||
var url string
|
||||
switch svc.Spec.Type {
|
||||
case v1.ServiceTypeNodePort, v1.ServiceTypeLoadBalancer:
|
||||
if x, err := k8sutil.CreateServiceURL(svc, "https", nil, nodeFetcher); err == nil {
|
||||
url = x
|
||||
} else {
|
||||
log.Warn().Err(err).Str("service", svc.Name).Msg("Failed to create URL for service")
|
||||
}
|
||||
default:
|
||||
// No suitable service type
|
||||
continue
|
||||
}
|
||||
result = append(result, server.OperatorReference{
|
||||
Namespace: svc.GetNamespace(),
|
||||
URL: url,
|
||||
Type: oType,
|
||||
})
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Found %d operator services", len(result))
|
||||
return result
|
||||
}
|
|
@ -29,20 +29,39 @@ import (
|
|||
)
|
||||
|
||||
type operatorsResponse struct {
|
||||
PodName string `json:"pod"`
|
||||
Namespace string `json:"namespace"`
|
||||
Deployment bool `json:"deployment"`
|
||||
DeploymentReplication bool `json:"deployment_replication"`
|
||||
Storage bool `json:"storage"`
|
||||
PodName string `json:"pod"`
|
||||
Namespace string `json:"namespace"`
|
||||
Deployment bool `json:"deployment"`
|
||||
DeploymentReplication bool `json:"deployment_replication"`
|
||||
Storage bool `json:"storage"`
|
||||
Other []OperatorReference `json:"other"`
|
||||
}
|
||||
|
||||
type OperatorType string
|
||||
|
||||
const (
|
||||
OperatorTypeDeployment OperatorType = "deployment"
|
||||
OperatorTypeDeploymentReplication OperatorType = "deployment_replication"
|
||||
OperatorTypeStorage OperatorType = "storage"
|
||||
)
|
||||
|
||||
// OperatorReference contains a reference to another operator
|
||||
type OperatorReference struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Type OperatorType `json:"type"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// Handle a GET /api/operators request
|
||||
func (s *Server) handleGetOperators(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, operatorsResponse{
|
||||
result := operatorsResponse{
|
||||
PodName: s.cfg.PodName,
|
||||
Namespace: s.cfg.Namespace,
|
||||
Deployment: s.deps.DeploymentProbe.IsReady(),
|
||||
DeploymentReplication: s.deps.DeploymentReplicationProbe.IsReady(),
|
||||
Storage: s.deps.StorageProbe.IsReady(),
|
||||
})
|
||||
Other: s.deps.Operators.FindOtherOperators(),
|
||||
}
|
||||
s.deps.Log.Info().Interface("result", result).Msg("handleGetOperators")
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
|
|
@ -73,6 +73,8 @@ type Operators interface {
|
|||
DeploymentReplicationOperator() DeploymentReplicationOperator
|
||||
// Return the local storage operator (if any)
|
||||
StorageOperator() StorageOperator
|
||||
// FindOtherOperators looks up references to other operators in the same Kubernetes cluster.
|
||||
FindOtherOperators() []OperatorReference
|
||||
}
|
||||
|
||||
// Server is the HTTPS server for the operator.
|
||||
|
|
|
@ -23,7 +23,11 @@
|
|||
package k8sutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
@ -164,3 +168,66 @@ func createService(kubecli kubernetes.Interface, svcName, deploymentName, ns, cl
|
|||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CreateServiceURL creates a URL used to reach the given service.
|
||||
func CreateServiceURL(svc v1.Service, scheme string, portPredicate func(v1.ServicePort) bool, nodeFetcher func() (v1.NodeList, error)) (string, error) {
|
||||
var port int32
|
||||
var nodePort int32
|
||||
portFound := false
|
||||
for _, p := range svc.Spec.Ports {
|
||||
if portPredicate == nil || portPredicate(p) {
|
||||
port = p.Port
|
||||
nodePort = p.NodePort
|
||||
portFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !portFound {
|
||||
return "", maskAny(fmt.Errorf("Cannot find port in service '%s.%s'", svc.GetName(), svc.GetNamespace()))
|
||||
}
|
||||
|
||||
var host string
|
||||
switch svc.Spec.Type {
|
||||
case v1.ServiceTypeLoadBalancer:
|
||||
for _, x := range svc.Status.LoadBalancer.Ingress {
|
||||
if x.IP != "" {
|
||||
host = x.IP
|
||||
break
|
||||
} else if x.Hostname != "" {
|
||||
host = x.Hostname
|
||||
break
|
||||
}
|
||||
}
|
||||
if host == "" {
|
||||
host = svc.Spec.LoadBalancerIP
|
||||
}
|
||||
case v1.ServiceTypeNodePort:
|
||||
if nodePort > 0 {
|
||||
port = nodePort
|
||||
}
|
||||
nodeList, err := nodeFetcher()
|
||||
if err != nil {
|
||||
return "", maskAny(err)
|
||||
}
|
||||
if len(nodeList.Items) == 0 {
|
||||
return "", maskAny(fmt.Errorf("No nodes found"))
|
||||
}
|
||||
node := nodeList.Items[rand.Intn(len(nodeList.Items))]
|
||||
if len(node.Status.Addresses) > 0 {
|
||||
host = node.Status.Addresses[0].Address
|
||||
}
|
||||
case v1.ServiceTypeClusterIP:
|
||||
if svc.Spec.ClusterIP != "None" {
|
||||
host = svc.Spec.ClusterIP
|
||||
}
|
||||
default:
|
||||
return "", maskAny(fmt.Errorf("Unknown service type '%s' in service '%s.%s'", svc.Spec.Type, svc.GetName(), svc.GetNamespace()))
|
||||
}
|
||||
if host == "" {
|
||||
return "", maskAny(fmt.Errorf("Cannot find host for service '%s.%s'", svc.GetName(), svc.GetNamespace()))
|
||||
}
|
||||
if !strings.HasSuffix(scheme, "://") {
|
||||
scheme = scheme + "://"
|
||||
}
|
||||
return scheme + net.JoinHostPort(host, strconv.Itoa(int(port))), nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue