├── .gitignore ├── src ├── Dockerfile ├── mho.sjs ├── docker.sjs └── commandline-parsing.sjs └── mho /.gitignore: -------------------------------------------------------------------------------- 1 | *~ -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM onilabs/conductance:latest 2 | MAINTAINER alex@onilabs.com 3 | 4 | # install mho 5 | RUN mkdir -p /usr/src/mho 6 | WORKDIR /usr/src/mho 7 | COPY . /usr/src/mho 8 | 9 | ENTRYPOINT [ "/usr/src/mho/mho.sjs" ] -------------------------------------------------------------------------------- /mho: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MHO_VERSION=latest 4 | 5 | # XXX check that we docker installed with api version >=1.25 6 | 7 | MHO_OPTS=~/.mho-local; [ -f $MHO_OPTS ] && . $MHO_OPTS 8 | 9 | if [ -t 0 ]; then 10 | MHO_TTY_OPTS='-t' 11 | fi 12 | 13 | docker run --rm -i $MHO_TTY_OPTS -v /var/run/docker.sock:/var/run/docker.sock -e "HOST_UID=$(id -u)" -e "HOST_GID=$(id -g)" -e "HOST_WD=$(pwd)" $MHO_DOCKER_OPTS onilabs/mho:$MHO_VERSION "$@" 14 | -------------------------------------------------------------------------------- /src/mho.sjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env conductance 2 | 3 | @ = require([ 4 | 'mho:std', 5 | {id:'./commandline-parsing', name:'commandline'}, 6 | {id:'mho:services', name:'services'}, 7 | {id:'./docker', name:'docker'} 8 | ]); 9 | 10 | //---------------------------------------------------------------------- 11 | // check that we've got the environment vars we're expecting and store on 'ENV': 12 | 13 | var ENV = ['HOST_WD', 'HOST_GID', 'HOST_UID'] .. 14 | @transform(function(v) { 15 | var val; 16 | if ((val = process.env[v]) === undefined) throw new Error("Missing environment variable #{v}"); 17 | return [v, val]; 18 | }) .. 19 | @pairsToObject; 20 | 21 | //---------------------------------------------------------------------- 22 | // dispatch 23 | 24 | var ARGV = @argv(); 25 | 26 | var COMMAND_SYNTAX = { 27 | name: 'mho', 28 | opts: { 29 | '-q': { help_txt: "Don't print superfluous information to stdout." } 30 | }, 31 | commands: { 32 | 'conductance': { 33 | opts: { 34 | '-v': { 35 | help_txt: 'version of conductance to run (default: latest)', 36 | arg: { name: 'VERSION', default: 'latest' } 37 | }, 38 | '-p': { 39 | help_txt: 'container ports to open on host (comma-separated list)', 40 | arg: { name: 'PORTS', default: [], parse: x -> x.split(',') } 41 | }, 42 | }, 43 | commands: { 44 | 'run': { 45 | summary_txt: 'Execute conductance in a sandboxed container.', 46 | help_txt: 'Executes conductance in a sandboxed container. Run without arguments for help. Note that only the current working directory (and sub-directories) will be available to the container (mapped under /usr/src/app).', 47 | arg_txt: '[ARGS...]', 48 | exec: conductance_run 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | 56 | var [exec, pars] = @commandline.dispatch(COMMAND_SYNTAX, ARGV); 57 | //console.log(exec); 58 | //console.log(pars ..@inspect(5, 5)); 59 | 60 | if (!pars[0].opts['-q']) 61 | console.log("mho - the stratified javascript ide"); 62 | 63 | exec(pars); 64 | process.exit(0); 65 | 66 | //---------------------------------------------------------------------- 67 | // commands 68 | 69 | function conductance_run([{/*mho*/}, 70 | {/*conductance*/ opts: {'-v':conductance_version, 71 | '-p':ports}}, 72 | {/*run*/ args:run_args} 73 | ]) { 74 | 75 | @services.initGlobalRegistry(@services.ServicesRegistry({ 76 | docker : @services.builtinServices.docker .. @merge({provisioning_data:{}}) 77 | })); 78 | 79 | @services.withService('docker') { 80 | |docker| 81 | 82 | docker .. @docker.runSubContainer({ 83 | Image: "onilabs/conductance:#{conductance_version}", 84 | args: run_args, 85 | ports: ports, 86 | volume_binds: ["#{ENV.HOST_WD}:/usr/src/app"] 87 | }); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/docker.sjs: -------------------------------------------------------------------------------- 1 | @ = require([ 2 | 'mho:std', 3 | {id:'mho:services/docker/REST/v1-25', name:'dockerREST'}, 4 | {id:'mho:services/docker/highlevel', name:'docker_highlevel'} 5 | ]); 6 | 7 | function exitWithError(txt) { 8 | process.stdout.write('\n'+txt+'\nExiting.\n'); 9 | process.exit(1); 10 | } 11 | 12 | exports.runSubContainer = function(docker, settings) { 13 | 14 | settings = { 15 | Image: undefined, 16 | ports: [], 17 | args: [], 18 | allow_failure: false, 19 | volume_binds: [] 20 | } .. @override(settings); 21 | 22 | var tty_mode = process.stdin.isTTY; 23 | 24 | var PortBindings = settings.ports .. 25 | @transform(p -> [String(p), [{HostPort:String(p)}]]) .. 26 | @pairsToObject; 27 | var ExposedPorts = settings.ports .. 28 | @transform(p -> [String(p), {}]) .. 29 | @pairsToObject; 30 | 31 | 32 | // create container: 33 | var {Id: container_id} = docker .. @dockerREST.containerCreate({ 34 | body: { 35 | Image: settings.Image, 36 | OpenStdin: true, 37 | Tty: tty_mode, 38 | WorkingDir: '/usr/src/app', 39 | HostConfig: { 40 | AutoRemove: true, 41 | NetworkMode: 'bridge', 42 | Binds: settings.volume_binds, 43 | PortBindings: PortBindings, 44 | }, 45 | ExposedPorts: ExposedPorts, 46 | Cmd: settings.args 47 | } 48 | }); 49 | 50 | waitfor { 51 | docker .. @dockerREST.containerAttach({ 52 | id: container_id, 53 | stream: true, 54 | stderr: true, 55 | stdout: true, 56 | stdin: true 57 | }) { 58 | |incoming| 59 | waitfor { 60 | if (tty_mode) { 61 | incoming.socket .. @stream.contents() .. @each { 62 | |x| 63 | process.stdout.write(x); 64 | } 65 | } 66 | else { 67 | // non-tty; streams are multiplexed: 68 | incoming.socket .. @docker_highlevel.parseMultiplexedStdOutErr .. @each { 69 | |{content}| 70 | process.stdout.write(content); 71 | } 72 | } 73 | break; 74 | } 75 | and { 76 | try { 77 | if (tty_mode) 78 | process.stdin.setRawMode(true); 79 | @stream.pump(process.stdin, incoming.socket); 80 | } 81 | finally { 82 | if (tty_mode) 83 | process.stdin.setRawMode(false); 84 | } 85 | } 86 | } /* containerAttach */ 87 | } 88 | and { 89 | docker .. @dockerREST.containerStart({id:container_id}); 90 | // XXX what happens if the container exits and is auto-removed before we call wait? 91 | var exitStatus = (docker .. @dockerREST.containerWait({ 92 | id: container_id})); 93 | if (exitStatus.StatusCode !== 0) { 94 | if (!settings.allow_failure) 95 | exitWithError("Container #{container_id} exited with non-zero exit status."); 96 | return false; 97 | } 98 | } 99 | catch(e) { 100 | try { 101 | docker .. @dockerREST.containerStop({id:container_id}); 102 | } 103 | catch(f) { /* ignore */ } 104 | throw e; 105 | } 106 | return true; 107 | 108 | }; 109 | 110 | -------------------------------------------------------------------------------- /src/commandline-parsing.sjs: -------------------------------------------------------------------------------- 1 | @ = require([ 2 | 'mho:std' 3 | ]); 4 | 5 | function peelOpts(argv, opts, ignoreErrors) { 6 | var rv = {}; 7 | if (opts) { 8 | while (argv.length) { 9 | var argname = argv[0]; 10 | var opt = opts[argname]; 11 | if (!opt) break; 12 | argv.shift(); 13 | var val; 14 | if (opt.arg) { 15 | if (!argv.length) { 16 | if (ignoreErrors) return rv; 17 | console.log("Missing argument for option #{argname}."); 18 | process.exit(1); 19 | } 20 | val = argv.shift(); 21 | if (opt.arg.parse) 22 | val = opt.arg.parse(val); 23 | } 24 | else 25 | val = true; 26 | rv[argname] = val; 27 | } 28 | // fill in defaults: 29 | opts .. @propertyPairs .. @each { 30 | |[propname,descriptor]| 31 | if (descriptor.arg && 32 | descriptor.arg['default'] !== undefined && 33 | rv[propname] === undefined 34 | ) 35 | rv[propname] = descriptor.arg['default']; 36 | } 37 | } 38 | return rv; 39 | } 40 | 41 | function formatHelp(syntax, command_path, argv) { 42 | var rv = []; 43 | // locate syntax @ command given by argv: 44 | while (argv.length) { 45 | peelOpts(argv, syntax.opts, true); // ignore opts 46 | if (!syntax.commands || !syntax.commands[argv[0]]) 47 | break; 48 | command_path.push(argv[0]); 49 | syntax = syntax.commands[argv.shift()]; 50 | } 51 | 52 | if (argv.length) 53 | rv.push("Unknown command '#{argv[0]}'\n"); 54 | 55 | var command_args = []; 56 | var opts_help = []; 57 | if (syntax.opts) { 58 | @propertyPairs(syntax.opts) .. @each { 59 | |[name,descr]| 60 | command_args.push("[#{name}#{descr.arg?" #{descr.arg.name||'VAL'}":''}]"); 61 | if (descr.help_txt) { 62 | opts_help.push(" #{name}#{descr.arg?" #{descr.arg.name||'VAL'}":''} : #{descr.help_txt}"); 63 | } 64 | } 65 | } 66 | 67 | if (syntax.arg_txt) 68 | command_args.push(syntax.arg_txt); 69 | else if (syntax.commands) 70 | command_args.push('COMMAND'); 71 | 72 | rv.push("Usage: #{command_path.join(' ')} #{command_args.join(' ')}"); 73 | 74 | if (syntax.help_txt) 75 | rv = rv.concat('', syntax.help_txt); 76 | 77 | if (opts_help.length) 78 | rv = rv.concat('', 'Options:', opts_help); 79 | 80 | var cmd_help = []; 81 | if (syntax.commands) { 82 | @propertyPairs(syntax.commands) .. @each { 83 | |[name,descr]| 84 | cmd_help.push(" #{name}#{descr.summary_txt? ': '+descr.summary_txt : ''}"); 85 | } 86 | } 87 | 88 | if (cmd_help.length) { 89 | var help_cmd = [command_path[0], 'help'].concat(command_path.slice(1), 'COMMAND').join(' '); 90 | rv = rv.concat('', 'Commands:', cmd_help, '', "Run '#{help_cmd}' for help on individual commands."); 91 | } 92 | rv.push("Run '#{command_path[0]} help' for full commandline help."); 93 | return rv.join('\n'); 94 | } 95 | 96 | function dispatch(syntax, argv, command_path) { 97 | if (!command_path) command_path = [syntax.name || "#{process.argv[0]} #{process.argv[1]}"]; 98 | var exec; 99 | var pars = {}; 100 | pars.opts = peelOpts(argv, syntax.opts); 101 | if (argv.length && syntax.commands && syntax.commands[argv[0]]) { 102 | var command_name = argv.shift(); 103 | [exec, subpars] = dispatch(syntax.commands[command_name], argv, command_path.concat(command_name)); 104 | subpars[0].command = command_name; 105 | return [exec, [pars].concat(subpars)]; 106 | } 107 | else if (argv.length && command_path.length === 1 && argv[0] === 'help') { 108 | argv.shift(); 109 | console.log(formatHelp(syntax, command_path, argv)); 110 | process.exit(0); 111 | } 112 | else { 113 | if (!syntax.exec) { 114 | // parse error 115 | console.log(formatHelp(syntax, command_path, argv)); 116 | process.exit(1); 117 | } 118 | pars.args = argv; 119 | return [syntax.exec, [pars]]; 120 | } 121 | } 122 | exports.dispatch = dispatch; 123 | 124 | 125 | 126 | --------------------------------------------------------------------------------