I'm nothing if not persistent! After feedback from my second try at proposing a way of doing defaults and aliases, I've got a third draft.
Feedback please! # Defaults and Shorthands in the Aurora Client ## Motivation Most of the time, users are doing the same thing, over and over again. They're working mainly on one particular service, in one particular workspace. But they need to repeat the same parameters, over and over again, and they need to remember what those parameters are, what order they occur in, what format they use, and other details that can be difficult to remember. In order to avoid these problems, users set up custom scripts that make it easy for them to run commands like "`myservice start`", instead of "`aurora job create west/markcc/prod/myserver src/main/aurora/myservice/myservice.aurora`" Scripts aren't necessarily a bad thing. We've designed the aurora client to be script friendly: every command has at least an option supporting easy-to-parse output. But scripts also often cover for problems that really shouldn't exist. Scripts get written for a variety of reasons. Among the many reasons that people implement scripts, there are scripts that add custom functionality for a specific group of users; scripts that can cover up for inadequate or missing core functionality; and there are scripts that cover up for poor user interfaces. In aurora, we've worked hard to eliminate cases where the core functionality of the command-line client is inadequate. But our user interface still needs a lot of work. In particular, commands in Aurora often require very long parameters, which users have a hard time remembering. Look at the example above - it's a pretty typical case. The user wants to create a job. They're going to be creating the same job, over and over again. But to run it, they need to type out 68 characters for the two parameters! This is a prime example of the kind of case where users will write scripts, not to provide new/special functionality, nor to cover for inadequate functionality, but just because it's so painful to remember and correctly type out an overly long string of parameters. To reduce this, we'd like to support a way for users to set up a configuration file that defines defaults and shorthands for their everyday work. With shorthands, a user that only works with a single service could say "aurora job create", instead of needing to spell out the full jobkey and configuration file location; a user working with multiple datacenters could say "`aurora job create @east`" or "`aurora job create @west`" to select the correct jobkey. ## Proposal: Aurora Init Files To allow users to customize shorthands, we'll provide a builtin capability to allow users to provide a configuration file, from which their customizations will be loaded. Many applications use a simple pattern to solve similar problems. Vagrant uses a file named "Vagrantfile"; when you run vagrant, if you don't specifically tell the tool where to find a configuration, it looks in the current directory or one of its parents to find a file named "Vagrantfile". We'd like to follow a similar pattern, and create an "AuroraInit" file. The aurora init file is found by searching the following locations, in order: * the contents of the "--init-file" parameter. * if the "--init-file" parameter is unspecified, then look in the current directory for a file named "AuroraInit". * if no "AuroraInit" file exists in the current directory, then look in the users home directory for an init file named ".aurora/init". > **Sidebar: Why fallback to the users home directory?** > > Codebases are often shared between multiple projects, each of which lives in > a set of subdirectories within the codebase. For example, just look at the aurora > sourcecode, which includes the aurora scheduler, the aurora executor, thermos, > and the aurora client. > > If multiple services live in a codebase, then users can't put an AuroraInit file in > the root directory of the codebase, because it would interfere with other users' > work. > > The home directory fallback provides an easy way for users to set up a pointer > to the correct init file, which won't be removed by operations like switching branches > in a source repository. Users write shared init files, which are located in their projects > in the codebase, and create a symbolic link from their project init file to their home > directory. ### What goes into an init file? We should support the following kinds of things: 1. Universal defaults - user-defined default settings that will be applied to all commands. For example, if there is a default config file that should always be used if the user doesn't specify one, that would be a universal default. 2. Command specific defaults - users should be able to specify that they always want to use certain parameter settings for a specific command. For example, if they want to always use a default batch size of 10 for updates, but don't want to affect batch sizes for other commands like kill, they could use a command specific default. 3. Aliases - shorthand names for longer parameters. A user could specify shorthands "east" and "west" for full jobkeys in two different datacenters. ### Defaults A default specifies a set of _bindings_. If a parameter is omitted from a command, and there's a binding for that parameter, it will be automatically inserted into the command as if the user had typed it. The binding is specified in the configuration file using a Python dictionary. For example, if the defaults included `{'jobspec': 'devcluster/me/prod/service'}`, then if you ran `aurora job create` without specifying any parameters, the command-line would automatically substitute `devcluster/me/prod/service` for the `jobspec` parameter. Defaults can be declared either globally (in which case they'll be inserted as parameters for all commands), or for specific commands (in which case they'll only be inserted for a single command). ### Aliases An alias is a short equivalent for a parameter. When a command line is provided by a user, aliases will be expanded inline. A user can specifically mark an alias for expansion by prefixing it with "@"; if an alias appears on the command-line surrounded by whitespace, it will be replaced even if it isn't marked with an "@". > **Sidebar: why not just use shell substitutions?** > > The @-substitution model proposed here is very similar to unix variable > substitution. So why implement the same thing all over again? Why not just tell > users to use their shell? > > There are several reasons. > > 1. All of the options and aliases that affect aurora command invocations can be specified > in one place: the AuroraInit file. > 2. Command shell substitution is complex - the ways in which the command shell does > expansion, tokenization, substitution, and parsing can be very hard to follow. Adding > the aurora aliases to that process just adds another layer of confusion to the process. > With internal alias substitution, we can avoid the confusions of expansions and > tokenization for aurora aliases. > 3. We can provide better debugging and error messages with our own alias substitution. > For example, if a user specifies a job using aliases like "`aurora job create @c/@me/test/myservice`", > we can create an error message that shows exactly what they typed, and how it expanded: > <pre> > Job "west/markcc/test/myservice" not found in configuration file config.aurora. > Jobkey was generated by alias expansion: original key was "@c/@me/test/myservice", where: > - "@c" was expanded to "west" > - "@me" was expanded to "markcc" > config_file parameter "config.aurora" was specified from defaults. > </pre> ### Defining Shorthands and Defaults: Syntax and Examples The init file is, like aurora job configurations, a python source file using a Pystachio schema. The schema is loaded, and an `Init` object is retrieved from the top-level variable `init` in that file. The pystachio schema for init files is: class CommandDefaults(Struct) command = Required(String) defaults=Map(String, String) class Init(Struct): defaults=Map(String, String) command_defaults = Optional(CommandDefaults) aliases=Map(String, String) For example, an init file could contain the following: init=Init( defaults={ "jobspec": "west/markcc/test/myservice", "config_file": "./src/aurora/myservice.aurora" }, aliases={ "east": "east/markcc/test/myservice", "c": "east", "me": "markchucarroll" }, command_defaults(command="job update", defaults={"--batch-size": 10} )) With this configuration file, if the user ran "`aurora job create`" without any parameters, it would automatically be expanded to "`aurora job create west/markcc/test/myservice ./src/aurora/myservice.aurora`". If a user ran "`aurora job create east`", the alias `east` would be expanded to "east/markcc/test/myservice", and the missing config file parameter would be instantiated using the default, to create a command-line: "`aurora job create east/markcc/test/myservice ./src/aurora/myservice.aurora`". If a user ran "`aurora job create @c/@me/test/myservice`", the two aliases would be expanded, and the omitted configuration parameter would be added: "`aurora job create east/markchucarroll/test/myservice ./src/aurora/myservice.aurora`". If a user ran "`aurora job update`", then the `jobspec` and `config_file` parameters would get inserted from the global defaults, and the "--batch-size=10" would be inserted from the command defaults for "aurora job update".