diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 2094e257ba..1b654a9343 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -322,6 +322,14 @@ export function receiveApiDetails(apiDetails) { }); } +export function receiveControlNodeRemoved(nodeId) { + AppDispatcher.dispatch({ + type: ActionTypes.RECEIVE_CONTROL_NODE_REMOVED, + nodeId + }); + updateRoute(); +} + export function receiveControlPipeFromParams(pipeId, rawTty) { // TODO add nodeId AppDispatcher.dispatch({ diff --git a/client/app/scripts/components/node-details/node-details-controls.js b/client/app/scripts/components/node-details/node-details-controls.js index d3028e74d5..703d5f79e1 100644 --- a/client/app/scripts/components/node-details/node-details-controls.js +++ b/client/app/scripts/components/node-details/node-details-controls.js @@ -1,4 +1,5 @@ import React from 'react'; +import _ from 'lodash'; import NodeDetailsControlButton from './node-details-control-button'; @@ -17,7 +18,7 @@ export default function NodeDetailsControls({controls, error, nodeId, pending}) {error} } - {controls && controls.map(control => )} {controls && } diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index b6290f2fb5..7731438299 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -27,6 +27,7 @@ const ACTION_TYPES = [ 'PIN_METRIC', 'UNPIN_METRIC', 'OPEN_WEBSOCKET', + 'RECEIVE_CONTROL_NODE_REMOVED', 'RECEIVE_CONTROL_PIPE', 'RECEIVE_CONTROL_PIPE_STATUS', 'RECEIVE_NODE_DETAILS', diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index b9d15d2dc6..cce23cb647 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -561,6 +561,11 @@ export class AppStore extends Store { this.__emitChange(); break; } + case ActionTypes.RECEIVE_CONTROL_NODE_REMOVED: { + closeNodeDetails(payload.nodeId); + this.__emitChange(); + break; + } case ActionTypes.RECEIVE_CONTROL_PIPE: { controlPipes = controlPipes.set(payload.pipeId, makeOrderedMap({ id: payload.pipeId, diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index ddd0a5f9fe..885adb271c 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -3,8 +3,8 @@ import reqwest from 'reqwest'; import { clearControlError, closeWebsocket, openWebsocket, receiveError, receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError, - receiveControlPipe, receiveControlPipeStatus, receiveControlSuccess, - receiveTopologies, receiveNotFound } from '../actions/app-actions'; + receiveControlNodeRemoved, receiveControlPipe, receiveControlPipeStatus, + receiveControlSuccess, receiveTopologies, receiveNotFound } from '../actions/app-actions'; import { API_INTERVAL, TOPOLOGY_INTERVAL } from '../constants/timer'; @@ -184,8 +184,13 @@ export function doControlRequest(nodeId, control) { url, success: (res) => { receiveControlSuccess(nodeId); - if (res && res.pipe) { - receiveControlPipe(res.pipe, nodeId, res.raw_tty, true); + if (res) { + if (res.pipe) { + receiveControlPipe(res.pipe, nodeId, res.raw_tty, true); + } + if (res.removedNode) { + receiveControlNodeRemoved(nodeId); + } } }, error: (err) => { diff --git a/common/xfer/controls.go b/common/xfer/controls.go index 49a0f35add..244d70add2 100644 --- a/common/xfer/controls.go +++ b/common/xfer/controls.go @@ -18,10 +18,15 @@ type Request struct { // Response is the Probe -> App -> UI message type for the control RPCs. type Response struct { - Value interface{} `json:"value,omitempty"` - Error string `json:"error,omitempty"` - Pipe string `json:"pipe,omitempty"` - RawTTY bool `json:"raw_tty,omitempty"` + Value interface{} `json:"value,omitempty"` + Error string `json:"error,omitempty"` + + // Pipe specific fields + Pipe string `json:"pipe,omitempty"` + RawTTY bool `json:"raw_tty,omitempty"` + + // Remove specific fields + RemovedNode string `json:"removedNode,omitempty"` // Set if node was removed } // Message is the unions of Request, Response and arbitrary Value. diff --git a/probe/docker/container.go b/probe/docker/container.go index ac16d03779..c5387e910f 100644 --- a/probe/docker/container.go +++ b/probe/docker/container.go @@ -394,7 +394,7 @@ func (c *container) GetNode(localAddrs []net.IP) report.Node { RestartContainer, StopContainer, PauseContainer, AttachContainer, ExecContainer, ) } else { - result = result.WithControls(StartContainer) + result = result.WithControls(StartContainer, RemoveContainer) } result = result.AddTable(LabelPrefix, c.container.Config.Labels) diff --git a/probe/docker/controls.go b/probe/docker/controls.go index 23b8d23ba2..de2f4fe30f 100644 --- a/probe/docker/controls.go +++ b/probe/docker/controls.go @@ -17,6 +17,7 @@ const ( RestartContainer = "docker_restart_container" PauseContainer = "docker_pause_container" UnpauseContainer = "docker_unpause_container" + RemoveContainer = "docker_remove_container" AttachContainer = "docker_attach_container" ExecContainer = "docker_exec_container" @@ -48,6 +49,18 @@ func (r *registry) unpauseContainer(containerID string, _ xfer.Request) xfer.Res return xfer.ResponseError(r.client.UnpauseContainer(containerID)) } +func (r *registry) removeContainer(containerID string, _ xfer.Request) xfer.Response { + log.Infof("Removing container %s", containerID) + if err := r.client.RemoveContainer(docker_client.RemoveContainerOptions{ + ID: containerID, + }); err != nil { + return xfer.ResponseError(err) + } + return xfer.Response{ + RemovedNode: containerID, + } +} + func (r *registry) attachContainer(containerID string, req xfer.Request) xfer.Response { c, ok := r.GetContainer(containerID) if !ok { @@ -156,6 +169,7 @@ func (r *registry) registerControls() { controls.Register(RestartContainer, captureContainerID(r.restartContainer)) controls.Register(PauseContainer, captureContainerID(r.pauseContainer)) controls.Register(UnpauseContainer, captureContainerID(r.unpauseContainer)) + controls.Register(RemoveContainer, captureContainerID(r.removeContainer)) controls.Register(AttachContainer, captureContainerID(r.attachContainer)) controls.Register(ExecContainer, captureContainerID(r.execContainer)) } @@ -166,6 +180,7 @@ func (r *registry) deregisterControls() { controls.Rm(RestartContainer) controls.Rm(PauseContainer) controls.Rm(UnpauseContainer) + controls.Rm(RemoveContainer) controls.Rm(AttachContainer) controls.Rm(ExecContainer) } diff --git a/probe/docker/registry.go b/probe/docker/registry.go index 45df659688..1ee0c4ca53 100644 --- a/probe/docker/registry.go +++ b/probe/docker/registry.go @@ -70,6 +70,7 @@ type Client interface { RestartContainer(string, uint) error PauseContainer(string) error UnpauseContainer(string) error + RemoveContainer(docker_client.RemoveContainerOptions) error AttachToContainerNonBlocking(docker_client.AttachToContainerOptions) (docker_client.CloseWaiter, error) CreateExec(docker_client.CreateExecOptions) (*docker_client.Exec, error) StartExecNonBlocking(string, docker_client.StartExecOptions) (docker_client.CloseWaiter, error) diff --git a/probe/docker/registry_test.go b/probe/docker/registry_test.go index 4b5609f200..e3239adcc2 100644 --- a/probe/docker/registry_test.go +++ b/probe/docker/registry_test.go @@ -138,6 +138,10 @@ func (m *mockDockerClient) UnpauseContainer(_ string) error { return fmt.Errorf("unpaused") } +func (m *mockDockerClient) RemoveContainer(_ client.RemoveContainerOptions) error { + return fmt.Errorf("remove") +} + type mockCloseWaiter struct{} func (mockCloseWaiter) Close() error { return nil } diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go index a0c22e7747..2843e2fff7 100644 --- a/probe/docker/reporter.go +++ b/probe/docker/reporter.go @@ -101,39 +101,52 @@ func (r *Reporter) containerTopology(localAddrs []net.IP) report.Topology { WithMetricTemplates(ContainerMetricTemplates). WithTableTemplates(ContainerTableTemplates) result.Controls.AddControl(report.Control{ - ID: StopContainer, - Human: "Stop", - Icon: "fa-stop", + ID: AttachContainer, + Human: "Attach", + Icon: "fa-desktop", + Rank: 1, + }) + result.Controls.AddControl(report.Control{ + ID: ExecContainer, + Human: "Exec shell", + Icon: "fa-terminal", + Rank: 2, }) result.Controls.AddControl(report.Control{ ID: StartContainer, Human: "Start", Icon: "fa-play", + Rank: 3, }) result.Controls.AddControl(report.Control{ ID: RestartContainer, Human: "Restart", Icon: "fa-repeat", + Rank: 4, }) result.Controls.AddControl(report.Control{ ID: PauseContainer, Human: "Pause", Icon: "fa-pause", + Rank: 5, }) result.Controls.AddControl(report.Control{ ID: UnpauseContainer, Human: "Unpause", Icon: "fa-play", + Rank: 6, }) result.Controls.AddControl(report.Control{ - ID: AttachContainer, - Human: "Attach", - Icon: "fa-desktop", + ID: StopContainer, + Human: "Stop", + Icon: "fa-stop", + Rank: 7, }) result.Controls.AddControl(report.Control{ - ID: ExecContainer, - Human: "Exec shell", - Icon: "fa-terminal", + ID: RemoveContainer, + Human: "Remove", + Icon: "fa-trash-o", + Rank: 8, }) metadata := map[string]string{report.ControlProbeID: r.probeID} diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go index d27a68731b..45d646a02e 100644 --- a/probe/kubernetes/reporter.go +++ b/probe/kubernetes/reporter.go @@ -129,6 +129,7 @@ func (r *Reporter) podTopology(services []Service) (report.Topology, report.Topo ID: GetLogs, Human: "Get logs", Icon: "fa-desktop", + Rank: 0, }) for _, service := range services { selectors[service.ID()] = service.Selector() diff --git a/render/detailed/node.go b/render/detailed/node.go index 7ab8de2939..11e110e350 100644 --- a/render/detailed/node.go +++ b/render/detailed/node.go @@ -45,6 +45,7 @@ type wiredControlInstance struct { ID string `json:"id"` Human string `json:"human"` Icon string `json:"icon"` + Rank int `json:"rank"` } // CodecEncodeSelf marshals this ControlInstance. It takes the basic Metric @@ -56,6 +57,7 @@ func (c *ControlInstance) CodecEncodeSelf(encoder *codec.Encoder) { ID: c.Control.ID, Human: c.Control.Human, Icon: c.Control.Icon, + Rank: c.Control.Rank, }) } @@ -70,6 +72,7 @@ func (c *ControlInstance) CodecDecodeSelf(decoder *codec.Decoder) { ID: in.ID, Human: in.Human, Icon: in.Icon, + Rank: in.Rank, }, } } diff --git a/report/controls.go b/report/controls.go index e4c3d243b2..4c294aa0ad 100644 --- a/report/controls.go +++ b/report/controls.go @@ -16,6 +16,7 @@ type Control struct { ID string `json:"id"` Human string `json:"human"` Icon string `json:"icon"` // from https://fortawesome.github.io/Font-Awesome/cheatsheet/ please + Rank int `json:"rank"` } // Merge merges other with cs, returning a fresh Controls.