2017-06-08 14:51:02 +00:00
|
|
|
package connector
|
2016-12-30 22:17:46 +00:00
|
|
|
|
|
|
|
import (
|
2017-05-15 10:54:35 +00:00
|
|
|
"fmt"
|
2020-11-17 09:42:45 +00:00
|
|
|
"github.com/op/go-logging"
|
2017-03-03 07:57:26 +00:00
|
|
|
"strings"
|
2017-03-10 09:10:39 +00:00
|
|
|
"sync"
|
2017-01-12 19:24:12 +00:00
|
|
|
|
2017-06-12 14:12:03 +00:00
|
|
|
"github.com/bcicen/ctop/connector/collector"
|
2017-11-22 14:27:38 +00:00
|
|
|
"github.com/bcicen/ctop/connector/manager"
|
2017-06-08 14:51:02 +00:00
|
|
|
"github.com/bcicen/ctop/container"
|
2017-06-08 15:01:08 +00:00
|
|
|
api "github.com/fsouza/go-dockerclient"
|
2016-12-30 22:17:46 +00:00
|
|
|
)
|
|
|
|
|
2018-01-29 12:47:10 +00:00
|
|
|
func init() { enabled["docker"] = NewDocker }
|
|
|
|
|
2020-11-17 19:50:25 +00:00
|
|
|
var actionToStatus = map[string]string{
|
|
|
|
"start": "running",
|
2020-11-18 20:56:33 +00:00
|
|
|
"die": "exited",
|
2020-11-17 19:50:25 +00:00
|
|
|
"stop": "exited",
|
|
|
|
"pause": "paused",
|
|
|
|
"unpause": "running",
|
|
|
|
}
|
|
|
|
|
2020-11-17 20:00:03 +00:00
|
|
|
type StatusUpdate struct {
|
|
|
|
Cid string
|
|
|
|
Field string // "status" or "health"
|
|
|
|
Status string
|
|
|
|
}
|
|
|
|
|
2017-06-08 15:01:08 +00:00
|
|
|
type Docker struct {
|
|
|
|
client *api.Client
|
2017-06-08 14:51:02 +00:00
|
|
|
containers map[string]*container.Container
|
2017-03-05 06:46:41 +00:00
|
|
|
needsRefresh chan string // container IDs requiring refresh
|
2020-11-17 20:00:03 +00:00
|
|
|
statuses chan StatusUpdate
|
2019-05-22 16:58:55 +00:00
|
|
|
closed chan struct{}
|
2017-03-10 09:10:39 +00:00
|
|
|
lock sync.RWMutex
|
2017-02-23 02:01:56 +00:00
|
|
|
}
|
|
|
|
|
2019-05-22 16:58:55 +00:00
|
|
|
func NewDocker() (Connector, error) {
|
2017-01-01 22:42:13 +00:00
|
|
|
// init docker client
|
2017-06-08 15:01:08 +00:00
|
|
|
client, err := api.NewClientFromEnv()
|
2017-01-01 22:42:13 +00:00
|
|
|
if err != nil {
|
2019-05-22 16:58:55 +00:00
|
|
|
return nil, err
|
2017-01-01 22:42:13 +00:00
|
|
|
}
|
2017-06-08 15:01:08 +00:00
|
|
|
cm := &Docker{
|
2017-02-24 01:18:59 +00:00
|
|
|
client: client,
|
2017-06-08 14:51:02 +00:00
|
|
|
containers: make(map[string]*container.Container),
|
2017-03-05 06:46:41 +00:00
|
|
|
needsRefresh: make(chan string, 60),
|
2020-11-17 20:00:03 +00:00
|
|
|
statuses: make(chan StatusUpdate, 60),
|
2019-05-22 16:58:55 +00:00
|
|
|
closed: make(chan struct{}),
|
2017-03-10 09:10:39 +00:00
|
|
|
lock: sync.RWMutex{},
|
2016-12-30 22:17:46 +00:00
|
|
|
}
|
2019-05-22 16:58:55 +00:00
|
|
|
|
|
|
|
// query info as pre-flight healthcheck
|
|
|
|
info, err := client.Info()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("docker-connector ID: %s", info.ID)
|
|
|
|
log.Debugf("docker-connector Driver: %s", info.Driver)
|
|
|
|
log.Debugf("docker-connector Images: %d", info.Images)
|
|
|
|
log.Debugf("docker-connector Name: %s", info.Name)
|
|
|
|
log.Debugf("docker-connector ServerVersion: %s", info.ServerVersion)
|
|
|
|
|
2017-02-26 21:12:28 +00:00
|
|
|
go cm.Loop()
|
2020-11-17 20:00:03 +00:00
|
|
|
go cm.LoopStatuses()
|
2017-03-09 07:39:11 +00:00
|
|
|
cm.refreshAll()
|
2017-02-24 09:10:14 +00:00
|
|
|
go cm.watchEvents()
|
2019-05-22 16:58:55 +00:00
|
|
|
return cm, nil
|
2016-12-30 22:17:46 +00:00
|
|
|
}
|
|
|
|
|
2019-05-22 16:58:55 +00:00
|
|
|
// Docker implements Connector
|
|
|
|
func (cm *Docker) Wait() struct{} { return <-cm.closed }
|
|
|
|
|
2017-02-24 01:18:59 +00:00
|
|
|
// Docker events watcher
|
2017-06-08 15:01:08 +00:00
|
|
|
func (cm *Docker) watchEvents() {
|
2017-02-24 01:18:59 +00:00
|
|
|
log.Info("docker event listener starting")
|
2017-06-08 15:01:08 +00:00
|
|
|
events := make(chan *api.APIEvents)
|
2021-02-04 07:22:57 +00:00
|
|
|
opts := api.EventsOptions{Filters: map[string][]string{
|
|
|
|
"type": {"container"},
|
|
|
|
"event": {"create", "start", "health_status", "pause", "unpause", "stop", "die", "destroy"},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
cm.client.AddEventListenerWithOptions(opts, events)
|
2017-01-26 00:53:03 +00:00
|
|
|
|
2017-02-24 01:18:59 +00:00
|
|
|
for e := range events {
|
2020-11-17 10:06:34 +00:00
|
|
|
actionName := e.Action
|
2017-08-27 23:29:59 +00:00
|
|
|
switch actionName {
|
2020-11-17 19:50:25 +00:00
|
|
|
// most frequent event is a health checks
|
2021-02-04 07:37:44 +00:00
|
|
|
case "health_status: healthy", "health_status: unhealthy":
|
|
|
|
sepIdx := strings.Index(actionName, ": ")
|
2020-11-17 19:50:25 +00:00
|
|
|
healthStatus := e.Action[sepIdx+2:]
|
|
|
|
if log.IsEnabledFor(logging.DEBUG) {
|
|
|
|
log.Debugf("handling docker event: action=health_status id=%s %s", e.ID, healthStatus)
|
|
|
|
}
|
2020-11-17 20:00:03 +00:00
|
|
|
cm.statuses <- StatusUpdate{e.ID, "health", healthStatus}
|
2020-11-17 19:50:25 +00:00
|
|
|
case "create":
|
2020-11-17 09:42:45 +00:00
|
|
|
if log.IsEnabledFor(logging.DEBUG) {
|
2020-11-17 19:50:25 +00:00
|
|
|
log.Debugf("handling docker event: action=create id=%s", e.ID)
|
2020-11-17 09:42:45 +00:00
|
|
|
}
|
2017-03-05 06:46:41 +00:00
|
|
|
cm.needsRefresh <- e.ID
|
2017-02-24 09:10:14 +00:00
|
|
|
case "destroy":
|
2020-11-17 09:42:45 +00:00
|
|
|
if log.IsEnabledFor(logging.DEBUG) {
|
|
|
|
log.Debugf("handling docker event: action=destroy id=%s", e.ID)
|
|
|
|
}
|
2017-02-26 21:12:28 +00:00
|
|
|
cm.delByID(e.ID)
|
2020-11-17 19:50:25 +00:00
|
|
|
default:
|
|
|
|
// check if this action changes status e.g. start -> running
|
|
|
|
status := actionToStatus[actionName]
|
|
|
|
if status != "" {
|
|
|
|
if log.IsEnabledFor(logging.DEBUG) {
|
|
|
|
log.Debugf("handling docker event: action=%s id=%s %s", actionName, e.ID, status)
|
|
|
|
}
|
2020-11-17 20:00:03 +00:00
|
|
|
cm.statuses <- StatusUpdate{e.ID, "status", status}
|
2020-11-17 19:50:25 +00:00
|
|
|
}
|
2017-02-24 09:10:14 +00:00
|
|
|
}
|
2017-02-24 01:18:59 +00:00
|
|
|
}
|
2019-05-22 16:58:55 +00:00
|
|
|
log.Info("docker event listener exited")
|
|
|
|
close(cm.closed)
|
2017-02-24 01:18:59 +00:00
|
|
|
}
|
2017-02-23 02:01:56 +00:00
|
|
|
|
2017-06-08 15:01:08 +00:00
|
|
|
func portsFormat(ports map[api.Port][]api.PortBinding) string {
|
2017-05-15 10:54:35 +00:00
|
|
|
var exposed []string
|
|
|
|
var published []string
|
|
|
|
|
2017-05-15 04:52:38 +00:00
|
|
|
for k, v := range ports {
|
2017-05-15 10:54:35 +00:00
|
|
|
if len(v) == 0 {
|
|
|
|
exposed = append(exposed, string(k))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
for _, binding := range v {
|
2017-06-18 20:17:56 +00:00
|
|
|
s := fmt.Sprintf("%s:%s -> %s", binding.HostIP, binding.HostPort, k)
|
2017-05-15 10:54:35 +00:00
|
|
|
published = append(published, s)
|
2017-05-15 04:52:38 +00:00
|
|
|
}
|
|
|
|
}
|
2017-05-15 10:54:35 +00:00
|
|
|
|
|
|
|
return strings.Join(append(exposed, published...), "\n")
|
2017-05-15 04:52:38 +00:00
|
|
|
}
|
|
|
|
|
2018-09-06 19:01:16 +00:00
|
|
|
func ipsFormat(networks map[string]api.ContainerNetwork) string {
|
|
|
|
var ips []string
|
|
|
|
|
|
|
|
for k, v := range networks {
|
|
|
|
s := fmt.Sprintf("%s:%s", k, v.IPAddress)
|
|
|
|
ips = append(ips, s)
|
|
|
|
}
|
|
|
|
|
|
|
|
return strings.Join(ips, "\n")
|
|
|
|
}
|
|
|
|
|
2017-06-08 15:01:08 +00:00
|
|
|
func (cm *Docker) refresh(c *container.Container) {
|
2020-11-20 17:44:09 +00:00
|
|
|
insp, found, failed := cm.inspect(c.Id)
|
|
|
|
if failed {
|
|
|
|
return
|
|
|
|
}
|
2017-02-24 01:18:59 +00:00
|
|
|
// remove container if no longer exists
|
2020-11-20 17:44:09 +00:00
|
|
|
if !found {
|
2017-03-05 06:46:41 +00:00
|
|
|
cm.delByID(c.Id)
|
2017-02-24 01:18:59 +00:00
|
|
|
return
|
|
|
|
}
|
2017-03-06 08:25:59 +00:00
|
|
|
c.SetMeta("name", shortName(insp.Name))
|
2017-03-06 22:05:04 +00:00
|
|
|
c.SetMeta("image", insp.Config.Image)
|
2018-09-06 19:01:16 +00:00
|
|
|
c.SetMeta("IPs", ipsFormat(insp.NetworkSettings.Networks))
|
2017-05-15 04:52:38 +00:00
|
|
|
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
|
2017-03-06 22:05:04 +00:00
|
|
|
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
|
2017-08-23 19:38:14 +00:00
|
|
|
c.SetMeta("health", insp.State.Health.Status)
|
2020-12-09 21:06:14 +00:00
|
|
|
c.SetMeta("[ENV-VAR]", strings.Join(insp.Config.Env, ";"))
|
2017-02-24 01:18:59 +00:00
|
|
|
c.SetState(insp.State.Status)
|
|
|
|
}
|
2017-02-23 02:24:26 +00:00
|
|
|
|
2020-11-20 17:44:09 +00:00
|
|
|
func (cm *Docker) inspect(id string) (insp *api.Container, found bool, failed bool) {
|
2017-02-24 01:18:59 +00:00
|
|
|
c, err := cm.client.InspectContainer(id)
|
|
|
|
if err != nil {
|
2020-11-20 17:44:09 +00:00
|
|
|
if _, notFound := err.(*api.NoSuchContainer); notFound {
|
|
|
|
return c, false, false
|
2017-02-23 02:01:56 +00:00
|
|
|
}
|
2020-11-20 17:44:09 +00:00
|
|
|
// other error e.g. connection failed
|
|
|
|
log.Errorf("%s (%T)", err.Error(), err)
|
|
|
|
return c, false, true
|
2017-01-26 00:53:03 +00:00
|
|
|
}
|
2020-11-20 17:44:09 +00:00
|
|
|
return c, true, false
|
2017-02-24 01:18:59 +00:00
|
|
|
}
|
|
|
|
|
2017-02-26 21:12:28 +00:00
|
|
|
// Mark all container IDs for refresh
|
2017-06-08 15:01:08 +00:00
|
|
|
func (cm *Docker) refreshAll() {
|
|
|
|
opts := api.ListContainersOptions{All: true}
|
2017-02-24 01:18:59 +00:00
|
|
|
allContainers, err := cm.client.ListContainers(opts)
|
|
|
|
if err != nil {
|
2019-05-22 16:58:55 +00:00
|
|
|
log.Errorf("%s (%T)", err.Error(), err)
|
|
|
|
return
|
2017-02-24 01:18:59 +00:00
|
|
|
}
|
2017-01-26 00:53:03 +00:00
|
|
|
|
2017-03-05 06:46:41 +00:00
|
|
|
for _, i := range allContainers {
|
|
|
|
c := cm.MustGet(i.ID)
|
2017-03-06 08:25:59 +00:00
|
|
|
c.SetMeta("name", shortName(i.Names[0]))
|
2017-03-05 06:46:41 +00:00
|
|
|
c.SetState(i.State)
|
|
|
|
cm.needsRefresh <- c.Id
|
2017-02-24 01:18:59 +00:00
|
|
|
}
|
2017-01-01 22:42:13 +00:00
|
|
|
}
|
|
|
|
|
2017-06-08 15:01:08 +00:00
|
|
|
func (cm *Docker) Loop() {
|
2019-05-22 16:58:55 +00:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case id := <-cm.needsRefresh:
|
|
|
|
c := cm.MustGet(id)
|
|
|
|
cm.refresh(c)
|
|
|
|
case <-cm.closed:
|
|
|
|
return
|
|
|
|
}
|
2017-01-09 15:02:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-17 20:00:03 +00:00
|
|
|
func (cm *Docker) LoopStatuses() {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case statusUpdate := <-cm.statuses:
|
|
|
|
c, _ := cm.Get(statusUpdate.Cid)
|
|
|
|
if c != nil {
|
|
|
|
if statusUpdate.Field == "health" {
|
|
|
|
c.SetMeta("health", statusUpdate.Status)
|
|
|
|
} else {
|
|
|
|
c.SetState(statusUpdate.Status)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case <-cm.closed:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-07 02:33:29 +00:00
|
|
|
// MustGet gets a single container, creating one anew if not existing
|
2017-06-08 15:01:08 +00:00
|
|
|
func (cm *Docker) MustGet(id string) *container.Container {
|
2017-03-05 06:46:41 +00:00
|
|
|
c, ok := cm.Get(id)
|
|
|
|
// append container struct for new containers
|
|
|
|
if !ok {
|
|
|
|
// create collector
|
2017-06-12 14:12:03 +00:00
|
|
|
collector := collector.NewDocker(cm.client, id)
|
2017-11-20 11:09:36 +00:00
|
|
|
// create manager
|
|
|
|
manager := manager.NewDocker(cm.client, id)
|
2017-03-05 06:46:41 +00:00
|
|
|
// create container
|
2017-11-20 11:09:36 +00:00
|
|
|
c = container.New(id, collector, manager)
|
2017-03-10 09:10:39 +00:00
|
|
|
cm.lock.Lock()
|
2017-03-05 06:46:41 +00:00
|
|
|
cm.containers[id] = c
|
2017-03-10 09:10:39 +00:00
|
|
|
cm.lock.Unlock()
|
2017-03-05 06:46:41 +00:00
|
|
|
}
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
2019-05-22 16:58:55 +00:00
|
|
|
// Docker implements Connector
|
2017-06-08 15:01:08 +00:00
|
|
|
func (cm *Docker) Get(id string) (*container.Container, bool) {
|
2017-03-10 09:10:39 +00:00
|
|
|
cm.lock.Lock()
|
2017-03-05 06:46:41 +00:00
|
|
|
c, ok := cm.containers[id]
|
2017-03-10 09:10:39 +00:00
|
|
|
cm.lock.Unlock()
|
2017-03-05 06:46:41 +00:00
|
|
|
return c, ok
|
2016-12-30 22:17:46 +00:00
|
|
|
}
|
|
|
|
|
2017-02-24 01:18:59 +00:00
|
|
|
// Remove containers by ID
|
2017-06-08 15:01:08 +00:00
|
|
|
func (cm *Docker) delByID(id string) {
|
2017-03-10 09:10:39 +00:00
|
|
|
cm.lock.Lock()
|
2017-03-05 06:46:41 +00:00
|
|
|
delete(cm.containers, id)
|
2017-03-10 09:10:39 +00:00
|
|
|
cm.lock.Unlock()
|
2017-03-05 06:46:41 +00:00
|
|
|
log.Infof("removed dead container: %s", id)
|
2017-01-26 00:53:03 +00:00
|
|
|
}
|
|
|
|
|
2019-05-22 16:58:55 +00:00
|
|
|
// Docker implements Connector
|
2017-06-08 15:01:08 +00:00
|
|
|
func (cm *Docker) All() (containers container.Containers) {
|
2017-03-10 09:10:39 +00:00
|
|
|
cm.lock.Lock()
|
2017-03-05 06:46:41 +00:00
|
|
|
for _, c := range cm.containers {
|
|
|
|
containers = append(containers, c)
|
|
|
|
}
|
2017-08-23 19:38:14 +00:00
|
|
|
|
2017-06-12 13:40:52 +00:00
|
|
|
containers.Sort()
|
2017-03-08 00:26:22 +00:00
|
|
|
containers.Filter()
|
2017-06-10 14:09:21 +00:00
|
|
|
cm.lock.Unlock()
|
2017-03-05 06:46:41 +00:00
|
|
|
return containers
|
2017-03-03 07:57:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// use primary container name
|
|
|
|
func shortName(name string) string {
|
2020-11-25 17:15:32 +00:00
|
|
|
return strings.TrimPrefix(name, "/")
|
2017-03-03 07:57:26 +00:00
|
|
|
}
|