Explorar o código

establish global websockets instead of setting up websockets per chart.

sunguroku %!s(int64=5) %!d(string=hai) anos
pai
achega
83f4d31a1d

+ 36 - 108
dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx

@@ -1,6 +1,5 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
-import api from '../../../../shared/api';
 
 import { ChartType, StorageType } from '../../../../shared/types';
 import { Context } from '../../../../shared/Context';
@@ -8,19 +7,34 @@ import { Context } from '../../../../shared/Context';
 type PropsType = {
   chart: ChartType,
   setCurrentChart: (c: ChartType) => void
+  controllers: Record<string, any>,
 };
 
 type StateType = {
   expand: boolean,
   controllers: Record<string, boolean>,
-  websockets: Record<string, any>, 
+  update: any[],
+  getAvailability: Function,
 };
 
 export default class Chart extends Component<PropsType, StateType> {
+  getAvailability = (kind: string, c: any) => {
+    switch (kind.toLowerCase()) {
+      case "deployment":
+      case "replicaset":
+        return (c.status.availableReplicas == c.status.replicas)
+      case "statefulset":
+       return (c.status.readyReplicas == c.status.replicas)
+      case "daemonset":
+        return (c.status.numberAvailable == c.status.desiredNumberScheduled)
+      }
+  }
+
   state = {
     expand: false,
     controllers: {} as Record<string, boolean>,
-    websockets : [] as any[],
+    update: [] as any[],
+    getAvailability: this.getAvailability.bind(this)
   }
 
   renderIcon = () => {
@@ -40,31 +54,19 @@ export default class Chart extends Component<PropsType, StateType> {
     return `${time} on ${date}`;
   }
 
-  getAvailability = (kind: string, c: any) => {
-    switch (kind.toLowerCase()) {
-      case "deployment":
-      case "replicaset":
-        return (c.status.availableReplicas == c.status.replicas)
-      case "statefulset":
-       return (c.status.readyReplicas == c.status.replicas)
-      case "daemonset":
-        return (c.status.numberAvailable == c.status.desiredNumberScheduled)
-      }
-  }
-
-  setControllerStatus = (cs: any[]) => {
+  setControllerStatus = (cs: Record<string, any>) => {
     let controllers = {} as Record<string, boolean>;
-    cs.map((c) => {
-      controllers[c.metadata.uid] = this.getAvailability(c.kind, c)
-    })
-    this.setState({ controllers })
+    for (var uid in cs) {
+      let value = cs[uid];
+      controllers[uid] = this.getAvailability(value.kind, value);
+    }
+    this.setState({ controllers });
   }
 
   getChartStatus = (chartStatus: string) => {
     if (chartStatus === 'deployed') {
       for (var uid in this.state.controllers) {
         if (!this.state.controllers[uid]) {
-          console.log(this.props.chart.name, uid)
           return 'updating'
         }
       }
@@ -73,97 +75,23 @@ export default class Chart extends Component<PropsType, StateType> {
     return chartStatus
   }
 
-  setupWebsocket = async (uid: string, namespace: string, name: string, kind: string) => {
-    return new Promise((resolve, reject) => {
-      let { currentCluster, currentProject } = this.context;
-      let ws = new WebSocket(`ws://localhost:8080/api/projects/${currentProject.id}/k8s/${namespace}/${kind}/${name}/status?cluster_id=${currentCluster.id}&service_account_id=${currentCluster.service_account_id}`)
-      ws.onopen = () => {
-        console.log('connected to websocket')
-      }
-  
-      ws.onmessage = (evt: MessageEvent) => {
-        let event = JSON.parse(evt.data)
-        let object = event.Object
-        const { chart } = this.props;
-        if (!this.state.controllers[object.metadata.uid]) {
-          return;
-        }
-        this.setState({
-          controllers: {
-            ...this.state.controllers,
-            [object.metadata.uid]: this.getAvailability(event.Kind, object) 
-          }
-        })
-      }
-  
-      ws.onclose = () => {
-        console.log('closing websocket')
-      }
-  
-      ws.onerror = (err: ErrorEvent) => {
-        console.log(err)
-        ws.close()
-      }
-  
-      if (!this.state.websockets) {
-        reject("Cannot establish websocket connection for controllers.")
-      };
-  
-      this.setState({
-        websockets: {
-          ...this.state.websockets,
-          [uid]: ws
-        }
-      }, () => {
-        resolve()
-      })
-    })
-  }
+  static getDerivedStateFromProps(nextProps: any, prevState: any) {
+    let controllers = {} as Record<string, boolean>;
+    
+    for (var uid in nextProps.controllers) {
+      let controller = nextProps.controllers[uid]
+      controllers[uid] = prevState.getAvailability(controller.kind, controller)
+    }
 
-  setControllerWebsockets = (controllers: any[]) => {
-    let { setCurrentError } = this.context;
-    controllers.forEach(async (c: any) => {
-      this.setupWebsocket(c.metadata.uid, c.metadata.namespace || "default", 
-        c.metadata.name, c.kind)
-      .catch(setCurrentError)
-    })
+    return {
+      controllers,
+    };
   }
 
   componentDidMount () {
-    let { currentCluster, currentProject, setCurrentError } = this.context;
-    const { chart } = this.props;
-
-    if (chart.info.status == 'failed') return; 
-
-    api.getChartControllers('<token>', {
-      namespace: chart.namespace,
-      cluster_id: currentCluster.id,
-      service_account_id: currentCluster.service_account_id,
-      storage: StorageType.Secret
-    }, {
-      id: currentProject.id,
-      name: chart.name,
-      revision: chart.version
-    }, (err: any, res: any) => {
-      if (err) {
-        setCurrentError(JSON.stringify(err));
-        return
-      }
-      console.log(res.data)
-      this.setControllerStatus(res.data)
-      this.setControllerWebsockets(res.data)
-    });
-  }
-
-  async componentWillUnmount () {
-    if (this.state.websockets) {
-      for (var uid in this.state.websockets) {
-        await new Promise(next => {
-          this.state.websockets[uid].close()
-          next()
-        })
-      }
-    }
+    const { chart, controllers } = this.props;
+    if (chart.info.status == 'failed') return;
+    this.setControllerStatus(controllers)
   }
 
   render() {

+ 108 - 26
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -16,22 +16,25 @@ type PropsType = {
 
 type StateType = {
   charts: ChartType[],
+  chartLookupTable: Record<string, string>,
+  controllers: Record<string, Record<string, any>>,
   loading: boolean,
   error: boolean,
-  ws: any,
+  websockets: Record<string, any>,
 };
 
 export default class ChartList extends Component<PropsType, StateType> {
   state = {
     charts: [] as ChartType[],
+    chartLookupTable: {} as Record<string, string>,
+    controllers: {} as Record<string, Record<string, any>>,
     loading: false,
     error: false,
-    ws : null as any
+    websockets : {} as Record<string, any>,
   }
 
-  updateCharts = () => {
+  updateCharts = (callback: Function) => {
     let { currentCluster, currentProject, setCurrentError } = this.context;
-
     this.setState({ loading: true });
     setTimeout(() => {
       if (this.state.loading) {
@@ -50,13 +53,14 @@ export default class ChartList extends Component<PropsType, StateType> {
       statusFilter: ['deployed', 'uninstalled', 'pending', 'pending_upgrade',
         'pending_rollback','superseded','failed']
     }, { id: currentProject.id }, (err: any, res: any) => {
-        if (err) {
+      if (err) {
         console.log(err)
         setCurrentError(JSON.stringify(err));
         this.setState({ loading: false, error: true });
       } else {
         if (res.data) {
           this.setState({ charts: res.data });
+          callback(res.data)
         } else {
           this.setState({ charts: [] });
         }
@@ -65,36 +69,113 @@ export default class ChartList extends Component<PropsType, StateType> {
     });
   }
 
-  setupWebsocket = () => {
-    let { currentCluster, currentProject } = this.context;
-    let ws = new WebSocket(`ws://localhost:8080`)
-
-    this.setState({ ws }, () => {
-      if (!this.state.ws) return;
-  
-      this.state.ws.onopen = () => {
+  setupWebsocket = (kind: string) => {
+      let { currentCluster, currentProject } = this.context;
+      let ws = new WebSocket(`ws://localhost:8080/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}&service_account_id=${currentCluster.service_account_id}`)
+      ws.onopen = () => {
         console.log('connected to websocket')
       }
   
-      this.state.ws.onmessage = (evt: MessageEvent) => {
-        console.log(evt.data)
+      ws.onmessage = (evt: MessageEvent) => {
+        let event = JSON.parse(evt.data)
+        let object = event.Object
+        let chartKey = this.state.chartLookupTable[object.metadata.uid]
+
+        // ignore if updated object does not belong to any chart in the list.
+        if (!chartKey) {
+          return;
+        }
+
+        let chartControllers = this.state.controllers[chartKey]
+        chartControllers[object.metadata.uid] = object
+
+        this.setState({
+          controllers: {
+            ...this.state.controllers,
+            [chartKey] : chartControllers
+          }
+        })
+      }
+  
+      ws.onclose = () => {
+        console.log('closing websocket')
       }
   
-      this.state.ws.onerror = (err: ErrorEvent) => {
+      ws.onerror = (err: ErrorEvent) => {
         console.log(err)
+        ws.close()
       }
+
+      return ws
+  }
+
+  setControllerWebsockets = (controllers: any[]) => {
+    let websockets = controllers.map((kind: string) => {
+      return this.setupWebsocket(kind)
+    })
+    this.setState({websockets})
+  }
+
+  getControllers = (charts: any[]) => {
+    let { currentCluster, currentProject, setCurrentError } = this.context;
+
+    charts.forEach(async (chart: any) => {
+      // don't retrieve controllers for chart that failed to even deploy.
+      if (chart.info.status == 'failed') return;
+
+      await new Promise(next => {
+        api.getChartControllers('<token>', {
+          namespace: chart.namespace,
+          cluster_id: currentCluster.id,
+          service_account_id: currentCluster.service_account_id,
+          storage: StorageType.Secret
+        }, {
+          id: currentProject.id,
+          name: chart.name,
+          revision: chart.version
+        }, (err: any, res: any) => {
+          if (err) {
+            setCurrentError(JSON.stringify(err));
+            return
+          }
+          // transform controller array into hash table for easy lookup during updates.
+          let chartControllers = {} as Record<string, Record<string, any>>
+          res.data.forEach((c: any) => {
+            chartControllers[c.metadata.uid] = c
+          })
+
+          res.data.forEach(async (c: any) => {
+            await new Promise(nextController => {
+              this.setState({
+                chartLookupTable: {
+                  ...this.state.chartLookupTable,
+                  [c.metadata.uid] : `${chart.namespace}-${chart.name}`
+                },
+                controllers: {
+                  ...this.state.controllers,
+                  [`${chart.namespace}-${chart.name}`] : chartControllers
+                }
+              }, () => {
+                nextController();
+              })
+            })
+          })
+          next();
+        });
+      })
     })
   }
 
   componentDidMount() {
-    this.updateCharts();
-    // this.setupWebsocket();
+    this.updateCharts(this.getControllers);
+    this.setControllerWebsockets(["deployment", "statefulset", "daemonset", "replicaset"]);
   }
 
-  componentWillUnmount() {
-    if (this.state.ws) {
-      console.log('closing websocket')
-      this.state.ws.close()
+  async componentWillUnmount () {
+    if (this.state.websockets) {
+      this.state.websockets.forEach((ws: WebSocket) => {
+        ws.close()
+      })
     }
   }
 
@@ -103,7 +184,7 @@ export default class ChartList extends Component<PropsType, StateType> {
     // Ret2: Prevents reload when opening ClusterConfigModal
     if (prevProps.currentCluster !== this.props.currentCluster || 
       prevProps.namespace !== this.props.namespace) {
-      this.updateCharts();
+      this.updateCharts(this.getControllers);
     }
   }
 
@@ -126,12 +207,13 @@ export default class ChartList extends Component<PropsType, StateType> {
       );
     }
 
-    return this.state.charts.map((x: ChartType, i: number) => {
+    return this.state.charts.map((chart: ChartType, i: number) => {
       return (
         <Chart
-          key={i}
-          chart={x}
+          key={`${chart.namespace}-${chart.name}`}
+          chart={chart}
           setCurrentChart={this.props.setCurrentChart}
+          controllers={this.state.controllers[`${chart.namespace}-${chart.name}`] || {} as Record<string, any>}
         />
       )
     })

+ 2 - 4
internal/kubernetes/agent.go

@@ -154,12 +154,10 @@ func (a *Agent) GetPodLogs(namespace string, name string, conn *websocket.Conn)
 
 // StreamControllerStatus streams controller status. Supports Deployment, StatefulSet, ReplicaSet, and DaemonSet
 // TODO: Support Jobs
-func (a *Agent) StreamControllerStatus(conn *websocket.Conn, namespace string,
-	name string, kind string) error {
-	factory := informers.NewSharedInformerFactoryWithOptions(
+func (a *Agent) StreamControllerStatus(conn *websocket.Conn, kind string) error {
+	factory := informers.NewSharedInformerFactory(
 		a.Clientset,
 		10,
-		informers.WithNamespace(namespace),
 	)
 	var informer cache.SharedInformer
 

+ 1 - 4
server/api/k8s_handler.go

@@ -263,11 +263,8 @@ func (app *App) HandleStreamControllerStatus(w http.ResponseWriter, r *http.Requ
 	}
 
 	// get path parameters
-	namespace := chi.URLParam(r, "namespace")
-	name := chi.URLParam(r, "name")
 	kind := chi.URLParam(r, "kind")
-
-	err = agent.StreamControllerStatus(conn, namespace, name, kind)
+	err = agent.StreamControllerStatus(conn, kind)
 
 	if err != nil {
 		app.handleErrorWebsocketWrite(err, w)

+ 1 - 1
server/router/router.go

@@ -304,7 +304,7 @@ func New(
 
 		r.Method(
 			"GET",
-			"/projects/{project_id}/k8s/{namespace}/{kind}/{name}/status",
+			"/projects/{project_id}/k8s/{kind}/status",
 			auth.DoesUserHaveProjectAccess(
 				auth.DoesUserHaveServiceAccountAccess(
 					requestlog.NewHandler(a.HandleStreamControllerStatus, l),