go.ligato.io/vpp-agent/v3@v3.5.0/plugins/linux/nsplugin/descriptor/microservice.go (about)

     1  // Copyright (c) 2018 Cisco and/or its affiliates.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at:
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package descriptor
    16  
    17  import (
    18  	"context"
    19  	"strings"
    20  	"sync"
    21  	"time"
    22  
    23  	docker "github.com/fsouza/go-dockerclient"
    24  	"github.com/pkg/errors"
    25  	"google.golang.org/protobuf/types/known/emptypb"
    26  
    27  	"go.ligato.io/cn-infra/v2/logging"
    28  	"go.ligato.io/cn-infra/v2/servicelabel"
    29  
    30  	kvs "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/api"
    31  
    32  	nsmodel "go.ligato.io/vpp-agent/v3/proto/ligato/linux/namespace"
    33  )
    34  
    35  const (
    36  	// MicroserviceDescriptorName is the name of the descriptor for microservices.
    37  	MicroserviceDescriptorName = "microservice"
    38  
    39  	// docker API keywords
    40  	dockerTypeContainer = "container"
    41  	dockerStateRunning  = "running"
    42  	dockerActionStart   = "start"
    43  	dockerActionStop    = "stop"
    44  )
    45  
    46  // MicroserviceDescriptor watches Docker and notifies KVScheduler about newly
    47  // started and stopped microservices.
    48  type MicroserviceDescriptor struct {
    49  	// input arguments
    50  	log         logging.Logger
    51  	kvscheduler kvs.KVScheduler
    52  
    53  	// map microservice label -> time of the last creation
    54  	createTime map[string]time.Time
    55  
    56  	// lock used to serialize access to microservice state data
    57  	msStateLock sync.Mutex
    58  
    59  	// conditional variable to check if microservice state data are in-sync
    60  	// with the docker
    61  	msStateInSync     bool
    62  	msStateInSyncCond *sync.Cond
    63  
    64  	// docker client - used to convert microservice label into the PID and
    65  	// ID of the container
    66  	dockerClient *docker.Client
    67  	// microservice label -> microservice state data
    68  	microServiceByLabel map[string]*Microservice
    69  	// microservice container ID -> microservice state data
    70  	microServiceByID map[string]*Microservice
    71  
    72  	// go routine management
    73  	ctx    context.Context
    74  	cancel context.CancelFunc
    75  	wg     sync.WaitGroup
    76  }
    77  
    78  // Microservice is used to store PID and ID of the container running a given
    79  // microservice.
    80  type Microservice struct {
    81  	Label string
    82  	PID   int
    83  	ID    string
    84  }
    85  
    86  // NewMicroserviceDescriptor creates a new instance of the descriptor for microservices.
    87  func NewMicroserviceDescriptor(kvscheduler kvs.KVScheduler, log logging.PluginLogger) (*MicroserviceDescriptor, error) {
    88  	var err error
    89  
    90  	descriptor := &MicroserviceDescriptor{
    91  		log:                 log.NewLogger("ms-descriptor"),
    92  		kvscheduler:         kvscheduler,
    93  		createTime:          make(map[string]time.Time),
    94  		microServiceByLabel: make(map[string]*Microservice),
    95  		microServiceByID:    make(map[string]*Microservice),
    96  	}
    97  	descriptor.msStateInSyncCond = sync.NewCond(&descriptor.msStateLock)
    98  	descriptor.ctx, descriptor.cancel = context.WithCancel(context.Background())
    99  
   100  	// Docker client
   101  	descriptor.dockerClient, err = docker.NewClientFromEnv()
   102  	if err != nil {
   103  		return nil, errors.Errorf("failed to get docker client instance from the environment variables: %v", err)
   104  	}
   105  	log.Debugf("Using docker client endpoint: %+v", descriptor.dockerClient.Endpoint())
   106  
   107  	return descriptor, nil
   108  }
   109  
   110  // GetDescriptor returns descriptor suitable for registration with the KVScheduler.
   111  func (d *MicroserviceDescriptor) GetDescriptor() *kvs.KVDescriptor {
   112  	return &kvs.KVDescriptor{
   113  		Name:        MicroserviceDescriptorName,
   114  		KeySelector: d.IsMicroserviceKey,
   115  		Retrieve:    d.Retrieve,
   116  	}
   117  }
   118  
   119  // IsMicroserviceKey returns true for key identifying microservices.
   120  func (d *MicroserviceDescriptor) IsMicroserviceKey(key string) bool {
   121  	return strings.HasPrefix(key, nsmodel.MicroserviceKeyPrefix)
   122  }
   123  
   124  // Retrieve returns key with empty value for every currently existing microservice.
   125  func (d *MicroserviceDescriptor) Retrieve(correlate []kvs.KVWithMetadata) (values []kvs.KVWithMetadata, err error) {
   126  	// wait until microservice state data are in-sync with the docker
   127  	d.msStateLock.Lock()
   128  	if !d.msStateInSync {
   129  		d.msStateInSyncCond.Wait()
   130  	}
   131  	defer d.msStateLock.Unlock()
   132  
   133  	for msLabel := range d.microServiceByLabel {
   134  		values = append(values, kvs.KVWithMetadata{
   135  			Key:    nsmodel.MicroserviceKey(msLabel),
   136  			Value:  &emptypb.Empty{},
   137  			Origin: kvs.FromSB,
   138  		})
   139  	}
   140  
   141  	return values, nil
   142  }
   143  
   144  // StartTracker starts microservice tracker,
   145  func (d *MicroserviceDescriptor) StartTracker() {
   146  	go d.trackMicroservices(d.ctx)
   147  }
   148  
   149  // StopTracker stops microservice tracker,
   150  func (d *MicroserviceDescriptor) StopTracker() {
   151  	d.cancel()
   152  	d.wg.Wait()
   153  }
   154  
   155  // GetMicroserviceStateData returns state data for the given microservice.
   156  func (d *MicroserviceDescriptor) GetMicroserviceStateData(msLabel string) (ms *Microservice, found bool) {
   157  	d.msStateLock.Lock()
   158  	if !d.msStateInSync {
   159  		d.msStateInSyncCond.Wait()
   160  	}
   161  	defer d.msStateLock.Unlock()
   162  
   163  	ms, found = d.microServiceByLabel[msLabel]
   164  	return ms, found
   165  }
   166  
   167  // detectMicroservice inspects container to see if it is a microservice.
   168  // If microservice is detected, processNewMicroservice() is called to process it.
   169  func (d *MicroserviceDescriptor) detectMicroservice(container *docker.Container) {
   170  	// Search for the microservice label.
   171  	var label string
   172  	for _, env := range container.Config.Env {
   173  		if strings.HasPrefix(env, servicelabel.MicroserviceLabelEnvVar+"=") {
   174  			label = env[len(servicelabel.MicroserviceLabelEnvVar)+1:]
   175  			if label != "" {
   176  				d.log.Debugf("detected container as microservice: Name=%v ID=%v Created=%v State.StartedAt=%v", container.Name, container.ID, container.Created, container.State.StartedAt)
   177  				last := d.createTime[label]
   178  				if last.After(container.Created) {
   179  					d.log.Debugf("ignoring older container created at %v as microservice: %+v", last, container)
   180  					continue
   181  				}
   182  				d.createTime[label] = container.Created
   183  				d.processNewMicroservice(label, container.ID, container.State.Pid)
   184  			}
   185  		}
   186  	}
   187  }
   188  
   189  // processNewMicroservice is triggered every time a new microservice gets freshly started. All pending interfaces are moved
   190  // to its namespace.
   191  func (d *MicroserviceDescriptor) processNewMicroservice(microserviceLabel string, id string, pid int) {
   192  	d.msStateLock.Lock()
   193  	defer d.msStateLock.Unlock()
   194  
   195  	ms, restarted := d.microServiceByLabel[microserviceLabel]
   196  	if restarted {
   197  		d.processTerminatedMicroservice(ms.ID)
   198  		d.log.WithFields(logging.Fields{"label": microserviceLabel, "new-pid": pid, "new-id": id}).
   199  			Warn("Microservice has been restarted")
   200  	} else {
   201  		d.log.WithFields(logging.Fields{"label": microserviceLabel, "pid": pid, "id": id}).
   202  			Debug("Discovered new microservice")
   203  	}
   204  
   205  	ms = &Microservice{Label: microserviceLabel, PID: pid, ID: id}
   206  	d.microServiceByLabel[microserviceLabel] = ms
   207  	d.microServiceByID[id] = ms
   208  
   209  	// Notify scheduler about new microservice
   210  	if d.msStateInSync {
   211  		if err := d.kvscheduler.PushSBNotification(kvs.KVWithMetadata{
   212  			Key:      nsmodel.MicroserviceKey(ms.Label),
   213  			Value:    &emptypb.Empty{},
   214  			Metadata: nil,
   215  		}); err != nil {
   216  			d.log.Errorf("pushing SB notification failed: %v", err)
   217  		}
   218  	}
   219  }
   220  
   221  // processTerminatedMicroservice is triggered every time a known microservice
   222  // has terminated. All associated interfaces become obsolete and are thus removed.
   223  func (d *MicroserviceDescriptor) processTerminatedMicroservice(id string) {
   224  	ms, exists := d.microServiceByID[id]
   225  	if !exists {
   226  		d.log.WithFields(logging.Fields{"id": id}).
   227  			Warn("Detected removal of an unknown microservice")
   228  		return
   229  	}
   230  	d.log.WithFields(logging.Fields{"label": ms.Label, "pid": ms.PID, "id": ms.ID}).
   231  		Debug("Microservice has terminated")
   232  
   233  	delete(d.microServiceByLabel, ms.Label)
   234  	delete(d.microServiceByID, ms.ID)
   235  
   236  	// Notify scheduler about terminated microservice
   237  	if d.msStateInSync {
   238  		if err := d.kvscheduler.PushSBNotification(kvs.KVWithMetadata{
   239  			Key:      nsmodel.MicroserviceKey(ms.Label),
   240  			Value:    nil,
   241  			Metadata: nil,
   242  		}); err != nil {
   243  			d.log.Errorf("pushing SB notification failed: %v", err)
   244  		}
   245  	}
   246  }
   247  
   248  // setStateInSync sets internal state to "in sync" and signals the state transition.
   249  func (d *MicroserviceDescriptor) setStateInSync() {
   250  	d.msStateLock.Lock()
   251  	d.msStateInSync = true
   252  	d.msStateLock.Unlock()
   253  	d.msStateInSyncCond.Broadcast()
   254  }
   255  
   256  // processStartedContainer processes a started Docker container - inspects whether it is a microservice.
   257  // If it is, notifies scheduler about a new microservice.
   258  func (d *MicroserviceDescriptor) processStartedContainer(id string) {
   259  	opts := docker.InspectContainerOptions{ID: id}
   260  	container, err := d.dockerClient.InspectContainerWithOptions(opts)
   261  	if err != nil {
   262  		d.log.Warnf("Error by inspecting container %s: %v", id, err)
   263  		return
   264  	}
   265  	d.detectMicroservice(container)
   266  }
   267  
   268  // processStoppedContainer processes a stopped Docker container - if it is a microservice,
   269  // notifies scheduler about its termination.
   270  func (d *MicroserviceDescriptor) processStoppedContainer(id string) {
   271  	d.msStateLock.Lock()
   272  	defer d.msStateLock.Unlock()
   273  
   274  	if _, found := d.microServiceByID[id]; found {
   275  		d.processTerminatedMicroservice(id)
   276  	}
   277  }
   278  
   279  // trackMicroservices is running in the background and maintains a map of microservice labels to container info.
   280  func (d *MicroserviceDescriptor) trackMicroservices(ctx context.Context) {
   281  	d.wg.Add(1)
   282  	defer func() {
   283  		d.wg.Done()
   284  		d.log.Debugf("Microservice tracking ended")
   285  	}()
   286  
   287  	// subscribe to Docker events
   288  	listener := make(chan *docker.APIEvents, 10)
   289  	err := d.dockerClient.AddEventListener(listener)
   290  	if err != nil {
   291  		d.log.Warnf("Failed to add Docker event listener: %v", err)
   292  		d.setStateInSync() // empty set of microservices is considered
   293  		return
   294  	}
   295  
   296  	// list currently running containers
   297  	listOpts := docker.ListContainersOptions{
   298  		All: true,
   299  	}
   300  	containers, err := d.dockerClient.ListContainers(listOpts)
   301  	if err != nil {
   302  		d.log.Warnf("Failed to list Docker containers: %v", err)
   303  		d.setStateInSync() // empty set of microservices is considered
   304  		return
   305  	}
   306  	for _, container := range containers {
   307  		if container.State == dockerStateRunning {
   308  			opts := docker.InspectContainerOptions{ID: container.ID}
   309  			details, err := d.dockerClient.InspectContainerWithOptions(opts)
   310  			if err != nil {
   311  				d.log.Warnf("Error by inspecting container %s: %v", container.ID, err)
   312  				continue
   313  			}
   314  			d.detectMicroservice(details)
   315  		}
   316  	}
   317  
   318  	// mark state data as in-sync
   319  	d.setStateInSync()
   320  
   321  	// process Docker events
   322  	for {
   323  		select {
   324  		case ev, ok := <-listener:
   325  			if !ok {
   326  				return
   327  			}
   328  			if ev.Type == dockerTypeContainer {
   329  				if ev.Action == dockerActionStart {
   330  					d.processStartedContainer(ev.Actor.ID)
   331  				}
   332  				if ev.Action == dockerActionStop {
   333  					d.processStoppedContainer(ev.Actor.ID)
   334  				}
   335  			}
   336  		case <-d.ctx.Done():
   337  			return
   338  		}
   339  	}
   340  }