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 }