// 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") }