youtubebeat/vendor/github.com/elastic/beats/libbeat/common/docker/watcher.go

395 lines
10 KiB
Go
Raw Normal View History

2018-11-18 11:08:38 +01:00
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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.
package docker
import (
"fmt"
"net/http"
"sync"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/go-connections/tlsconfig"
"golang.org/x/net/context"
"github.com/elastic/beats/libbeat/common/bus"
"github.com/elastic/beats/libbeat/logp"
)
// Select Docker API version
const (
shortIDLen = 12
)
// Watcher reads docker events and keeps a list of known containers
type Watcher interface {
// Start watching docker API for new containers
Start() error
// Stop watching docker API for new containers
Stop()
// Container returns the running container with the given ID or nil if unknown
Container(ID string) *Container
// Containers returns the list of known containers
Containers() map[string]*Container
// ListenStart returns a bus listener to receive container started events, with a `container` key holding it
ListenStart() bus.Listener
// ListenStop returns a bus listener to receive container stopped events, with a `container` key holding it
ListenStop() bus.Listener
}
// TLSConfig for docker socket connection
type TLSConfig struct {
CA string `config:"certificate_authority"`
Certificate string `config:"certificate"`
Key string `config:"key"`
}
type watcher struct {
sync.RWMutex
client Client
ctx context.Context
stop context.CancelFunc
containers map[string]*Container
deleted map[string]time.Time // deleted annotations key -> last access time
cleanupTimeout time.Duration
lastValidTimestamp int64
stopped sync.WaitGroup
bus bus.Bus
shortID bool // whether to store short ID in "containers" too
}
// Container info retrieved by the watcher
type Container struct {
ID string
Name string
Image string
Labels map[string]string
IPAddresses []string
Ports []types.Port
}
// Client for docker interface
type Client interface {
ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error)
ContainerInspect(ctx context.Context, container string) (types.ContainerJSON, error)
Events(ctx context.Context, options types.EventsOptions) (<-chan events.Message, <-chan error)
}
// WatcherConstructor represent a function that creates a new Watcher from giving parameters
type WatcherConstructor func(host string, tls *TLSConfig, storeShortID bool) (Watcher, error)
// NewWatcher returns a watcher running for the given settings
func NewWatcher(host string, tls *TLSConfig, storeShortID bool) (Watcher, error) {
var httpClient *http.Client
if tls != nil {
options := tlsconfig.Options{
CAFile: tls.CA,
CertFile: tls.Certificate,
KeyFile: tls.Key,
}
tlsc, err := tlsconfig.Client(options)
if err != nil {
return nil, err
}
httpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsc,
},
}
}
client, err := NewClient(host, httpClient, nil)
if err != nil {
return nil, err
}
return NewWatcherWithClient(client, 60*time.Second, storeShortID)
}
// NewWatcherWithClient creates a new Watcher from a given Docker client
func NewWatcherWithClient(client Client, cleanupTimeout time.Duration, storeShortID bool) (Watcher, error) {
ctx, cancel := context.WithCancel(context.Background())
return &watcher{
client: client,
ctx: ctx,
stop: cancel,
containers: make(map[string]*Container),
deleted: make(map[string]time.Time),
cleanupTimeout: cleanupTimeout,
bus: bus.New("docker"),
shortID: storeShortID,
}, nil
}
// Container returns the running container with the given ID or nil if unknown
func (w *watcher) Container(ID string) *Container {
w.RLock()
container := w.containers[ID]
if container == nil {
w.RUnlock()
return nil
}
_, ok := w.deleted[container.ID]
w.RUnlock()
// Update last access time if it's deleted
if ok {
w.Lock()
w.deleted[container.ID] = time.Now()
w.Unlock()
}
return container
}
// Containers returns the list of known containers
func (w *watcher) Containers() map[string]*Container {
w.RLock()
defer w.RUnlock()
res := make(map[string]*Container)
for k, v := range w.containers {
if !w.shortID || len(k) != shortIDLen {
res[k] = v
}
}
return res
}
// Start watching docker API for new containers
func (w *watcher) Start() error {
// Do initial scan of existing containers
logp.Debug("docker", "Start docker containers scanner")
w.lastValidTimestamp = time.Now().Unix()
w.Lock()
defer w.Unlock()
containers, err := w.listContainers(types.ContainerListOptions{})
if err != nil {
return err
}
for _, c := range containers {
w.containers[c.ID] = c
if w.shortID {
w.containers[c.ID[:shortIDLen]] = c
}
}
// Emit all start events (avoid blocking if the bus get's blocked)
go func() {
for _, c := range containers {
w.bus.Publish(bus.Event{
"start": true,
"container": c,
})
}
}()
w.stopped.Add(2)
go w.watch()
go w.cleanupWorker()
return nil
}
func (w *watcher) Stop() {
w.stop()
}
func (w *watcher) watch() {
filter := filters.NewArgs()
filter.Add("type", "container")
options := types.EventsOptions{
Since: fmt.Sprintf("%d", w.lastValidTimestamp),
Filters: filter,
}
for {
events, errors := w.client.Events(w.ctx, options)
WATCH:
for {
select {
case event := <-events:
logp.Debug("docker", "Got a new docker event: %v", event)
w.lastValidTimestamp = event.Time
// Add / update
if event.Action == "start" || event.Action == "update" {
filter := filters.NewArgs()
filter.Add("id", event.Actor.ID)
containers, err := w.listContainers(types.ContainerListOptions{
Filters: filter,
})
if err != nil || len(containers) != 1 {
logp.Err("Error getting container info: %v", err)
continue
}
container := containers[0]
w.Lock()
w.containers[event.Actor.ID] = container
if w.shortID {
w.containers[event.Actor.ID[:shortIDLen]] = container
}
// un-delete if it's flagged (in case of update or recreation)
delete(w.deleted, event.Actor.ID)
w.Unlock()
w.bus.Publish(bus.Event{
"start": true,
"container": container,
})
}
// Delete
if event.Action == "die" {
container := w.Container(event.Actor.ID)
if container != nil {
w.bus.Publish(bus.Event{
"stop": true,
"container": container,
})
}
w.Lock()
w.deleted[event.Actor.ID] = time.Now()
w.Unlock()
}
case err := <-errors:
// Restart watch call
logp.Err("Error watching for docker events: %v", err)
time.Sleep(1 * time.Second)
break WATCH
case <-w.ctx.Done():
logp.Debug("docker", "Watcher stopped")
w.stopped.Done()
return
}
}
}
}
func (w *watcher) listContainers(options types.ContainerListOptions) ([]*Container, error) {
containers, err := w.client.ContainerList(w.ctx, options)
if err != nil {
return nil, err
}
var result []*Container
for _, c := range containers {
var ipaddresses []string
for _, net := range c.NetworkSettings.Networks {
if net.IPAddress != "" {
ipaddresses = append(ipaddresses, net.IPAddress)
}
}
// If there are no network interfaces, assume that the container is on host network
// Inspect the container directly and use the hostname as the IP address in order
if len(ipaddresses) == 0 {
info, err := w.client.ContainerInspect(w.ctx, c.ID)
if err == nil {
ipaddresses = append(ipaddresses, info.Config.Hostname)
} else {
logp.Warn("unable to inspect container %s due to error %v", c.ID, err)
}
}
result = append(result, &Container{
ID: c.ID,
Name: c.Names[0][1:], // Strip '/' from container names
Image: c.Image,
Labels: c.Labels,
Ports: c.Ports,
IPAddresses: ipaddresses,
})
}
return result, nil
}
// Clean up deleted containers after they are not used anymore
func (w *watcher) cleanupWorker() {
for {
// Wait a full period
time.Sleep(w.cleanupTimeout)
select {
case <-w.ctx.Done():
w.stopped.Done()
return
default:
// Check entries for timeout
var toDelete []string
timeout := time.Now().Add(-w.cleanupTimeout)
w.RLock()
for key, lastSeen := range w.deleted {
if lastSeen.Before(timeout) {
logp.Debug("docker", "Removing container %s after cool down timeout", key)
toDelete = append(toDelete, key)
}
}
w.RUnlock()
// Delete timed out entries:
for _, key := range toDelete {
container := w.Container(key)
if container != nil {
w.bus.Publish(bus.Event{
"delete": true,
"container": container,
})
}
}
w.Lock()
for _, key := range toDelete {
delete(w.deleted, key)
delete(w.containers, key)
if w.shortID {
delete(w.containers, key[:shortIDLen])
}
}
w.Unlock()
}
}
}
// ListenStart returns a bus listener to receive container started events, with a `container` key holding it
func (w *watcher) ListenStart() bus.Listener {
return w.bus.Subscribe("start")
}
// ListenStop returns a bus listener to receive container stopped events, with a `container` key holding it
func (w *watcher) ListenStop() bus.Listener {
return w.bus.Subscribe("stop")
}