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 }