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  }