/* Spits out a chunk of json on stdout that can be used as a `Input` specification in a `Formula`. */ func Scan(outputSpec def.Output) def.Output { // TODO validate MountPath exists, give nice errors // todo: create validity checking api for URIs, check them all before launching anything warehouses := make([]integrity.SiloURI, len(outputSpec.Warehouses)) for i, wh := range outputSpec.Warehouses { warehouses[i] = integrity.SiloURI(wh) } // So, this CLI command is *not* in its rights to change the subject area, // so take that as a pretty strong hint that filters are going to have to pass down *into* transmats. commitID := util.DefaultTransmat().Scan( // All of this stuff that's type-coercing? // Yeah these are hints that this stuff should be facing data validation. integrity.TransmatKind(outputSpec.Type), outputSpec.MountPath, warehouses, ) return def.Output{ Type: outputSpec.Type, Warehouses: outputSpec.Warehouses, Hash: string(commitID), } }
/* Spits out a chunk of json on stdout that can be used as a `Input` specification in a `Formula`. */ func Scan(outputSpec def.Output) def.Output { // TODO validate Location exists, give nice errors siloURIs := []integrity.SiloURI{ integrity.SiloURI(outputSpec.URI), } if outputSpec.URI == "" { // ugly. figure out how we want the user-facing API to see multiple silo URIs. siloURIs = nil } // So, this CLI command is *not* in its rights to change the subject area, // so take that as a pretty strong hint that filters are going to have to pass down *into* transmats. commitID := util.DefaultTransmat().Scan( // All of this stuff that's type-coercing? // Yeah these are hints that this stuff should be facing data validation. integrity.TransmatKind(outputSpec.Type), outputSpec.Location, siloURIs, ) return def.Output{ Type: outputSpec.Type, URI: outputSpec.URI, Hash: string(commitID), } }
// Execute a formula in a specified directory. MAY PANIC. func (e *Executor) Execute(f def.Formula, j def.Job, d string, result *def.JobResult, outS, errS io.WriteCloser, journal log15.Logger) { // Dedicated rootfs folder to distinguish container from nsinit noise rootfs := filepath.Join(d, "rootfs") // nsinit wants to have a logfile logFile := filepath.Join(d, "nsinit-debug.log") // Prep command args := []string{} // Global options: // --root will place the 'nsinit' folder (holding a state.json file) in d // --log-file does much the same with a log file (unsure if care?) // --debug allegedly enables debug output in the log file args = append(args, "--root", d, "--log-file", logFile, "--debug") // Subcommand, and tell nsinit to not desire a JSON file (instead just use many flergs) args = append(args, "exec", "--create") // Use the host's networking (no bridge, no namespaces, etc) args = append(args, "--net=host") // Where our system image exists args = append(args, "--rootfs", rootfs) // Set cwd args = append(args, "--cwd", f.Accents.Cwd) // Add all desired environment variables for k, v := range f.Accents.Env { args = append(args, "--env", k+"="+v) } // Unroll command args args = append(args, f.Accents.Entrypoint...) // Prepare command to exec cmd := exec.Command("nsinit", args...) cmd.Stdin = nil cmd.Stdout = outS cmd.Stderr = errS // Prepare filesystem transmat := util.DefaultTransmat() assembly := util.ProvisionInputs( transmat, util.BestAssembler(), f.Inputs, rootfs, journal, ) defer assembly.Teardown() // What ever happens: Disassemble filesystem util.ProvisionOutputs(f.Outputs, rootfs, journal) // launch execution. // transform gosh's typed errors to repeatr's hierarchical errors. // this is... not untroubled code: since we're invoking a helper that's then // proxying the exec even further, most errors are fatal (the mapping here is // very different than in e.g. chroot executor, and provides much less meaning). var proc gosh.Proc try.Do(func() { proc = gosh.ExecProcCmd(cmd) }).CatchAll(func(err error) { switch err.(type) { case gosh.NoSuchCommandError: panic(executor.ConfigError.New("nsinit binary is missing")) case gosh.NoArgumentsError: panic(executor.UnknownError.Wrap(err)) case gosh.NoSuchCwdError: panic(executor.UnknownError.Wrap(err)) case gosh.ProcMonitorError: panic(executor.TaskExecError.Wrap(err)) default: panic(executor.UnknownError.Wrap(err)) } }).Done() // Wait for the job to complete // REVIEW: consider exposing `gosh.Proc`'s interface as part of repeatr's job tracking api? result.ExitCode = proc.GetExitCode() // Horrifyingly ambiguous attempts to detect failure modes from inside nsinit. // This can only be made correct by pushing patches into nsinit to use another channel for control data reporting that is completely separated from user data flows. // (Or, arguably, putting another layer of control processes as the first parent inside nsinit, but that's ducktape within a ducktape mesh; let's not.) // Certain program outputs may be incorrectly attributed as launch failure, though this should be... "unlikely". // Also note that if we ever switch to non-blocking execution, this will become even more of a mess: we won't be able to tell if exec failed, esp. in the case of e.g. a long running process with no output, and so we won't know when it's safe to return. // TODO handle the following leading strings: // - "exec: \"%s\": executable file not found in $PATH\n" // - "no such file or directory\n" // this will probably require rejiggering a whole bunch of stuff so that the streamer is reachable down here. // Save outputs result.Outputs = util.PreserveOutputs(transmat, f.Outputs, rootfs, journal) }
// Execute a formula in a specified directory. MAY PANIC. func (e *Executor) Execute(f def.Formula, j def.Job, d string, result *def.JobResult, stdin io.Reader, outS, errS io.WriteCloser, journal log15.Logger) { // Prepare filesystem rootfs := filepath.Join(d, "rootfs") transmat := util.DefaultTransmat() assembly := util.ProvisionInputs( transmat, util.BestAssembler(), f.Inputs, rootfs, journal, ) defer assembly.Teardown() // What ever happens: Disassemble filesystem util.ProvisionOutputs(f.Outputs, rootfs, journal) // chroot's are pretty easy. cmdName := f.Accents.Entrypoint[0] cmd := exec.Command(cmdName, f.Accents.Entrypoint[1:]...) cmd.SysProcAttr = &syscall.SysProcAttr{ Chroot: rootfs, Pdeathsig: syscall.SIGKILL, } // except handling cwd is a little odd. // see comments in gosh tests with chroot for information about the odd behavior we're hacking around here; // we're comfortable making this special check here, but not upstreaming it to gosh, because in our context we "know" we're not racing anyone. if externalCwdStat, err := os.Stat(filepath.Join(rootfs, f.Accents.Cwd)); err != nil { panic(executor.TaskExecError.New("cannot set cwd to %q: %s", f.Accents.Cwd, err.(*os.PathError).Err)) } else if !externalCwdStat.IsDir() { panic(executor.TaskExecError.New("cannot set cwd to %q: not a dir", f.Accents.Cwd)) } cmd.Dir = f.Accents.Cwd // set env. // initialization already required by earlier 'validate' calls. cmd.Env = envToSlice(f.Accents.Env) cmd.Stdin = stdin cmd.Stdout = outS cmd.Stderr = errS // launch execution. // transform gosh's typed errors to repeatr's hierarchical errors. var proc gosh.Proc try.Do(func() { proc = gosh.ExecProcCmd(cmd) }).CatchAll(func(err error) { switch err.(type) { case gosh.NoSuchCommandError: panic(executor.NoSuchCommandError.Wrap(err)) case gosh.NoArgumentsError: panic(executor.NoSuchCommandError.Wrap(err)) case gosh.NoSuchCwdError: // included for clarity and completeness, but we'll never actually see this; see comments in gosh about the interaction of chroot and cwd error handling. panic(executor.TaskExecError.Wrap(err)) case gosh.ProcMonitorError: panic(executor.TaskExecError.Wrap(err)) default: panic(executor.UnknownError.Wrap(err)) } }).Done() // Wait for the job to complete // REVIEW: consider exposing `gosh.Proc`'s interface as part of repeatr's job tracking api? result.ExitCode = proc.GetExitCode() // Save outputs result.Outputs = util.PreserveOutputs(transmat, f.Outputs, rootfs, journal) }