go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tools/internal/apigen/service.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package apigen
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"net/url"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  
    27  	log "go.chromium.org/luci/common/logging"
    28  
    29  	"gopkg.in/yaml.v2"
    30  )
    31  
    32  // appYAML is a subset of the contents of an AppEngine application's "app.yaml"
    33  // descriptor needed by this service.
    34  type appYAML struct {
    35  	Runtime string `yaml:"runtime"`
    36  	VM      bool   `yaml:"vm"`
    37  }
    38  
    39  type service interface {
    40  	run(context.Context, serviceRunFunc) error
    41  }
    42  
    43  type serviceRunFunc func(c context.Context, u url.URL) error
    44  
    45  // loadService is a generic service loader routine. It attempts to:
    46  // 1) Identify the filesystem path of the service being described.
    47  // 2) Analyze its "app.yaml" to determine its runtime parameters.
    48  // 3) Construct and return a `service` instance for the result.
    49  //
    50  // "path" is decoded as:
    51  // - A discovery base URL
    52  // - A filesystem path, pointing to an "app.yaml" file.
    53  // - A Go package path containing an "app.yaml" file.
    54  func loadService(c context.Context, path string) (service, error) {
    55  	url, err := url.Parse(path)
    56  	if err == nil && url.Scheme != "" {
    57  		log.Fields{
    58  			"url": path,
    59  		}.Infof(c, "Identified path as service URL.")
    60  		return &remoteDiscoveryService{
    61  			url: *url,
    62  		}, nil
    63  	}
    64  	log.Fields{
    65  		log.ErrorKey: err,
    66  		"value":      path,
    67  	}.Debugf(c, "Path did not parse as URL. Trying local filesystem options.")
    68  
    69  	yamlPath := ""
    70  	st, err := os.Stat(path)
    71  	switch {
    72  	case os.IsNotExist(err):
    73  		log.Fields{
    74  			"path": path,
    75  		}.Debugf(c, "Path does not exist. Maybe it's a Go path?")
    76  
    77  		// Not a filesysem path. Perhaps it's a Go package on GOPATH?
    78  		pkgPath, err := getPackagePath(path)
    79  		if err != nil {
    80  			log.Fields{
    81  				"path": path,
    82  			}.Debugf(c, "Could not resolve package path.")
    83  			return nil, fmt.Errorf("could not resolve path [%s]", path)
    84  		}
    85  		path = pkgPath
    86  
    87  	case err != nil:
    88  		return nil, fmt.Errorf("failed to stat [%s]: %s", path, err)
    89  
    90  	case st.IsDir():
    91  		break
    92  
    93  	default:
    94  		// "path" is a path to a non-directory. Use its parent directory.
    95  		yamlPath, err = filepath.Abs(path)
    96  		if err != nil {
    97  			return nil, fmt.Errorf("could not get absolute path for YAML config [%s]: %s", path, err)
    98  		}
    99  		path = filepath.Dir(path)
   100  	}
   101  
   102  	// "path" is a directory. Does its `app.yaml` exist?
   103  	if yamlPath == "" {
   104  		yamlPath = filepath.Join(path, "app.yaml")
   105  	}
   106  
   107  	if _, err = os.Stat(yamlPath); err != nil {
   108  		return nil, fmt.Errorf("unable to stat YAML config at [%s]: %s", yamlPath, err)
   109  	}
   110  
   111  	configData, err := os.ReadFile(yamlPath)
   112  	if err != nil {
   113  		return nil, fmt.Errorf("failed to read YAML config at [%s]: %s", yamlPath, err)
   114  	}
   115  
   116  	config := appYAML{}
   117  	if err := yaml.Unmarshal(configData, &config); err != nil {
   118  		return nil, fmt.Errorf("failed to Unmarshal YAML config from [%s]: %s", yamlPath, err)
   119  	}
   120  
   121  	switch config.Runtime {
   122  	case "go":
   123  		if config.VM {
   124  			return &discoveryTranslateService{
   125  				dir: path,
   126  			}, nil
   127  		}
   128  		return &devAppserverService{
   129  			prerun: func(c context.Context) error {
   130  				return checkBuild(c, path)
   131  			},
   132  			args: []string{"goapp", "serve", yamlPath},
   133  		}, nil
   134  
   135  	case "python27":
   136  		return &devAppserverService{
   137  			args: []string{"dev_appserver.py", yamlPath},
   138  		}, nil
   139  
   140  	default:
   141  		return nil, fmt.Errorf("don't know how to load service runtime [%s]", config.Runtime)
   142  	}
   143  }
   144  
   145  type remoteDiscoveryService struct {
   146  	url url.URL
   147  }
   148  
   149  func (s *remoteDiscoveryService) run(c context.Context, f serviceRunFunc) error {
   150  	return f(c, s.url)
   151  }
   152  
   153  type devAppserverService struct {
   154  	prerun func(context.Context) error
   155  	args   []string
   156  }
   157  
   158  func (s *devAppserverService) run(c context.Context, f serviceRunFunc) error {
   159  	if s.prerun != nil {
   160  		if err := s.prerun(c); err != nil {
   161  			return err
   162  		}
   163  	}
   164  
   165  	log.Fields{
   166  		"args": s.args,
   167  	}.Infof(c, "Executing service.")
   168  
   169  	if len(s.args) == 0 {
   170  		return errors.New("no command configured")
   171  	}
   172  
   173  	// Execute `dev_appserver`.
   174  	cmd := &killableCommand{
   175  		Cmd: exec.Command(s.args[0], s.args[1:]...),
   176  	}
   177  	if err := cmd.Start(); err != nil {
   178  		return err
   179  	}
   180  	defer cmd.kill(c)
   181  
   182  	return f(c, url.URL{
   183  		Scheme: "http",
   184  		Host:   "localhost:8080",
   185  	})
   186  }
   187  
   188  // discoveryTranslateService is a service that loads a backend discovery
   189  // document, translates it to a frontend directory list, then hosts its own
   190  // frontend server to expose the translated data.
   191  type discoveryTranslateService struct {
   192  	dir string
   193  }
   194  
   195  func (s *discoveryTranslateService) run(c context.Context, f serviceRunFunc) error {
   196  	// Build the Go Managed VM service application.
   197  	p, err := filepath.Abs(s.dir)
   198  	if err != nil {
   199  		return fmt.Errorf("failed to get absolute path [%s]: %s", s.dir, err)
   200  	}
   201  
   202  	d, err := ioutil.TempDir(p, "apigen_service")
   203  	if err != nil {
   204  		return err
   205  	}
   206  	defer os.RemoveAll(d)
   207  
   208  	svcPath := filepath.Join(d, "service")
   209  	cmd := exec.Command("go", "build", "-o", svcPath, ".")
   210  	cmd.Dir = p
   211  	log.Fields{
   212  		"args": cmd.Args,
   213  		"wd":   cmd.Dir,
   214  	}.Debugf(c, "Executing `go build` command.")
   215  	if out, err := cmd.CombinedOutput(); err != nil {
   216  		log.Fields{
   217  			log.ErrorKey: err,
   218  			"dst":        svcPath,
   219  			"wd":         cmd.Dir,
   220  		}.Errorf(c, "Failed to build package:\n%s", string(out))
   221  		return fmt.Errorf("failed to build package: %s", err)
   222  	}
   223  
   224  	// Execute the service.
   225  	svc := &killableCommand{
   226  		Cmd: exec.Command(svcPath),
   227  	}
   228  	svc.Env = append(os.Environ(), "LUCI_GO_APPENGINE_APIGEN=1")
   229  	if err := svc.Start(); err != nil {
   230  		return err
   231  	}
   232  	defer svc.kill(c)
   233  
   234  	return f(c, url.URL{
   235  		Scheme: "http",
   236  		Host:   "localhost:8080",
   237  	})
   238  }
   239  
   240  func checkBuild(c context.Context, dir string) error {
   241  	d, err := ioutil.TempDir(dir, "apigen_service")
   242  	if err != nil {
   243  		return err
   244  	}
   245  	defer os.RemoveAll(d)
   246  
   247  	cmd := exec.Command("go", "build", "-o", filepath.Join(filepath.Base(d), "service"), ".")
   248  	cmd.Dir = dir
   249  	log.Fields{
   250  		"args": cmd.Args,
   251  		"wd":   cmd.Dir,
   252  	}.Debugf(c, "Executing `go build` command.")
   253  	if out, err := cmd.CombinedOutput(); err != nil {
   254  		log.Fields{
   255  			log.ErrorKey: err,
   256  			"wd":         cmd.Dir,
   257  		}.Errorf(c, "Failed to build package:\n%s", string(out))
   258  		return fmt.Errorf("failed to build package: %s", err)
   259  	}
   260  	return nil
   261  }