github.com/tickoalcantara12/micro/v3@v3.0.0-20221007104245-9d75b9bcbab9/service/runtime/local/local.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/local/local.go
    14  
    15  package local
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"io"
    21  	"log"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  	"sync"
    26  
    27  	"github.com/hpcloud/tail"
    28  	"github.com/tickoalcantara12/micro/v3/service/logger"
    29  	"github.com/tickoalcantara12/micro/v3/service/runtime"
    30  )
    31  
    32  // defaultNamespace to use if not provided as an option
    33  const defaultNamespace = "micro"
    34  
    35  var (
    36  	// The directory for logs to be output
    37  	LogDir = filepath.Join(os.TempDir(), "micro", "logs")
    38  	// The source directory where code lives
    39  	SourceDir = filepath.Join(os.TempDir(), "micro", "uploads")
    40  )
    41  
    42  type localRuntime struct {
    43  	sync.RWMutex
    44  	// options configure runtime
    45  	options runtime.Options
    46  	// used to start new services
    47  	start chan *service
    48  	// indicates if we're running
    49  	running bool
    50  	// namespaces stores services grouped by namespace, e.g. namespaces["foo"]["go.micro.auth:latest"]
    51  	// would return the latest version of go.micro.auth from the foo namespace
    52  	namespaces map[string]map[string]*service
    53  }
    54  
    55  // NewRuntime creates new local runtime and returns it
    56  func NewRuntime(opts ...runtime.Option) runtime.Runtime {
    57  	// get default options
    58  	options := runtime.Options{}
    59  
    60  	// apply requested options
    61  	for _, o := range opts {
    62  		o(&options)
    63  	}
    64  
    65  	// make the logs directory
    66  	os.MkdirAll(LogDir, 0755)
    67  	if logger.V(logger.DebugLevel, logger.DefaultLogger) {
    68  		logger.Debugf("Micro log directory: %v", LogDir)
    69  	}
    70  
    71  	return &localRuntime{
    72  		options:    options,
    73  		start:      make(chan *service, 128),
    74  		namespaces: make(map[string]map[string]*service),
    75  	}
    76  }
    77  
    78  // Init initializes runtime options
    79  func (r *localRuntime) Init(opts ...runtime.Option) error {
    80  	r.Lock()
    81  	defer r.Unlock()
    82  
    83  	for _, o := range opts {
    84  		o(&r.options)
    85  	}
    86  
    87  	return nil
    88  }
    89  
    90  func logFile(serviceName string) string {
    91  	// make the directory
    92  	name := strings.Replace(serviceName, "/", "-", -1)
    93  	return filepath.Join(LogDir, fmt.Sprintf("%v.log", name))
    94  }
    95  
    96  func serviceKey(s *runtime.Service) string {
    97  	return fmt.Sprintf("%v:%v", s.Name, s.Version)
    98  }
    99  
   100  // Create creates a new service which is then started by runtime
   101  func (r *localRuntime) Create(resource runtime.Resource, opts ...runtime.CreateOption) error {
   102  	var options runtime.CreateOptions
   103  	for _, o := range opts {
   104  		o(&options)
   105  	}
   106  
   107  	r.Lock()
   108  	defer r.Unlock()
   109  
   110  	// Handle the various different types of resources:
   111  	switch resource.Type() {
   112  	case runtime.TypeNamespace:
   113  		// noop (Namespace is not supported by local)
   114  		return nil
   115  	case runtime.TypeNetworkPolicy:
   116  		// noop (NetworkPolicy is not supported by local)
   117  		return nil
   118  	case runtime.TypeResourceQuota:
   119  		// noop (ResourceQuota is not supported by local)
   120  		return nil
   121  	case runtime.TypeService:
   122  
   123  		// Assert the resource back into a *runtime.Service
   124  		s, ok := resource.(*runtime.Service)
   125  		if !ok {
   126  			return runtime.ErrInvalidResource
   127  		}
   128  
   129  		if len(options.Namespace) == 0 {
   130  			options.Namespace = defaultNamespace
   131  		}
   132  		if len(options.Entrypoint) > 0 {
   133  			s.Source = filepath.Join(s.Source, options.Entrypoint)
   134  		}
   135  		if len(options.Command) == 0 {
   136  			options.Command = []string{"go"}
   137  
   138  			// not all source will have a vendor directory (e.g. source pulled from a git remote)
   139  			if _, err := os.Stat(filepath.Join(s.Source, "vendor")); err == nil {
   140  				options.Args = []string{"run", "-mod", "vendor", "."}
   141  			} else {
   142  				options.Args = []string{"run", "."}
   143  			}
   144  		}
   145  
   146  		// pass secrets as env vars
   147  		for key, value := range options.Secrets {
   148  			options.Env = append(options.Env, fmt.Sprintf("%v=%v", key, value))
   149  		}
   150  
   151  		if _, ok := r.namespaces[options.Namespace]; !ok {
   152  			r.namespaces[options.Namespace] = make(map[string]*service)
   153  		}
   154  		if _, ok := r.namespaces[options.Namespace][serviceKey(s)]; ok && !options.Force {
   155  			return runtime.ErrAlreadyExists
   156  		}
   157  
   158  		// create new service
   159  		service := newService(s, options)
   160  
   161  		f, err := os.OpenFile(logFile(service.Name), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
   162  		if err != nil {
   163  			log.Fatal(err)
   164  		}
   165  
   166  		if service.output != nil {
   167  			service.output = io.MultiWriter(service.output, f)
   168  		} else {
   169  			service.output = f
   170  		}
   171  		// start the service
   172  		if err := service.Start(); err != nil {
   173  			return err
   174  		}
   175  		// save service
   176  		r.namespaces[options.Namespace][serviceKey(s)] = service
   177  
   178  		return nil
   179  	default:
   180  		return runtime.ErrInvalidResource
   181  	}
   182  }
   183  
   184  // exists returns whether the given file or directory exists
   185  func exists(path string) (bool, error) {
   186  	_, err := os.Stat(path)
   187  	if err == nil {
   188  		return true, nil
   189  	}
   190  	if os.IsNotExist(err) {
   191  		return false, nil
   192  	}
   193  	return true, err
   194  }
   195  
   196  // @todo: Getting existing lines is not supported yet.
   197  // The reason for this is because it's hard to calculate line offset
   198  // as opposed to character offset.
   199  // This logger streams by default and only supports the `StreamCount` option.
   200  func (r *localRuntime) Logs(resource runtime.Resource, options ...runtime.LogsOption) (runtime.LogStream, error) {
   201  	lopts := runtime.LogsOptions{}
   202  	for _, o := range options {
   203  		o(&lopts)
   204  	}
   205  
   206  	// Handle the various different types of resources:
   207  	switch resource.Type() {
   208  	case runtime.TypeNamespace:
   209  		// noop (Namespace is not supported by local)
   210  		return nil, nil
   211  	case runtime.TypeNetworkPolicy:
   212  		// noop (NetworkPolicy is not supported by local)
   213  		return nil, nil
   214  	case runtime.TypeResourceQuota:
   215  		// noop (ResourceQuota is not supported by local)
   216  		return nil, nil
   217  	case runtime.TypeService:
   218  
   219  		// Assert the resource back into a *runtime.Service
   220  		s, ok := resource.(*runtime.Service)
   221  		if !ok {
   222  			return nil, runtime.ErrInvalidResource
   223  		}
   224  
   225  		ret := &logStream{
   226  			service: s.Name,
   227  			stream:  make(chan runtime.Log),
   228  			stop:    make(chan bool),
   229  		}
   230  
   231  		fpath := logFile(s.Name)
   232  		if ex, err := exists(fpath); err != nil {
   233  			return nil, err
   234  		} else if !ex {
   235  			return nil, fmt.Errorf("Logs not found for service %s", s.Name)
   236  		}
   237  
   238  		// have to check file size to avoid too big of a seek
   239  		fi, err := os.Stat(fpath)
   240  		if err != nil {
   241  			return nil, err
   242  		}
   243  		size := fi.Size()
   244  
   245  		whence := 2
   246  		// Multiply by length of an average line of log in bytes
   247  		offset := lopts.Count * 200
   248  
   249  		if offset > size {
   250  			offset = size
   251  		}
   252  		offset *= -1
   253  
   254  		t, err := tail.TailFile(fpath, tail.Config{Follow: lopts.Stream, Location: &tail.SeekInfo{
   255  			Whence: whence,
   256  			Offset: int64(offset),
   257  		}, Logger: tail.DiscardingLogger})
   258  		if err != nil {
   259  			return nil, err
   260  		}
   261  
   262  		ret.tail = t
   263  		go func() {
   264  			for {
   265  				select {
   266  				case line, ok := <-t.Lines:
   267  					if !ok {
   268  						ret.Stop()
   269  						return
   270  					}
   271  					ret.stream <- runtime.Log{Message: line.Text}
   272  				case <-ret.stop:
   273  					return
   274  				}
   275  			}
   276  
   277  		}()
   278  		return ret, nil
   279  	default:
   280  		return nil, runtime.ErrInvalidResource
   281  	}
   282  }
   283  
   284  type logStream struct {
   285  	tail    *tail.Tail
   286  	service string
   287  	stream  chan runtime.Log
   288  	sync.Mutex
   289  	stop chan bool
   290  	err  error
   291  }
   292  
   293  func (l *logStream) Chan() chan runtime.Log {
   294  	return l.stream
   295  }
   296  
   297  func (l *logStream) Error() error {
   298  	return l.err
   299  }
   300  
   301  func (l *logStream) Stop() error {
   302  	l.Lock()
   303  	defer l.Unlock()
   304  
   305  	select {
   306  	case <-l.stop:
   307  		return nil
   308  	default:
   309  		close(l.stop)
   310  		close(l.stream)
   311  		err := l.tail.Stop()
   312  		if err != nil {
   313  			logger.Errorf("Error stopping tail: %v", err)
   314  			return err
   315  		}
   316  	}
   317  	return nil
   318  }
   319  
   320  // Read returns all instances of requested service
   321  // If no service name is provided we return all the track services.
   322  func (r *localRuntime) Read(opts ...runtime.ReadOption) ([]*runtime.Service, error) {
   323  	r.Lock()
   324  	defer r.Unlock()
   325  
   326  	gopts := runtime.ReadOptions{}
   327  	for _, o := range opts {
   328  		o(&gopts)
   329  	}
   330  	if len(gopts.Namespace) == 0 {
   331  		gopts.Namespace = defaultNamespace
   332  	}
   333  
   334  	save := func(k, v string) bool {
   335  		if len(k) == 0 {
   336  			return true
   337  		}
   338  		return k == v
   339  	}
   340  
   341  	//nolint:prealloc
   342  	var services []*runtime.Service
   343  
   344  	if _, ok := r.namespaces[gopts.Namespace]; !ok {
   345  		return make([]*runtime.Service, 0), nil
   346  	}
   347  
   348  	for _, service := range r.namespaces[gopts.Namespace] {
   349  		if !save(gopts.Service, service.Name) {
   350  			continue
   351  		}
   352  		if !save(gopts.Version, service.Version) {
   353  			continue
   354  		}
   355  		// TODO deal with service type
   356  		// no version has sbeen requested, just append the service
   357  		services = append(services, service.Service)
   358  	}
   359  
   360  	return services, nil
   361  }
   362  
   363  // Update attempts to update the service
   364  func (r *localRuntime) Update(resource runtime.Resource, opts ...runtime.UpdateOption) error {
   365  	var options runtime.UpdateOptions
   366  	for _, o := range opts {
   367  		o(&options)
   368  	}
   369  
   370  	// Handle the various different types of resources:
   371  	switch resource.Type() {
   372  	case runtime.TypeNamespace:
   373  		// noop (Namespace is not supported by local)
   374  		return nil
   375  	case runtime.TypeNetworkPolicy:
   376  		// noop (NetworkPolicy is not supported by local)
   377  		return nil
   378  	case runtime.TypeResourceQuota:
   379  		// noop (ResourceQuota is not supported by local)
   380  		return nil
   381  	case runtime.TypeService:
   382  
   383  		// Assert the resource back into a *runtime.Service
   384  		s, ok := resource.(*runtime.Service)
   385  		if !ok {
   386  			return runtime.ErrInvalidResource
   387  		}
   388  
   389  		if len(options.Entrypoint) > 0 {
   390  			s.Source = filepath.Join(s.Source, options.Entrypoint)
   391  		}
   392  
   393  		if len(options.Namespace) == 0 {
   394  			options.Namespace = defaultNamespace
   395  		}
   396  
   397  		r.Lock()
   398  		srvs, ok := r.namespaces[options.Namespace]
   399  		r.Unlock()
   400  		if !ok {
   401  			return errors.New("Service not found")
   402  		}
   403  
   404  		r.Lock()
   405  		service, ok := srvs[serviceKey(s)]
   406  		r.Unlock()
   407  		if !ok {
   408  			return errors.New("Service not found")
   409  		}
   410  
   411  		if err := service.Stop(); err != nil && err.Error() != "no such process" {
   412  			logger.Errorf("Error stopping service %s: %s", service.Name, err)
   413  			return err
   414  		}
   415  
   416  		// update the source to the new location and restart the service
   417  		service.Source = s.Source
   418  		service.Exec.Dir = s.Source
   419  		return service.Start()
   420  
   421  	default:
   422  		return runtime.ErrInvalidResource
   423  	}
   424  }
   425  
   426  // Delete removes the service from the runtime and stops it
   427  func (r *localRuntime) Delete(resource runtime.Resource, opts ...runtime.DeleteOption) error {
   428  
   429  	// Handle the various different types of resources:
   430  	switch resource.Type() {
   431  	case runtime.TypeNamespace:
   432  		// noop (Namespace is not supported by local)
   433  		return nil
   434  	case runtime.TypeNetworkPolicy:
   435  		// noop (NetworkPolicy is not supported by local)
   436  		return nil
   437  	case runtime.TypeResourceQuota:
   438  		// noop (ResourceQuota is not supported by local)
   439  		return nil
   440  	case runtime.TypeService:
   441  
   442  		// Assert the resource back into a *runtime.Service
   443  		s, ok := resource.(*runtime.Service)
   444  		if !ok {
   445  			return runtime.ErrInvalidResource
   446  		}
   447  
   448  		r.Lock()
   449  		defer r.Unlock()
   450  
   451  		var options runtime.DeleteOptions
   452  		for _, o := range opts {
   453  			o(&options)
   454  		}
   455  		if len(options.Namespace) == 0 {
   456  			options.Namespace = defaultNamespace
   457  		}
   458  
   459  		srvs, ok := r.namespaces[options.Namespace]
   460  		if !ok {
   461  			return nil
   462  		}
   463  
   464  		if logger.V(logger.DebugLevel, logger.DefaultLogger) {
   465  			logger.Debugf("Runtime deleting service %s", s.Name)
   466  		}
   467  
   468  		service, ok := srvs[serviceKey(s)]
   469  		if !ok {
   470  			return nil
   471  		}
   472  
   473  		// check if running
   474  		if !service.Running() {
   475  			delete(srvs, service.key())
   476  			r.namespaces[options.Namespace] = srvs
   477  			return nil
   478  		}
   479  		// otherwise stop it
   480  		if err := service.Stop(); err != nil {
   481  			return err
   482  		}
   483  		// delete it
   484  		delete(srvs, service.key())
   485  		r.namespaces[options.Namespace] = srvs
   486  		return nil
   487  	default:
   488  		return runtime.ErrInvalidResource
   489  	}
   490  }
   491  
   492  // Start starts the runtime
   493  func (r *localRuntime) Start() error {
   494  	r.Lock()
   495  	defer r.Unlock()
   496  
   497  	// already running
   498  	if r.running {
   499  		return nil
   500  	}
   501  
   502  	// set running
   503  	r.running = true
   504  	return nil
   505  }
   506  
   507  // Stop stops the runtime
   508  func (r *localRuntime) Stop() error {
   509  	r.Lock()
   510  	defer r.Unlock()
   511  
   512  	if !r.running {
   513  		return nil
   514  	}
   515  
   516  	// set not running
   517  	r.running = false
   518  
   519  	// stop all the services
   520  	for _, services := range r.namespaces {
   521  		for _, service := range services {
   522  			if logger.V(logger.DebugLevel, logger.DefaultLogger) {
   523  				logger.Debugf("Runtime stopping %s", service.Name)
   524  			}
   525  			// stop the service
   526  			service.Stop()
   527  			// wait for exit
   528  			service.Wait()
   529  		}
   530  	}
   531  
   532  	return nil
   533  }
   534  
   535  // String implements stringer interface
   536  func (r *localRuntime) String() string {
   537  	return "local"
   538  }
   539  
   540  // Entrypoint determines the entrypoint for the service, since main.go doesn't always exist at
   541  // the top level. Entrypoint will firstly look for a directory containing a .mu file (e.g. users.mu),
   542  // if this isn't present it'll look for main.go in the top level of the directory and then in the
   543  // cmd package (idiomatic service structure).
   544  func Entrypoint(dir string) (string, error) {
   545  	// entrypoints is a slice of all .mu files in the directory
   546  	var entrypoints []string
   547  	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
   548  		if err != nil {
   549  			return err
   550  		}
   551  
   552  		// get the relative path to the directory
   553  		rel, err := filepath.Rel(dir, path)
   554  		if err != nil {
   555  			return err
   556  		}
   557  
   558  		// check for the file extension
   559  		if strings.HasSuffix(rel, ".mu") {
   560  			entrypoints = append(entrypoints, rel)
   561  		}
   562  
   563  		return nil
   564  	})
   565  	if len(entrypoints) == 1 {
   566  		return entrypoints[0], nil
   567  	} else if len(entrypoints) > 1 {
   568  		return "", errors.New("More than one .mu file found")
   569  	}
   570  
   571  	// mainEntrypoints is a slice of all main.go files in a directory which are either at the top level
   572  	// or in the cmd folder.
   573  	var mainEntrypoints []string
   574  	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
   575  		if err != nil {
   576  			return err
   577  		}
   578  
   579  		// get the relative path to the directory
   580  		rel, err := filepath.Rel(dir, path)
   581  		if err != nil {
   582  			return err
   583  		}
   584  
   585  		// only look for files in the top level or the cmd folder
   586  		if dir := filepath.Dir(rel); !filepath.HasPrefix(dir, "cmd") && dir != "." {
   587  			return nil
   588  		}
   589  
   590  		// only look for main.go files
   591  		if filepath.Base(rel) == "main.go" {
   592  			mainEntrypoints = append(mainEntrypoints, rel)
   593  		}
   594  
   595  		return nil
   596  	})
   597  
   598  	// only one main.go was found, use this as the fallback
   599  	if len(mainEntrypoints) == 1 {
   600  		return mainEntrypoints[0], nil
   601  	}
   602  
   603  	return "", errors.New("No entrypoint found. Add a .mu file to the directory you want to run")
   604  }