github.com/tickoalcantara12/micro/v3@v3.0.0-20221007104245-9d75b9bcbab9/service/runtime/kubernetes/kubernetes.go (about)

     1  // Licensed under the Apache License, Version 2.0 (the "License");
     2  // you may not use this file except in compliance with the License.
     3  // You may obtain a copy of the License at
     4  //
     5  //     https://www.apache.org/licenses/LICENSE-2.0
     6  //
     7  // Unless required by applicable law or agreed to in writing, software
     8  // distributed under the License is distributed on an "AS IS" BASIS,
     9  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    10  // See the License for the specific language governing permissions and
    11  // limitations under the License.
    12  //
    13  // Original source: github.com/micro/go-micro/v3/runtime/kubernetes/kubernetes.go
    14  
    15  // Package kubernetes implements kubernetes micro runtime
    16  package kubernetes
    17  
    18  import (
    19  	"fmt"
    20  	"sync"
    21  	"time"
    22  
    23  	"github.com/tickoalcantara12/micro/v3/service/logger"
    24  	"github.com/tickoalcantara12/micro/v3/service/runtime"
    25  	"github.com/tickoalcantara12/micro/v3/service/runtime/kubernetes/api"
    26  	"github.com/tickoalcantara12/micro/v3/service/runtime/kubernetes/client"
    27  )
    28  
    29  var (
    30  	DefaultServiceResources = &runtime.Resources{
    31  		Mem:  200,
    32  		Disk: 2000,
    33  		// explicitly not doing CPU here
    34  	}
    35  
    36  	DefaultImage = "ghcr.io/micro/cells:v3"
    37  )
    38  
    39  // action to take on runtime service
    40  type action int
    41  
    42  type kubernetes struct {
    43  	sync.Mutex
    44  	// options configure runtime
    45  	options runtime.Options
    46  	// client is kubernetes client
    47  	client client.Client
    48  	// namespaces which exist
    49  	namespaces []client.Namespace
    50  }
    51  
    52  // Init initializes runtime options
    53  func (k *kubernetes) Init(opts ...runtime.Option) error {
    54  	for _, o := range opts {
    55  		o(&k.options)
    56  	}
    57  	return nil
    58  }
    59  
    60  func (k *kubernetes) Logs(resource runtime.Resource, options ...runtime.LogsOption) (runtime.LogStream, error) {
    61  	// Handle the various different types of resources:
    62  	switch resource.Type() {
    63  	case runtime.TypeNamespace:
    64  		// noop (Namespace is not supported by *kubernetes.Logs())
    65  		return nil, nil
    66  	case runtime.TypeNetworkPolicy:
    67  		// noop (NetworkPolicy is not supported by *kubernetes.Logs()))
    68  		return nil, nil
    69  	case runtime.TypeResourceQuota:
    70  		// noop (ResourceQuota is not supported by *kubernetes.Logs()))
    71  		return nil, nil
    72  	case runtime.TypeService:
    73  		// Assert the resource back into a *runtime.Service
    74  		s, ok := resource.(*runtime.Service)
    75  		if !ok {
    76  			return nil, runtime.ErrInvalidResource
    77  		}
    78  
    79  		klo := newLog(k.client, s.Name, s.Version, options...)
    80  
    81  		// if its not a stream then read the records, return and close
    82  		if !klo.options.Stream {
    83  			records, err := klo.Read()
    84  			if err != nil {
    85  				logger.Errorf("Failed to get logs for service '%v' from k8s: %v", s.Name, err)
    86  				return nil, err
    87  			}
    88  
    89  			// create a stream even though we're not streaming
    90  			kstream := &kubeStream{
    91  				// make a stream buffer of size records
    92  				stream: make(chan runtime.Log, len(records)),
    93  				stop:   make(chan bool),
    94  			}
    95  
    96  			// load the records
    97  			for _, record := range records {
    98  				kstream.stream <- record
    99  			}
   100  
   101  			// close the stream so it doesn't block
   102  			close(kstream.stream)
   103  
   104  			return kstream, nil
   105  		}
   106  
   107  		// otherwise stream the logs
   108  		stream, err := klo.Stream()
   109  		if err != nil {
   110  			return nil, err
   111  		}
   112  
   113  		return stream, nil
   114  	default:
   115  		return nil, runtime.ErrInvalidResource
   116  	}
   117  }
   118  
   119  type kubeStream struct {
   120  	// the k8s log stream
   121  	stream chan runtime.Log
   122  	// the stop chan
   123  	sync.Mutex
   124  	stop chan bool
   125  	err  error
   126  }
   127  
   128  func (k *kubeStream) Error() error {
   129  	return k.err
   130  }
   131  
   132  func (k *kubeStream) Chan() chan runtime.Log {
   133  	return k.stream
   134  }
   135  
   136  func (k *kubeStream) Stop() error {
   137  	k.Lock()
   138  	defer k.Unlock()
   139  
   140  	select {
   141  	case <-k.stop:
   142  		return nil
   143  	default:
   144  		close(k.stop)
   145  	}
   146  
   147  	return nil
   148  }
   149  
   150  // Create a resource
   151  func (k *kubernetes) Create(resource runtime.Resource, opts ...runtime.CreateOption) error {
   152  	k.Lock()
   153  	defer k.Unlock()
   154  	return k.create(resource, opts...)
   155  }
   156  
   157  func (k *kubernetes) create(resource runtime.Resource, opts ...runtime.CreateOption) error {
   158  	// parse the options
   159  	options := &runtime.CreateOptions{
   160  		Type:      k.options.Type,
   161  		Image:     k.options.Image,
   162  		Namespace: client.DefaultNamespace,
   163  	}
   164  	for _, o := range opts {
   165  		o(options)
   166  	}
   167  
   168  	// Handle the various different types of resources:
   169  	switch resource.Type() {
   170  	case runtime.TypeNamespace:
   171  		// Assert the resource back into a *runtime.Namespace
   172  		namespace, ok := resource.(*runtime.Namespace)
   173  		if !ok {
   174  			return runtime.ErrInvalidResource
   175  		}
   176  		return k.createNamespace(namespace)
   177  	case runtime.TypeNetworkPolicy:
   178  		// Assert the resource back into a *runtime.NetworkPolicy
   179  		networkPolicy, ok := resource.(*runtime.NetworkPolicy)
   180  		if !ok {
   181  			return runtime.ErrInvalidResource
   182  		}
   183  		return k.createNetworkPolicy(networkPolicy)
   184  	case runtime.TypeResourceQuota:
   185  		// Assert the resource back into a *runtime.ResourceQuota
   186  		resourceQuota, ok := resource.(*runtime.ResourceQuota)
   187  		if !ok {
   188  			return runtime.ErrInvalidResource
   189  		}
   190  		return k.createResourceQuota(resourceQuota)
   191  	case runtime.TypeService:
   192  		// Assert the resource back into a *runtime.Service
   193  		s, ok := resource.(*runtime.Service)
   194  		if !ok {
   195  			return runtime.ErrInvalidResource
   196  		}
   197  
   198  		// default the service's source and version
   199  		if len(s.Source) == 0 {
   200  			s.Source = k.options.Source
   201  		}
   202  		if len(s.Version) == 0 {
   203  			s.Version = "latest"
   204  		}
   205  
   206  		// ensure the namespace exists
   207  		if err := k.ensureNamepaceExists(options.Namespace); err != nil {
   208  			return nil
   209  		}
   210  
   211  		// create a secret for the deployment
   212  		if err := k.createCredentials(s, options); err != nil {
   213  			return err
   214  		}
   215  
   216  		// create some default resource requests
   217  		if options.Resources == nil && options.Namespace != "micro" {
   218  			options.Resources = DefaultServiceResources
   219  		}
   220  
   221  		if len(options.Image) == 0 {
   222  			options.Image = DefaultImage
   223  		}
   224  
   225  		// create the deployment and set the runtime class name if provided
   226  		dep := client.NewDeployment(s, options)
   227  		if rcn := getRuntimeClassName(k.options.Context); len(rcn) > 0 {
   228  			dep.Value.(*client.Deployment).Spec.Template.PodSpec.RuntimeClassName = rcn
   229  			logger.Infof("Setting runtime class name to %v", rcn)
   230  		}
   231  
   232  		// create the deployment
   233  		if err := k.client.Create(dep, client.CreateNamespace(options.Namespace)); err != nil {
   234  			if parseError(err).Reason == "AlreadyExists" {
   235  				return runtime.ErrAlreadyExists
   236  			}
   237  			if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
   238  				logger.Errorf("Runtime failed to create deployment: %v", err)
   239  			}
   240  			return err
   241  		}
   242  
   243  		// create the service, one could already exist for another version so ignore ErrAlreadyExists
   244  		if err := k.client.Create(client.NewService(s, options), client.CreateNamespace(options.Namespace)); err != nil {
   245  			if parseError(err).Reason == "AlreadyExists" {
   246  				return nil
   247  			}
   248  			if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
   249  				logger.Errorf("Runtime failed to create service: %v", err)
   250  			}
   251  			return err
   252  		}
   253  
   254  		return nil
   255  	default:
   256  		return runtime.ErrInvalidResource
   257  	}
   258  }
   259  
   260  // Read returns all instances of given service
   261  func (k *kubernetes) Read(opts ...runtime.ReadOption) ([]*runtime.Service, error) {
   262  	k.Lock()
   263  	defer k.Unlock()
   264  
   265  	// parse the options
   266  	options := runtime.ReadOptions{
   267  		Namespace: client.DefaultNamespace,
   268  	}
   269  	for _, o := range opts {
   270  		o(&options)
   271  	}
   272  
   273  	// construct the query
   274  	labels := map[string]string{}
   275  	if len(options.Service) > 0 {
   276  		labels["name"] = client.Format(options.Service)
   277  	}
   278  	if len(options.Version) > 0 {
   279  		labels["version"] = client.Format(options.Version)
   280  	}
   281  	if len(options.Type) > 0 {
   282  		labels["micro"] = client.Format(options.Type)
   283  	}
   284  
   285  	// lookup all the serivces which match this query, if one service has two different versions,
   286  	// they'll be returned as two seperate resullts
   287  	return k.getServices(client.GetNamespace(options.Namespace), client.GetLabels(labels))
   288  }
   289  
   290  // Update a resource in place
   291  func (k *kubernetes) Update(resource runtime.Resource, opts ...runtime.UpdateOption) error {
   292  	k.Lock()
   293  	defer k.Unlock()
   294  
   295  	// parse the options
   296  	options := runtime.UpdateOptions{
   297  		Namespace: client.DefaultNamespace,
   298  	}
   299  	for _, o := range opts {
   300  		o(&options)
   301  	}
   302  
   303  	// Handle the various different types of resources:
   304  	switch resource.Type() {
   305  	case runtime.TypeNamespace:
   306  		// noop (Namespace is not supported by *kubernetes.Update())
   307  		return nil
   308  	case runtime.TypeNetworkPolicy:
   309  		// Assert the resource back into a *runtime.NetworkPolicy
   310  		networkPolicy, ok := resource.(*runtime.NetworkPolicy)
   311  		if !ok {
   312  			return runtime.ErrInvalidResource
   313  		}
   314  		return k.updateNetworkPolicy(networkPolicy)
   315  	case runtime.TypeResourceQuota:
   316  		// Assert the resource back into a *runtime.ResourceQuota
   317  		resourceQuota, ok := resource.(*runtime.ResourceQuota)
   318  		if !ok {
   319  			return runtime.ErrInvalidResource
   320  		}
   321  		return k.updateResourceQuota(resourceQuota)
   322  	case runtime.TypeService:
   323  
   324  		// Assert the resource back into a *runtime.Service
   325  		s, ok := resource.(*runtime.Service)
   326  		if !ok {
   327  			return runtime.ErrInvalidResource
   328  		}
   329  
   330  		// construct the query
   331  		labels := map[string]string{}
   332  		if len(s.Name) > 0 {
   333  			labels["name"] = client.Format(s.Name)
   334  		}
   335  		if len(s.Version) > 0 {
   336  			labels["version"] = client.Format(s.Version)
   337  		}
   338  
   339  		// get the existing deployments
   340  		depList := new(client.DeploymentList)
   341  		d := &client.Resource{
   342  			Kind:  "deployment",
   343  			Value: depList,
   344  		}
   345  		depOpts := []client.GetOption{
   346  			client.GetNamespace(options.Namespace),
   347  			client.GetLabels(labels),
   348  		}
   349  		if err := k.client.Get(d, depOpts...); err != nil {
   350  			return err
   351  		} else if len(depList.Items) == 0 {
   352  			return runtime.ErrNotFound
   353  		}
   354  
   355  		// update the deployments which match the query
   356  		for _, dep := range depList.Items {
   357  			// the service wan't created by the k8s runtime
   358  			if dep.Metadata == nil || dep.Metadata.Annotations == nil {
   359  				continue
   360  			}
   361  
   362  			// update metadata
   363  			for k, v := range s.Metadata {
   364  				dep.Metadata.Annotations[k] = v
   365  			}
   366  
   367  			if rcn := getRuntimeClassName(k.options.Context); len(rcn) > 0 {
   368  				dep.Spec.Template.PodSpec.RuntimeClassName = rcn
   369  				logger.Infof("Setting runtime class name to %v", rcn)
   370  			}
   371  
   372  			// update build time annotation
   373  			dep.Spec.Template.Metadata.Annotations["updated"] = fmt.Sprintf("%d", time.Now().Unix())
   374  
   375  			// set num instances (there is currently no way to set to 0
   376  			if options.Instances > 0 {
   377  				dep.Spec.Replicas = int(options.Instances)
   378  			}
   379  
   380  			// update the deployment
   381  			res := &client.Resource{
   382  				Kind:  "deployment",
   383  				Name:  resourceName(s),
   384  				Value: &dep,
   385  			}
   386  			if err := k.client.Update(res, client.UpdateNamespace(options.Namespace)); err != nil {
   387  				if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
   388  					logger.Errorf("Runtime failed to update deployment: %v", err)
   389  				}
   390  				return err
   391  			}
   392  		}
   393  
   394  		return nil
   395  	default:
   396  		return runtime.ErrInvalidResource
   397  	}
   398  }
   399  
   400  // Delete removes a resource
   401  func (k *kubernetes) Delete(resource runtime.Resource, opts ...runtime.DeleteOption) error {
   402  	k.Lock()
   403  	defer k.Unlock()
   404  
   405  	options := runtime.DeleteOptions{
   406  		Namespace: client.DefaultNamespace,
   407  	}
   408  	for _, o := range opts {
   409  		o(&options)
   410  	}
   411  
   412  	// Handle the various different types of resources:
   413  	switch resource.Type() {
   414  	case runtime.TypeNamespace:
   415  		// Assert the resource back into a *runtime.Namespace
   416  		namespace, ok := resource.(*runtime.Namespace)
   417  		if !ok {
   418  			return runtime.ErrInvalidResource
   419  		}
   420  		return k.deleteNamespace(namespace)
   421  	case runtime.TypeNetworkPolicy:
   422  		// Assert the resource back into a *runtime.NetworkPolicy
   423  		networkPolicy, ok := resource.(*runtime.NetworkPolicy)
   424  		if !ok {
   425  			return runtime.ErrInvalidResource
   426  		}
   427  		return k.deleteNetworkPolicy(networkPolicy)
   428  	case runtime.TypeResourceQuota:
   429  		// Assert the resource back into a *runtime.ResourceQuota
   430  		resourceQuota, ok := resource.(*runtime.ResourceQuota)
   431  		if !ok {
   432  			return runtime.ErrInvalidResource
   433  		}
   434  		return k.deleteResourceQuota(resourceQuota)
   435  	case runtime.TypeService:
   436  
   437  		// Assert the resource back into a *runtime.Service
   438  		s, ok := resource.(*runtime.Service)
   439  		if !ok {
   440  			return runtime.ErrInvalidResource
   441  		}
   442  
   443  		// delete the deployment
   444  		dep := client.NewDeployment(s, &runtime.CreateOptions{
   445  			Type:      k.options.Type,
   446  			Namespace: options.Namespace,
   447  		})
   448  		if err := k.client.Delete(dep, client.DeleteNamespace(options.Namespace)); err != nil {
   449  			if err == api.ErrNotFound {
   450  				return runtime.ErrNotFound
   451  			}
   452  			if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
   453  				logger.Errorf("Runtime failed to delete deployment: %v", err)
   454  			}
   455  			return err
   456  		}
   457  
   458  		// delete the credentials
   459  		if err := k.deleteCredentials(s, &runtime.CreateOptions{Namespace: options.Namespace}); err != nil {
   460  			return err
   461  		}
   462  
   463  		// if there are more deployments for this service, then don't delete it
   464  		labels := map[string]string{}
   465  		if len(s.Name) > 0 {
   466  			labels["name"] = client.Format(s.Name)
   467  		}
   468  
   469  		// get the existing services. todo: refactor to just get the deployments
   470  		services, err := k.getServices(client.GetNamespace(options.Namespace), client.GetLabels(labels))
   471  		if err != nil || len(services) > 0 {
   472  			return err
   473  		}
   474  
   475  		// delete the service
   476  		srv := client.NewService(s, &runtime.CreateOptions{
   477  			Type:      k.options.Type,
   478  			Namespace: options.Namespace,
   479  		})
   480  		if err := k.client.Delete(srv, client.DeleteNamespace(options.Namespace)); err != nil {
   481  			if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
   482  				logger.Errorf("Runtime failed to delete service: %v", err)
   483  			}
   484  			return err
   485  		}
   486  
   487  		return nil
   488  	default:
   489  		return runtime.ErrInvalidResource
   490  	}
   491  }
   492  
   493  // Start starts the runtime
   494  func (k *kubernetes) Start() error {
   495  	return nil
   496  }
   497  
   498  // Stop shuts down the runtime
   499  func (k *kubernetes) Stop() error {
   500  	return nil
   501  }
   502  
   503  // String implements stringer interface
   504  func (k *kubernetes) String() string {
   505  	return "kubernetes"
   506  }
   507  
   508  // NewRuntime creates new kubernetes runtime
   509  func NewRuntime(opts ...runtime.Option) runtime.Runtime {
   510  	// get default options
   511  	options := runtime.Options{
   512  		// Create labels with type "micro": "service"
   513  		Type: "service",
   514  	}
   515  
   516  	// apply requested options
   517  	for _, o := range opts {
   518  		o(&options)
   519  	}
   520  
   521  	// kubernetes client
   522  	client := client.NewClusterClient()
   523  
   524  	return &kubernetes{
   525  		options: options,
   526  		client:  client,
   527  	}
   528  }