go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/invoke/options.go (about) 1 // Copyright 2019 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 invoke implements the process of invoking a 'luciexe' compatible 16 // subprocess, but without setting up any of the 'host' requirements (like 17 // a Logdog Butler or LUCI Auth). 18 // 19 // See go.chromium.org/luci/luciexe for details on the protocol. 20 package invoke 21 22 import ( 23 "context" 24 "io" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "strings" 29 30 "github.com/golang/protobuf/ptypes" 31 32 bbpb "go.chromium.org/luci/buildbucket/proto" 33 "go.chromium.org/luci/common/clock" 34 "go.chromium.org/luci/common/errors" 35 "go.chromium.org/luci/common/system/environ" 36 "go.chromium.org/luci/logdog/client/butlerlib/bootstrap" 37 "go.chromium.org/luci/logdog/client/butlerlib/streamclient" 38 "go.chromium.org/luci/logdog/common/types" 39 "go.chromium.org/luci/lucictx" 40 "go.chromium.org/luci/luciexe" 41 ) 42 43 // Options represents settings to use when Start'ing a luciexe. 44 // 45 // All values here have defaults, so Start can accept `nil`. 46 type Options struct { 47 // This should be in Build.Step.Name format, i.e. "parent|parent|leaf". 48 // 49 // Namespace is used as: 50 // * The Step name of the enclosing Build Step. 51 // * Generating the new LOGDOG_NAMESPACE in the environment for the 52 // subprocess. Non-StreamName characters are replaced with '_', 53 // and if the first character of a segment is not a character, "s_" is 54 // prpended. i.e. if the current LOGDOG_NAMESPACE is "u", and this 55 // namespace is "x|!!cool!!|z", then LOGDOG_NAMESPACE for the 56 // subprocess will be "u/x/s___cool__/z". 57 // * The LUCI Executable host process will assert that all logdog stream 58 // names mentioned in the subprocess's Build are relative to this. 59 // * The basis for the stdout/stderr Logdog streams for the subprocess. 60 // * The LUCI Executable host process will adjust the subprocess's step 61 // names to be relative to this. i.e. if the subprocess emits a step "a|b" 62 // and the Namespace is "x|y|z", then the step will show up as "x|y|z|a|b" 63 // on the overall Build. 64 // 65 // TODO(iannucci): Logdog stream names are restricted because they show up in 66 // URLs on the logdog service. If the logdog service instead uses URL encoding 67 // of the stream name, then StreamName could be expanded to allow all 68 // characters except for "/". At some later point we could potentially change 69 // the separator from "/" to "|" to match Buildbucket semantics. 70 // 71 // TODO(iannucci): Find a way to have logdog stream name namespacing be more 72 // transparent (i.e. without needing explicit cooperation from the logdog 73 // client library via LOGDOG_NAMESPACE envvar). 74 // 75 // Default: The Namespace is empty. This is ONLY useful when writing 76 // a transparent wrapper for a luciexe which doesn't, itself, implement the 77 // luciexe protocol. If Namespace is empty, then Subprocess.Step will be 78 // `nil`. 79 Namespace string 80 81 // The base dir where all sub-directories (i.e. workdir, tempdir, etc.) 82 // will be created under (i.e. the `cwd` for invoked luciexe will be 83 // `$BaseDir/w`). If specified, this must exist and be a directory. 84 // 85 // If empty, a random directory under os.TempDir will be used. 86 BaseDir string 87 88 // Absolute path to the cache base directory. This must exist and be 89 // a directory. 90 // 91 // Default: If LUCI_CONFIG['lucictx']['cache_dir'] is set, it will be passed 92 // through unchanged. Otherwise a new empty directory is allocated which will 93 // be destroyed on the subprocess's completion. 94 CacheDir string 95 96 // If set, the subprocess's final Build message will be collected and returned 97 // from Wait. 98 // 99 // If CollectOutput is specified, but CollectOutputPath is not, a temporary 100 // path will be seleceted and destroyed on the subprocess's completion. 101 CollectOutput bool 102 103 // If set, will be used as the path to output the subprocess's final Build 104 // state. Must end with one of {'.pb', '.json', '.textpb'}. This must be 105 // a path to a non-existent file (i.e. parent must be a directory). 106 // 107 // If CollectOutputPath is specified, but CollectOutput is not, the subprocess 108 // will be instructed to dump the result to this path, but we won't attempt to 109 // parse or validate it in any way, and Wait will return a nil `output`. 110 CollectOutputPath string 111 112 // A replacement environment for the Options. 113 // 114 // If this is not specified, the current process's environment is inherited. 115 // 116 // The following environment variables will be ignored from this `Env`: 117 // * LOGDOG_NAMESPACE 118 // * LUCI_CONTEXT 119 Env environ.Env 120 } 121 122 // launchOptions is a 'digested' form of Options, used for starting 123 // a subprocess. 124 type launchOptions struct { 125 // lctx is the exported LUCI_CONTEXT object must be Close()'d by the caller of 126 // Options.rationalize. It's already been set in `env`. 127 lctx lucictx.Exported 128 129 // step is the constructed Step object for the user of the invoke library. It 130 // may be nil if Namespace was omitted. 131 step *bbpb.Step 132 133 // workDir is the working directory for the invoked luciexe. 134 workDir string 135 136 // args are the CLI arguments to the luciexe. 137 args []string 138 139 // These are the open streams ready to attach to the subprocess. 140 stdout io.WriteCloser 141 stderr io.WriteCloser 142 143 // collectPath, if set, is the build file to read after the completion of the 144 // subprocess. 145 collectPath string 146 147 // env is an environment suitable to run the luciexe in. 148 env environ.Env 149 } 150 151 func (o *Options) prepNamespace(ctx context.Context, lo *launchOptions) error { 152 if o.Namespace == "" { 153 return nil 154 } 155 156 var bits []string 157 bits = append(bits, strings.Split(o.Namespace, "|")...) 158 relNS, _ := types.MakeStreamName("s_", bits...) 159 160 // The $LOGDOG_NAMESPACE envvar is currently cumulative, even though the 161 // Log.Url field is relative. 162 fullNS := string(relNS) 163 if curNS := lo.env.Get(luciexe.LogdogNamespaceEnv); curNS != "" { 164 fullNS = strings.Join([]string{curNS, fullNS}, "/") 165 } 166 lo.env.Set(luciexe.LogdogNamespaceEnv, fullNS) 167 168 startTime, err := ptypes.TimestampProto(clock.Now(ctx)) 169 if err != nil { 170 return errors.Annotate(err, "invalid StartTime").Err() 171 } 172 lo.step = &bbpb.Step{ 173 Name: o.Namespace, 174 StartTime: startTime, 175 Status: bbpb.Status_STARTED, 176 Logs: []*bbpb.Log{ 177 { 178 Name: "stdout", 179 Url: string(relNS.Concat("stdout")), 180 }, 181 { 182 Name: "stderr", 183 Url: string(relNS.Concat("stderr")), 184 }, 185 }, 186 MergeBuild: &bbpb.Step_MergeBuild{ 187 FromLogdogStream: string(relNS.Concat(luciexe.BuildProtoStreamSuffix)), 188 }, 189 } 190 return nil 191 } 192 193 func (o *Options) prepCacheDir(ctx context.Context, cdir string, lo *launchOptions) (newCtx context.Context, err error) { 194 newCtx = ctx // so we don't trip up our caller 195 newDir := o.CacheDir 196 197 if newDir == "" { 198 if curval := lucictx.GetLUCIExe(ctx); curval != nil && curval.CacheDir == "" { 199 err = errors.New( 200 `$LUCI_CONTEXT["luciexe"] is set, but "cache_dir" is empty`) 201 return 202 } 203 newDir = cdir 204 } else { 205 if newDir, err = filepath.Abs(newDir); err != nil { 206 return nil, errors.Annotate(err, "resolving CacheDir").Err() 207 } 208 if err = checkDirExists(newDir); err != nil { 209 return nil, errors.Annotate(err, "checking CacheDir").Err() 210 } 211 } 212 213 newCtx = lucictx.SetLUCIExe(ctx, &lucictx.LUCIExe{CacheDir: newDir}) 214 lo.lctx, err = lucictx.Export(newCtx) 215 lo.lctx.SetInEnviron(lo.env) 216 return 217 } 218 219 func (o *Options) prepCollection(outDir string, lo *launchOptions) error { 220 if !o.CollectOutput && o.CollectOutputPath == "" { 221 return nil 222 } 223 224 collect := o.CollectOutputPath 225 if collect == "" { 226 collect = filepath.Join(outDir, "out"+luciexe.BuildFileCodecBinary.FileExtension()) 227 } else { 228 if err := checkDirExists(filepath.Dir(collect)); err != nil { 229 return errors.Annotate(err, "checking CollectOutputPath's parent").Err() 230 } 231 232 if _, err := os.Stat(collect); !os.IsNotExist(err) { 233 return errors.Reason("CollectOutputPath points to an existing file: %q", collect).Err() 234 } 235 } 236 237 if _, err := luciexe.BuildFileCodecForPath(collect); err != nil { 238 return errors.Annotate(err, "CollectOutputPath").Err() 239 } 240 241 lo.args = []string{luciexe.OutputCLIArg, collect} 242 lo.collectPath = collect 243 return nil 244 } 245 246 type dirs struct { 247 // May or may not be used; for simplicity we always create them under dirs, 248 // but rationalize may override them if the user specified a specific location 249 // for them in Options. 250 cacheDir string 251 collectOutputDir string 252 253 // always used 254 tempDir string 255 workDir string 256 } 257 258 func (o *Options) mkdirs() (ret dirs, err error) { 259 base := o.BaseDir 260 if base == "" { 261 if base, err = ioutil.TempDir("", ""); err != nil { 262 return 263 } 264 } 265 if err = checkDirExists(base); err != nil { 266 return ret, errors.Annotate(err, "checking BaseDir").Err() 267 } 268 269 // maybeMkdir attempts to make the dir named `dirname` under `base`, 270 // annotating the error with `friendlyName` as long as `err` is nil. 271 // 272 // Updates `err` with the result of the mkdir call as a side effect. 273 maybeMkdir := func(out *string, dirname, friendlyName string) { 274 if err == nil { 275 *out = filepath.Join(base, dirname) 276 err = errors.Annotate(os.Mkdir(*out, 0777), "preparing %q", friendlyName).Err() 277 } 278 } 279 280 maybeMkdir(&ret.cacheDir, "c", "cache-dir") 281 maybeMkdir(&ret.collectOutputDir, "o", "output-dir") 282 maybeMkdir(&ret.tempDir, "t", "temp-dir") 283 maybeMkdir(&ret.workDir, "w", "work-dir") 284 285 return 286 } 287 288 func (lo *launchOptions) prepStdio(ctx context.Context) error { 289 // NOTE: bootstrapping doesn't do any 'write' actions to the logdog state and 290 // is a fancy way of reading a couple of envvars and building a struct (i.e. 291 // this is quite cheap). 292 bs, err := bootstrap.GetFromEnv(lo.env) // picks up Namespace 293 switch err { 294 case nil: 295 case bootstrap.ErrNotBootstrapped: 296 return errors.New("Logdog Butler environment required") 297 default: 298 return errors.Annotate(err, "bootstrapping logdog client").Err() 299 } 300 301 openStream := func(name string) (ret io.WriteCloser, err error) { 302 ret, err = bs.Client.NewStream( 303 ctx, types.StreamName(name), streamclient.ForProcess()) 304 err = errors.Annotate(err, "opening %q", name).Err() 305 return 306 } 307 308 if lo.stdout, err = openStream("stdout"); err != nil { 309 return err 310 } 311 if lo.stderr, err = openStream("stderr"); err != nil { 312 lo.stdout.Close() 313 return err 314 } 315 return nil 316 } 317 318 // rationalize converts from a set of requested Options to a usable 319 // launchOptions object. 320 func (o *Options) rationalize(ctx context.Context) (ret launchOptions, newCtx context.Context, err error) { 321 if o == nil { 322 o = &Options{} 323 } 324 if o.Env.Len() != 0 { 325 ret.env = o.Env.Clone() 326 } else { 327 ret.env = environ.System() 328 } 329 330 var d dirs 331 if d, err = o.mkdirs(); err != nil { 332 return 333 } 334 ret.workDir = d.workDir 335 for _, key := range luciexe.TempDirEnvVars { 336 ret.env.Set(key, d.tempDir) 337 } 338 339 if newCtx, err = o.prepCacheDir(ctx, d.cacheDir, &ret); err != nil { 340 err = errors.Annotate(err, "preparing cachedir").Err() 341 return 342 } 343 344 if err = o.prepNamespace(newCtx, &ret); err != nil { 345 err = errors.Annotate(err, "preparing namespace").Err() 346 return 347 } 348 349 if err = o.prepCollection(d.collectOutputDir, &ret); err != nil { 350 err = errors.Annotate(err, "preparing collection").Err() 351 return 352 } 353 354 if err = ret.prepStdio(newCtx); err != nil { 355 err = errors.Annotate(err, "preparing outputs").Err() 356 return 357 } 358 359 return 360 } 361 362 // checkDirExists returns error if the given path is not an 363 // existing directory. 364 func checkDirExists(path string) error { 365 fInfo, err := os.Stat(path) 366 if err != nil { 367 if os.IsNotExist(err) { 368 return errors.Reason("dir does not exist: %q", path).Err() 369 } 370 return errors.Annotate(err, "statting path: %q", path).Err() 371 } 372 if !fInfo.IsDir() { 373 return errors.Reason("path is not a directory: %q", path).Err() 374 } 375 return nil 376 }