github.com/annwntech/go-micro/v2@v2.9.5/runtime/kubernetes/kubernetes.go (about) 1 // Package kubernetes implements kubernetes micro runtime 2 package kubernetes 3 4 import ( 5 "fmt" 6 "sync" 7 "time" 8 9 log "github.com/annwntech/go-micro/v2/logger" 10 "github.com/annwntech/go-micro/v2/runtime" 11 "github.com/annwntech/go-micro/v2/util/kubernetes/client" 12 ) 13 14 // action to take on runtime service 15 type action int 16 17 type kubernetes struct { 18 sync.RWMutex 19 // options configure runtime 20 options runtime.Options 21 // indicates if we're running 22 running bool 23 // used to stop the runtime 24 closed chan bool 25 // client is kubernetes client 26 client client.Client 27 // namespaces which exist 28 namespaces []client.Namespace 29 } 30 31 // namespaceExists returns a boolean indicating if a namespace exists 32 func (k *kubernetes) namespaceExists(name string) (bool, error) { 33 // populate the cache 34 if k.namespaces == nil { 35 namespaceList := new(client.NamespaceList) 36 resource := &client.Resource{Kind: "namespace", Value: namespaceList} 37 if err := k.client.List(resource); err != nil { 38 return false, err 39 } 40 k.namespaces = namespaceList.Items 41 } 42 43 // check if the namespace exists in the cache 44 for _, n := range k.namespaces { 45 if n.Metadata.Name == name { 46 return true, nil 47 } 48 } 49 50 return false, nil 51 } 52 53 // createNamespace creates a new k8s namespace 54 func (k *kubernetes) createNamespace(namespace string) error { 55 ns := client.Namespace{Metadata: &client.Metadata{Name: namespace}} 56 err := k.client.Create(&client.Resource{Kind: "namespace", Value: ns}) 57 58 // add to cache 59 if err == nil && k.namespaces != nil { 60 k.namespaces = append(k.namespaces, ns) 61 } 62 63 return err 64 } 65 66 // getService queries kubernetes for micro service 67 // NOTE: this function is not thread-safe 68 func (k *kubernetes) getService(labels map[string]string, opts ...client.GetOption) ([]*service, error) { 69 // get the service status 70 serviceList := new(client.ServiceList) 71 r := &client.Resource{ 72 Kind: "service", 73 Value: serviceList, 74 } 75 76 opts = append(opts, client.GetLabels(labels)) 77 78 // get the service from k8s 79 if err := k.client.Get(r, opts...); err != nil { 80 return nil, err 81 } 82 83 // get the deployment status 84 depList := new(client.DeploymentList) 85 d := &client.Resource{ 86 Kind: "deployment", 87 Value: depList, 88 } 89 if err := k.client.Get(d, opts...); err != nil { 90 return nil, err 91 } 92 93 // get the pods from k8s 94 podList := new(client.PodList) 95 p := &client.Resource{ 96 Kind: "pod", 97 Value: podList, 98 } 99 if err := k.client.Get(p, opts...); err != nil { 100 return nil, err 101 } 102 103 // service map 104 svcMap := make(map[string]*service) 105 106 // collect info from kubernetes service 107 for _, kservice := range serviceList.Items { 108 // name of the service 109 name := kservice.Metadata.Labels["name"] 110 // version of the service 111 version := kservice.Metadata.Labels["version"] 112 113 srv := &service{ 114 Service: &runtime.Service{ 115 Name: name, 116 Version: version, 117 Metadata: make(map[string]string), 118 }, 119 kservice: &kservice, 120 } 121 122 // set the address 123 address := kservice.Spec.ClusterIP 124 port := kservice.Spec.Ports[0] 125 srv.Service.Metadata["address"] = fmt.Sprintf("%s:%d", address, port.Port) 126 // set the type of service 127 srv.Service.Metadata["type"] = kservice.Metadata.Labels["micro"] 128 129 // copy annotations metadata into service metadata 130 for k, v := range kservice.Metadata.Annotations { 131 srv.Service.Metadata[k] = v 132 } 133 134 // save as service 135 svcMap[name+version] = srv 136 } 137 138 // collect additional info from kubernetes deployment 139 for _, kdep := range depList.Items { 140 // name of the service 141 name := kdep.Metadata.Labels["name"] 142 // versio of the service 143 version := kdep.Metadata.Labels["version"] 144 145 // access existing service map based on name + version 146 if svc, ok := svcMap[name+version]; ok { 147 // we're expecting our own service name in metadata 148 if _, ok := kdep.Metadata.Annotations["name"]; !ok { 149 continue 150 } 151 152 // set the service name, version and source 153 // based on existing annotations we stored 154 svc.Service.Name = kdep.Metadata.Annotations["name"] 155 svc.Service.Version = kdep.Metadata.Annotations["version"] 156 svc.Service.Source = kdep.Metadata.Annotations["source"] 157 158 // delete from metadata 159 delete(kdep.Metadata.Annotations, "name") 160 delete(kdep.Metadata.Annotations, "version") 161 delete(kdep.Metadata.Annotations, "source") 162 163 // copy all annotations metadata into service metadata 164 for k, v := range kdep.Metadata.Annotations { 165 svc.Service.Metadata[k] = v 166 } 167 168 // parse out deployment status and inject into service metadata 169 if len(kdep.Status.Conditions) > 0 { 170 svc.Status(kdep.Status.Conditions[0].Type, nil) 171 svc.Metadata["started"] = kdep.Status.Conditions[0].LastUpdateTime 172 } else { 173 svc.Status("n/a", nil) 174 } 175 176 // get the real status 177 for _, item := range podList.Items { 178 var status string 179 180 // check the name 181 if item.Metadata.Labels["name"] != name { 182 continue 183 } 184 // check the version 185 if item.Metadata.Labels["version"] != version { 186 continue 187 } 188 189 switch item.Status.Phase { 190 case "Failed": 191 status = item.Status.Reason 192 default: 193 status = item.Status.Phase 194 } 195 196 // skip if we can't get the container 197 if len(item.Status.Containers) == 0 { 198 continue 199 } 200 201 // now try get a deeper status 202 state := item.Status.Containers[0].State 203 204 // set start time 205 if state.Running != nil { 206 svc.Metadata["started"] = state.Running.Started 207 } 208 209 // set status from waiting 210 if v := state.Waiting; v != nil { 211 if len(v.Reason) > 0 { 212 status = v.Reason 213 } 214 } 215 // TODO: set from terminated 216 svc.Status(status, nil) 217 } 218 219 // save deployment 220 svc.kdeploy = &kdep 221 } 222 } 223 224 // collect all the services and return 225 services := make([]*service, 0, len(serviceList.Items)) 226 227 for _, service := range svcMap { 228 services = append(services, service) 229 } 230 231 return services, nil 232 } 233 234 // run runs the runtime management loop 235 func (k *kubernetes) run(events <-chan runtime.Event) { 236 t := time.NewTicker(time.Second * 10) 237 defer t.Stop() 238 239 for { 240 select { 241 case <-t.C: 242 // TODO: figure out what to do here 243 // - do we even need the ticker for k8s services? 244 case event := <-events: 245 // NOTE: we only handle Update events for now 246 if log.V(log.DebugLevel, log.DefaultLogger) { 247 log.Debugf("Runtime received notification event: %v", event) 248 } 249 switch event.Type { 250 case runtime.Update: 251 // only process if there's an actual service 252 // we do not update all the things individually 253 if event.Service == nil { 254 continue 255 } 256 257 // format the name 258 name := client.Format(event.Service.Name) 259 260 // set the default labels 261 labels := map[string]string{ 262 "micro": k.options.Type, 263 "name": name, 264 } 265 266 if len(event.Service.Version) > 0 { 267 labels["version"] = event.Service.Version 268 } 269 270 // get the deployment status 271 deployed := new(client.DeploymentList) 272 273 // get the existing service rather than creating a new one 274 err := k.client.Get(&client.Resource{ 275 Kind: "deployment", 276 Value: deployed, 277 }, client.GetLabels(labels)) 278 279 if err != nil { 280 if log.V(log.DebugLevel, log.DefaultLogger) { 281 log.Debugf("Runtime update failed to get service %s: %v", event.Service, err) 282 } 283 continue 284 } 285 286 // technically we should not receive multiple versions but hey ho 287 for _, service := range deployed.Items { 288 // check the name matches 289 if service.Metadata.Name != name { 290 continue 291 } 292 293 // update build time annotation 294 if service.Spec.Template.Metadata.Annotations == nil { 295 service.Spec.Template.Metadata.Annotations = make(map[string]string) 296 } 297 298 // update the build time 299 service.Spec.Template.Metadata.Annotations["updated"] = fmt.Sprintf("%d", event.Timestamp.Unix()) 300 301 if log.V(log.DebugLevel, log.DefaultLogger) { 302 log.Debugf("Runtime updating service: %s deployment: %s", event.Service, service.Metadata.Name) 303 } 304 if err := k.client.Update(deploymentResource(&service)); err != nil { 305 if log.V(log.DebugLevel, log.DefaultLogger) { 306 log.Debugf("Runtime failed to update service %s: %v", event.Service, err) 307 } 308 continue 309 } 310 } 311 } 312 case <-k.closed: 313 if log.V(log.DebugLevel, log.DefaultLogger) { 314 log.Debugf("Runtime stopped") 315 } 316 return 317 } 318 } 319 } 320 321 // Init initializes runtime options 322 func (k *kubernetes) Init(opts ...runtime.Option) error { 323 k.Lock() 324 defer k.Unlock() 325 326 for _, o := range opts { 327 o(&k.options) 328 } 329 330 return nil 331 } 332 333 func (k *kubernetes) Logs(s *runtime.Service, options ...runtime.LogsOption) (runtime.LogStream, error) { 334 klo := newLog(k.client, s.Name, options...) 335 stream, err := klo.Stream() 336 if err != nil { 337 return nil, err 338 } 339 // If requested, also read existing records and stream those too 340 if klo.options.Count > 0 { 341 go func() { 342 records, err := klo.Read() 343 if err != nil { 344 log.Errorf("Failed to get logs for service '%v' from k8s: %v", err) 345 return 346 } 347 // @todo: this might actually not run before podLogStream starts 348 // and might cause out of order log retrieval at the receiving end. 349 // A better approach would probably to suppor this inside the `klog.Stream` method. 350 for _, record := range records { 351 stream.Chan() <- record 352 } 353 }() 354 } 355 return stream, nil 356 } 357 358 type kubeStream struct { 359 // the k8s log stream 360 stream chan runtime.LogRecord 361 // the stop chan 362 sync.Mutex 363 stop chan bool 364 err error 365 } 366 367 func (k *kubeStream) Error() error { 368 return k.err 369 } 370 371 func (k *kubeStream) Chan() chan runtime.LogRecord { 372 return k.stream 373 } 374 375 func (k *kubeStream) Stop() error { 376 k.Lock() 377 defer k.Unlock() 378 select { 379 case <-k.stop: 380 return nil 381 default: 382 close(k.stop) 383 close(k.stream) 384 } 385 return nil 386 } 387 388 // Creates a service 389 func (k *kubernetes) Create(s *runtime.Service, opts ...runtime.CreateOption) error { 390 k.Lock() 391 defer k.Unlock() 392 393 options := runtime.CreateOptions{ 394 Type: k.options.Type, 395 Namespace: client.DefaultNamespace, 396 } 397 for _, o := range opts { 398 o(&options) 399 } 400 401 // default type if it doesn't exist 402 if len(options.Type) == 0 { 403 options.Type = k.options.Type 404 } 405 406 // default the source if it doesn't exist 407 if len(s.Source) == 0 { 408 s.Source = k.options.Source 409 } 410 411 // ensure the namespace exists 412 namespace := client.SerializeResourceName(options.Namespace) 413 // only do this if the namespace is not default 414 if namespace != "default" { 415 if exist, err := k.namespaceExists(namespace); err == nil && !exist { 416 if err := k.createNamespace(namespace); err != nil { 417 return err 418 } 419 } else if err != nil { 420 return err 421 } 422 } 423 424 // determine the image from the source and options 425 options.Image = k.getImage(s, options) 426 427 // create new service 428 service := newService(s, options) 429 430 // start the service 431 return service.Start(k.client, client.CreateNamespace(options.Namespace)) 432 } 433 434 // Read returns all instances of given service 435 func (k *kubernetes) Read(opts ...runtime.ReadOption) ([]*runtime.Service, error) { 436 k.Lock() 437 defer k.Unlock() 438 439 // set the default labels 440 labels := map[string]string{} 441 442 options := runtime.ReadOptions{ 443 Namespace: client.DefaultNamespace, 444 } 445 446 for _, o := range opts { 447 o(&options) 448 } 449 450 if len(options.Service) > 0 { 451 labels["name"] = client.Format(options.Service) 452 } 453 454 // add version to labels if a version has been supplied 455 if len(options.Version) > 0 { 456 labels["version"] = options.Version 457 } 458 459 if len(options.Type) > 0 { 460 labels["micro"] = options.Type 461 } 462 463 srvs, err := k.getService(labels, client.GetNamespace(options.Namespace)) 464 if err != nil { 465 return nil, err 466 } 467 468 var services []*runtime.Service 469 for _, service := range srvs { 470 services = append(services, service.Service) 471 } 472 473 return services, nil 474 } 475 476 // Update the service in place 477 func (k *kubernetes) Update(s *runtime.Service, opts ...runtime.UpdateOption) error { 478 options := runtime.UpdateOptions{ 479 Namespace: client.DefaultNamespace, 480 } 481 482 for _, o := range opts { 483 o(&options) 484 } 485 486 labels := map[string]string{} 487 488 if len(s.Name) > 0 { 489 labels["name"] = client.Format(s.Name) 490 } 491 492 if len(s.Version) > 0 { 493 labels["version"] = s.Version 494 } 495 496 // get the existing service 497 services, err := k.getService(labels) 498 if err != nil { 499 return err 500 } 501 502 // update the relevant services 503 for _, service := range services { 504 // nil check 505 if service.kdeploy.Metadata == nil || service.kdeploy.Metadata.Annotations == nil { 506 md := new(client.Metadata) 507 md.Annotations = make(map[string]string) 508 service.kdeploy.Metadata = md 509 } 510 511 // update metadata 512 for k, v := range s.Metadata { 513 service.kdeploy.Metadata.Annotations[k] = v 514 } 515 516 // update build time annotation 517 service.kdeploy.Spec.Template.Metadata.Annotations["updated"] = fmt.Sprintf("%d", time.Now().Unix()) 518 519 // update the service 520 if err := service.Update(k.client, client.UpdateNamespace(options.Namespace)); err != nil { 521 return err 522 } 523 } 524 525 return nil 526 } 527 528 // Delete removes a service 529 func (k *kubernetes) Delete(s *runtime.Service, opts ...runtime.DeleteOption) error { 530 options := runtime.DeleteOptions{ 531 Namespace: client.DefaultNamespace, 532 } 533 534 for _, o := range opts { 535 o(&options) 536 } 537 538 k.Lock() 539 defer k.Unlock() 540 541 // create new kubernetes micro service 542 service := newService(s, runtime.CreateOptions{ 543 Type: k.options.Type, 544 Namespace: options.Namespace, 545 }) 546 547 return service.Stop(k.client, client.DeleteNamespace(options.Namespace)) 548 } 549 550 // Start starts the runtime 551 func (k *kubernetes) Start() error { 552 k.Lock() 553 defer k.Unlock() 554 555 // already running 556 if k.running { 557 return nil 558 } 559 560 // set running 561 k.running = true 562 k.closed = make(chan bool) 563 564 var events <-chan runtime.Event 565 if k.options.Scheduler != nil { 566 var err error 567 events, err = k.options.Scheduler.Notify() 568 if err != nil { 569 // TODO: should we bail here? 570 if log.V(log.DebugLevel, log.DefaultLogger) { 571 log.Debugf("Runtime failed to start update notifier") 572 } 573 } 574 } 575 576 go k.run(events) 577 578 return nil 579 } 580 581 // Stop shuts down the runtime 582 func (k *kubernetes) Stop() error { 583 k.Lock() 584 defer k.Unlock() 585 586 if !k.running { 587 return nil 588 } 589 590 select { 591 case <-k.closed: 592 return nil 593 default: 594 close(k.closed) 595 // set not running 596 k.running = false 597 // stop the scheduler 598 if k.options.Scheduler != nil { 599 return k.options.Scheduler.Close() 600 } 601 } 602 603 return nil 604 } 605 606 // String implements stringer interface 607 func (k *kubernetes) String() string { 608 return "kubernetes" 609 } 610 611 // NewRuntime creates new kubernetes runtime 612 func NewRuntime(opts ...runtime.Option) runtime.Runtime { 613 // get default options 614 options := runtime.Options{ 615 // Create labels with type "micro": "service" 616 Type: "service", 617 } 618 619 // apply requested options 620 for _, o := range opts { 621 o(&options) 622 } 623 624 // kubernetes client 625 client := client.NewClusterClient() 626 627 return &kubernetes{ 628 options: options, 629 closed: make(chan bool), 630 client: client, 631 } 632 } 633 634 func (k *kubernetes) getImage(s *runtime.Service, options runtime.CreateOptions) string { 635 // use the image when its specified 636 if len(options.Image) > 0 { 637 return options.Image 638 } 639 640 if len(k.options.Image) > 0 { 641 return k.options.Image 642 } 643 644 return "" 645 }