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 }