Giter Club home page Giter Club logo

ops's People

Contributors

dependabot[bot] avatar nickthecook avatar rmtlynick avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

erikhumphrey

ops's Issues

Handle error cases more neatly

Most of the time, if something is not as ops expects (e.g. ops.yml not present, the structure of a yml or json file is not what ops expects, etc.) ops will exit with a stack trace.

For many of these cases, ops should be more defensive and print a helpful error, rather than just let the stack trace rise to the top.

A []: No such method for NilClass error is not very helpful to the user, but Error: expected "actions" section in ops.yml is.

Add support for argument-checking in Actions

When ops runs an action, it appends all command-line arguments to the command specified in the action config. Thus, it's easy to write actions that take arguments. For example:

actions:
  hello:
    command: echo Hello,
$ ops hello these are arguments.
Running 'echo Hello, these are arguments' from ops.yml in environment 'dev'...
Hello, these are arguments.

Also, if you're just running another executable or a script from the action, that executable or script can check its own arguments.

However, sometimes the action uses a specific subset of another executable's functionality. E.g., a script to run a command inside each of a set of containers shouldn't care what the command is or how many containers you tell it to run the command in, but if you have an ops action like console that needs to run a command in exactly one container, that action cares that exactly one argument is supplied.

It's also not obvious to a user that the command needs exactly one argument unless the developer writes it into the action description. And if the description contains the info, it could become stale and inaccurate if the command is updated and the description isn't (as unlikely as it is that a developer would do such a thing).

To help with this case, ops could take specifications like this:

actions:
  console:
    args:
      container_name:
        description: the name of one of the ops test containers
        mandatory: yes
    extra_args_allowed: no
    command: bin/run.sh bash "$container_name"

ops would assign its first command-line argument to the variable $container_name.

This approach:

  • documents the command for the user
  • enforces the documented usage, to prevent drift
  • might be flexible enough to allow most basic actions to get command argument checking

The default for extra_args_allowed should be true (thanks to YAML, a value of yes evaulates to true), and it should work on commands that do not define args.

Alternative, shorter format

An alternative format could also be supported, to make it less onerous to add argument-checking, and leave ops.yml a little smaller:

actions:
  console:
    args:
      - container_name
    extra_args_allowed: no
    command: bin/run.sh bash "$container_name"

Solution for argument interpolation

This approach may also partially solve #10. It would still be nice to have unnamed arguments interpolated into the command, but anyone wanting that could get something like it in most cases by using this functionality. Naming an argument is a bit more work to add to an action that just putting $1 in a command string, but it would work.

Implementation

There are a couple of ways to implement this, at a high level:

  1. update the string before executing the command

This method would have ops gsub all occurrences of $container_name in the command string before executing the command.

Pros: ops could warn if an argument was specified but not used in the command string
Cons: would evaluate all variable references regardless of quoting, which may lead to unexpected results

  1. set environment variables for each arg

This method would have ops set the environment variable "$container_name` to the value of its first argument before executing the command, and execute the command unmodified.

Pros: this would respect shell quoting; e.g. variable references inside single quotes would not be evaluated by the shell
Cons: ops could still warn about unused args, but it wouldn't be accurate if an arg reference occurred inside single-quotes


I feel that the second option is better via the Principle of Least Surprise, since someone writing a shell command wouldn't expect a variable reference inside single-quotes to be evaluated. The ability to warn about unused argument references is of questionable value, since perhaps the arg could be an environment variable that is not used in the command string, but is used by the script or command that it calls.

Add `pip` dependency support

Need to write a project with Python using pyenv to figure out what options are needed for controlling how pip is called, whether to call pip3, whether there are clues in the environment to that ops should look at, etc.

Support YAML config

Sometimes you want a .yml config file instead of a .json config file.

Secrets need not support YAML; there is no eyaml, and secrets files for production should always be encrypted, so ops should not encourage storing secrets in a format that is not easily encryptable.

Support `sshkey` dependency

dependencies:
  sshkey:
    - keys/user@server

ops should generate the above key with no passphrase if it does not already exist.

ops could also print a stern warning to add the files or the directory to .gitignore. ops could actually add the file, if it finds a .gitignore file already there, and it looks like git isn't already ignoring the keys.

Ability to load secrets for some dependencies

ops can load secrets before running actions, but not before trying to satisfy dependencies.

Secrets could be useful in dependencies for, e.g.:

dependencies:
  custom:
    - docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD privateregistry.example.com
    - terraform init -backend-config=$tf_be_config

For security, it would be best to allow secrets loading to be enabled per-dependency, or at least per-dependency-type. Also, it would be best to have the configuration to load secrets visible in the dependencies section, rather than in the options section, which could be at the end of the file and not visible to users while they're adding or modifying dependencies.

Option 1: dependencies-with-secrets

Add support for a dependencies-with-secrets section that works exactly like the dependencies section, except that dependencies listed in the former would have secrets loaded into the environment first.

dependencies-with-secrets could be processed before or after dependencies, but probably after. If a project uses terraform, for example, the dependencies section would install terraform, while dependencies-with-secrets would run terraform init .., which depends on terraform being installed.

Option 2: custom-with-secrets

If the only use case for access to secrets while installing dependencies is in custom dependencies, perhaps ops only needs a custom-with-secrets section, in which custom dependencies are executed after loading secrets.

This is probably as much work as Option 1, dependencies-with-secrets, while limiting functionality more.

Option 3: call an action from a custom dependency

This works now:

dependencies:
  custom:
    - ops tf-init
actions:
  tf-init:
    command: terraform init -backend-config=$tf_be_config
    load_secrets: true

It also has the benefit of allowing the user to run the terraform initialization independent of the other dependencies in ops up.

This:

  • works now
  • keeps the load_secrets option highly visible to users adding or modifying dependencies
  • may be a cleaner, DRYer structure for custom dependencies that running commands directly in custom dependencies

Recommendation

Use Option 3 for now; if secrets are required outside custom dependencies or brew install and apt install commands end up in actions, then consider Option 1.

`service` dependency

Sometimes an app needs to have certain services running in order to function. These can be started as part of ops up using custom dependencies, but that is onerous when the app may be run on Mac or Linux. E.g.:

dependencies:
  custom:
    - which brew && brew service start some_service || service start some_service

If ops had a service dependency, which was platform-aware, the dependency would look like this:

dependencies:
  service:
    - some_service

Benefits include:

  • the latter dependency is easier to read
  • ops down would stop the service (right now there's no way to write a custom command to be run on ops down)
  • platform detection could be better than which brew or uname | something, and could support multiple variants of linux service management (e.g. service, systemd, etc.)

Evaluate environment variable references without using `echo ...`

When using the options.config.path option or anything in options.environment, ops will evaluate the string from the ops.yml file in the shell first, to expand any variable references that may be in it. This is not good because:

  • quoting will be lost
  • things other than environment variables will be expanded (e.g. {}, [])

This could be confusing for users, or even dangerous, in that a command with quotes stripped may delete data that is otherwise unrecoverable.

Replace occurrences of this (Secrets, Environment) with calls to a new helper class/method that will expand environment variable references using string substitution.

Will this seem odd to the user when it expands variables that are inside single-quotes in the option value?

This new helper can then be used in #10: it can be modified to accept a hash of variables to expand in addition to those defined in ENV, like {1: 'first_arg', 2: 'second_arg'}.

Allow specifying gem version

Just ran into an issue where bundler 1.17.2 was installed. gem i bundler gets 2.1.4, but ops was skipping installing the gem because it was already installed.

Don't know how the old one got there, but ops wasn't trying to install the new one.

Support version specification for gem dependencies, so ops will install a gem if one that is too old is installed.

Add capability to load secrets from `.ejson` files into environment variables before executing command

It's common for applications to need access to secrets without having those secrets committed in plain text in the repo. ejson is a useful tool for encrypting secrets within a file so that the file can be committed safely. All you need to do is get the right private key to the application, and it can decrypt all its other secrets.

Within an application written in, e.g., Ruby, it is easy to load secrets from an ejson file. However, for apps that are written in languages like Terraform or shell script, it can be challenging. Also, for applications that have code from multiple languages (e.g. Terraform + Ruby) often both types of code need access to secrets, meaning you need to either write a shim or implement secret loading in both languages.

Well, ops is a lot like a shim that loads an application. If it could load secrets from an ejson file and store them in environment variables then every command run by ops could use that functionality, regardless of language.

Where to load secrets from?

Since no one location will work for every repo ops should pick a reasonable default and allow it to be overridden in config. Also, since it's common to have different config and secrets for different execution environments, ops should look in different places for the secrets depending on the environment.

ops will look in the environment variable environment to get the current execution environment; e.g. dev, prod, staging. It will then look in the following places, in order, for a secrets file:

  • config/$environment/secrets.ejson
  • config/$environment/secrets.json (so that in dev secrets don't need to be encrypted)
  • a location specified by the following attributes in ops.yml:
options:
  secrets:
    path: "secrets/$environment/secrets.ejson"

Environment variables in the path value will be expanded, because there is a high likelihood that the path would depend on at least the current environment (dev, staging, prod, etc.).

This variable expansion will be for that field only. In the future, broader support for shell variable expansion may be warranted.

Load secrets for all Actions?

Some actions will require access to secrets, and some will not. Running some actions access with secrets in their environment may be dangerous, e.g. if the action logs its environment, or calls something that might.

Therefore, the default will be to not load secrets into environment variables. To enable secret loading in an action:

actions:
  start:
    command: bin/run-my-app
    load_secrets: true

What would the secrets file format be?

Sometimes an app's secrets file may contain things that are not simply key-value pairs. For this reason, the secrets file format for ops should contain an environment sub-key to contain secrets to be loaded into environment variables, leaving the rest of the key space for the application's use.

{
  "environment": {
    "key1": "EJ[1...",
  },
  "application_specific_stuff": {
    "key2": "EJ[1...",
    "nested_data": {
      "key3": "EJ[1..."
    }
  }
}

In the above example, an environment variable called key1 will be set to the decrypted value from the file. Variables key2 and key3 will not be set.

Use `sudo` for `apt` dependencies

By default, use sudo for apt dependencies.

We don't want people to have to use sudo to run ops, or to get into the habit of it. Only the apt dependencies need root privileges, so only the apt commands should be run with them.

DO NOT use sudo if:

  • $(whoami) == root OR
  • options.apt.sudo == false

Add argument checking

ops can be used to run scripts, e.g. in a bin/ directory, passing command-line arguments to the script. It would be useful if ops could also perform some checks on arguments.

The minimum check would be checking number of arguments. It could also perform some validation of the argument values.

With the action definition

actions:
  hello:
    command: echo "Hello, "
    arguments:
      name:
        optional: false
        description: says 'hello' to the given name

, ops could infer that the required number of arguments is one. ops hello would print:

ops: Usage: hello <name>

says 'hello' to the given name

and exit with the existing syntax error status code.

ops help hello could print the same message, but exit with 0.

actions:
  copy:
    command: scp -i key/id_rsa
    arguments:
      sources:
        optional: false
        multiple: true
        description: a list of locations to copy files from; must be all local paths or all remote paths
      destination:
        optional: false
        description: the location to which to copy files; can be a local or remote file

From this action definition, ops copy file host: would execute the command. ops copy file1 file2 host would execute the command. ops copy or ops help copy would print:

ops: Usage: copy <sources> <destination>

sources: a list of locations to copy files from; must be all local paths or all remote paths
desintations: the location to which to copy files; can be a local or remote file

Should more than one multiple: true argument be allowed? Can ops perform syntax checking in this case?
Does this add too much complexity for the value it delivers? ops will be less of a pleasure to use if users feel that because this feature exists they must now document parameters. It does, however, help users document the scripts they use in their project.
Does it make sense for ops to do this? Scripts can be called outside of ops, and ops should never assume otherwise. Argument checking would be better in the script itself.

add top-level `forwards` section

Consider a project in which there is:

  • a top-level infrastructure project, e.g. terraform code, that creates infrastructure and deploys an app to it
  • an app directory, self-contained, with its own ops.yml, that can be run on a development machine or on deployed infrastructure

It's common in this case to have the top-level project provide secrets and config that are also needed by the app project. In that case, the developer must choose one of the following options for using top-level secrets and config in the app:

  • duplicate the config and secrets (not great)
  • generate app config and secrets in the top-level project (it's a pain to generate an ejson file from terraform in all your apps, and makes it harder for a developer to find the config and secrets since they're not checked into the app directory where they're used)
  • "forward" a bunch of ops commands to the app dir from the top-level dir (verbose and ugly - see below)

"Forwarding" in ops looks like this:

actions:
  pack:
    command: cd packer && ops pack-all
    alias: p
  pack-force:
    command: cd packer && ops pack-force-all
    alias: pf
  pack-all:
    command: cd packer && ops pack-all
    alias: pa
  pack-force-all:
    command: cd packer && ops pack-force-all
    alias: pfa

But fowarding could look like this:

forwards:
  packer:
    - pack, p
    - pack-force, pf
    - pack-all, pa
    - pack-force-all, pfa

This is more idiomatic, communicating more clearly to the reader that a subdirectory handles these. It's like the difference between a for loop and the ruby .each: obvious intent (and less typing).

In the case of having subprojects inherit top-level secrets and config, however, it doesn't work when running on deployed infrastructure. The top-level dir shouldn't be copied to the infrastructure at all. Is there another use case where this does make sense?

Test on linux

Create a VM in some cloud computing platform on which to run e2e tests.

Benefits:

  • ability to test apt outside unit tests
  • ability to test service if it's implemented
  • ability to test background if it's implemented
  • test for case-sensitivity differences (e.g. require 'english' vs. require 'English')

User-defined templates

ops has some built-in templates (e.g. ruby, terraform). If a user wants a different template, they have to either:

  1. store a file somewhere and copy it into a repo when initializing a new app
  2. send a PR to this project to add their template (which may not have universal utility)

ops should look for templates first in ~/.ops/templates, then fall back to built-in templates.

The ~/.ops directory can be used in the future for global (rather than project-specific) config.

Handle hanging dependency execution commands

Sometimes a command meant to meet a dependency hangs, either because it's stuck or because it's asking for input. I need to CTRL+C after a while, but then I don't get to see the output, to figure out why it failed.

ops could:

  1. redirect input from /dev/null, so that any command that tried to read from stdin would error out, which would cause ops to print stdout and stderr.

  2. trap CTRL+C, print the command's stdout and stderr, and kill the command

The first one seems like it would be easier (because in the second, a signal handler has to find the pid of the command that is currently running). However, the first one would only help when a command hangs because it waits for input; it would not help with processes that get stuck for other reasons.

The first option should be implemented anyway, since there is no mechanism for feeding user input to commands that are meant to meet a dependency.

Load secrets and config before processing options.environment

There is a use case in which a developer wants to alias an environment variable, because their environment will have one variable set, but they need to have the value of that variable in another variable with a specific name.

For example, terraform needs variables named starting with TF_VAR_ in order for it to be used in terraform code. If we had a variable set by CI like $REGISTRY_FQDN, and we wanted to use that registry domain name in our terraform code, we'd need to do something like:

export TF_VAR_registry_fqdn="$REGISTRY_FQDN"

ops has a facility for this:

options:
  environment:
    TF_VAR_registry_fqdn: $REGISTRY_FQDN

This works if the variable is set when ops is run. However, it does not work when the variable comes from config or secrets. We need to be able to alias variables that come from config and secrets, because, as in the earlier example, CI may set a variable, but in the dev environment, the app isn't run by CI, so we need to put those values in our environment config.

The reason this doesn't work for config is that Ops sets environment variables before it loads app config. That's easy to fix; the lines are adjacent and can be reordered.

The reason this doesn't work for secrets is that Action loads secrets if it's config says to, so Ops can't just load secrets with some logic around it. Also, since the setting that causes Action to load secrets is within the action definition, it seems a bit unnatural. The best solution may be to rework Ops so that the Action has a hook to load secrets if it wants to. Ops can call this before setting environment variables.

add ability to run one-liner

Support one-liners from the command line. E.g.:

ops exec 'echo $CONFIG_VAR'

Useful for running commands within the ops environment to, for example, debug issues with environment variables in config and secrets.

Automatically build platform containers when platform tests are run

On a machine with no platform test container image, ops tpe produces this:

$ ops tpe
Running 'cd platforms && ops test-e2e ' from ops.yml in environment 'dev'...
Running 'bin/run.sh "bin/ops test-e2e" $TEST_PLATFORMS ' from ops.yml in environment 'dev'...
bin/run.sh: Running 'bin/ops test-e2e' on platforms: ops-debian
bin/run.sh: Mounting '/Users/nickthecook/src/ops' into the container at '/ops'.
bin/run.sh: Running new container 'ops-debian_bin_ops_test_e2e' from image 'ops-debian'...
Unable to find image 'ops-debian:latest' locally
docker: Error response from daemon: pull access denied for ops-debian, repository does not exist or may require 'docker login': denied: requested access to the resource is denied.
See 'docker run --help'.

That's because the container image ops-debian did not exist, so docker thought it was meant to pull the container from docker hub.

Automatically build the image if it's not already present when ops t or ops e2e are run in platforms/. Possibly using the new before hooks...?

Fall back to `.json` secrets file if `.ejson` file cannot be found

By default, ops will load secrets from config/$environment/secrets.ejson. If this file does not exist, it will load secrets from config/$environment/secrets.json. This allows the development environment to have unencrypted secrets (which is safe because development secrets should not be checked in).

However, if the user overrides the secrets file path with options.secrets.path, this fall-back-to-json logic is not employed.

Even if a repo changed the path to the secrets file, it is still likely that there would be different secrets for different environments, and that the development secrets would still not be committed to source control. Therefore, the fallback-to-json logic should be used in this case as well.

E.g., with the following options:

options:
  secrets:
    path: "secrets/$environment.ejson"

ops should look for that file first, and, if it does not exist, look for secrets/$environment.json.

`project` dependency

Sometimes one repo needs a service defined in another repo to be running in order to run itself. E.g. there is an app and a monitoring system; the app needs to send data to the monitoring system (or, at least, a developer should be able to test this).

With a dependency like this defined in the app's ops.yml:

dependencies:
  project:
    - ~/src/monitoring

ops up should try to start the service in the given project directory (monitoring). The monitoring project must have an ops.yml file as well, that supports the following actions:

  • start
  • stop
  • status
  • (optionally) restart

Because these are not ops builtins, ops will need to be able to provide a helpful message in the case that the other project has not defined these actions.

Some consideration must be given to how to invoke these other actions, and whether they must, by themselves, run in the background. E.g. perhaps ops start runs a server in the foreground, so that a developer can see the log output.

  • Should ops run start in a background process, or dictate that the ops start action must start a service and return?
  • Should ops use an action other than start, so that the action used to start projects does not conflict with a commonly-used action name?
  • Should ops use a builtin to handle this, e.g. ops service start that will run ops start in the background?
  • Should ops make the logs of backgrounded services available to developers via commands like ops logs, or rely on the app to write its own logs?

Set `environment` environment variable when running a command

Could be handy to have $environment set to dev, prod, staging, etc. automatically when running actions.

Ops already has the concept of environment, to support the ops env command. This would just be a matter of setting the variable in Action before executing.

The user should be able to disable this behaviour with config, and if environment is already set Ops should maybe not set it.

Make test containers faster

The test container is removed every time it's run. This was simpler to implement, but it means that bundler will take 20-30s to install dependencies every run.

Since the working copy of ops is just linked into the container, the same container could just be started with docker start instead of being created with docker run most of the time. We should only need to create a new container when the underlying image changes.

An optimization to call docker start if the container already exists would be nice. Also, remove the container at the end of build.sh, so that when we rebuild the image, the container needs to be recreated.

Add verbose mode

It would be helpful to have a verbose mode, either in the options section of ops.yml, as a command-line option like -v, or both.

Verbose mode should print things like:

  • where ops looks for secrets files, and whether it finds them
  • where ops looks for app config files, and whether is finds them
  • what ENV vars ops loads
  • what commands ops runs to satisfy dependencies

Provide proper test report from platform tests

Right now, ops will run tests on different platforms via containers (yay!) but it just executes the tests in containers serially. To determine if there was a failure, the user must scroll up and look through the terminal output. If there is a failure, the user must scroll up above that to the find the most recent line that says which container was being executed.

It would be nice to have RSpec roll up these results in a traditional RSpec report, or at least have a summary printed at the end.

Perhaps RSpec could execute the container runs, so that each platform appears as only one test. That isn't as good as having all tests rolled up and magically wrapped in a context like "when running on debian", but it is probably achievable. If there's a failure, the user can scroll up to look through the output for that container to see the details of the test run.

Another feature that would be useful with the above is if each container output was put in a text file, and printed in full if there was an error running tests in that container. This might even work without having RSpec run tests in each container as a single test...

Improve security of `sshkey`

When used properly, the literal passphrase for an SSH key should never be configured directly in options.sshkey.passphrase. That value should be an environment variable, possibly loaded from a secrets file (but never loaded from a plaintext file that is checked in).

Currently, it's easy for a lazy (and, as programmers, we're all lazy) user to just put a plaintext passphrase in ops.yml.

ops could have an attribute like options.sshkey.passphrase_variable which took the name of a variable, instead of allowing the passphrase to be put directly in the file as a string.

ops could have an attribute like options.sshkey.passphrase_secret, which would only look in the configured secrets file for this variable. This would be more secure, but might break the workflow of users who manage passphrases outside ops.

Fix apt errors during ops up in platform test

ops t
Running 'bin/run.sh "bin/ops test" $TEST_PLATFORMS ' from ops.yml in environment 'dev'...
bin/run.sh: Running 'bin/ops test' on platforms: ops-debian
bin/run.sh: Mounting '/Users/nickthecook/src/ops' into the container at '/ops'.
bin/run.sh: Starting existing container 'ops-debian_bin_ops_test'...
/entrypoint.sh: loading SSH agent...
Agent pid 8
/entrypoint.sh: running 'bundler install'...
/entrypoint.sh: running 'ops up'...
/usr/bin/apt-get
[Apt] curl                                         FAILED
Error meeting Apt dependency 'curl':

/usr/bin/apt-get
[Apt] sl                                           FAILED
Error meeting Apt dependency 'sl':

[Gem] bundler                                      OK
[Gem] rerun                                        OK
[Gem] ejson                                        OK
[Dir] runtime_data                                 OK
[Custom] bundle install --quiet                    OK
[Custom] echo this is stdout                       OK
/entrypoint.sh: Running command: bin/ops test
Running 'environment=test bundle exec rspec --exclude-pattern 'spec/e2e/**/*_spec.rb'' from ops.yml in environment 'dev'...
 164/164 |============================================ 100 =============================================>| Time: 00:00:00

Finished in 0.60002 seconds (files took 0.69255 seconds to load)
164 examples, 0 failures

`background` builtin

Description

ops should support a background builtin that runs any action as a background task. E.g., with this action defined:

actions:
  start:
    command: rackup

ops start will run rackup in the current terminal; the logs will be printed to the screen, and the user can CTRL+C to kill the app.

This is what the user would want if they were running the app directly, however:

  • CI systems will need to start the app in the background sometimes; e.g. to run end-to-end tests
  • the `project dependency needs the ability to run an existing action as a background task

ops background start should run the action start as a background service. This may be accomplished using screen, tmux, nohup, etc.

Ideally, aliases for builtins could be implemented, so that ops bg functions like ops background.

Interaction with user-defined actions

With this feature, the user can write start, stop, and status, and either run the app in the foreground (ops start) or the background (ops background start). If the status and stop actions are written properly, they should work whether the app is in the background or not, regardless of the underlying mechanism used by ops for backgrounding the app.

One action the user will not be able to write without an awareness of the underlying backgrounding mechanism is a logs action that is meant to dump the logs from the current or previous background session. ops may provide a builtin to accomplish this (e.g. background-logs; simply logs would be likely to conflict with user-defined actions), or it may choose to force the user to be aware of the backgrounding mechanism to do this themselves*.

*(The rest of this issue assumes ops will implement the background-logs feature)

If aliases for builtins are implemented (as described above, so ops bg is an alias for ops background) then ops bglogs could be a more usable builtin than background-logs.

Logs

The background-logs or bglogs builtin should take the name of an action, and dump the output of that action's background session to the terminal.

actions:
  hello:
    command: echo HELLO && sleep 60 && echo GOODBYE
$ ops bg hello
Running `hello` in a background session...
$ ops bglogs hello
Displaying logs from background session for 'hello`...
HELLO
Background session 'hello' is still running.
$ # wait one minute
$ ops bglogs hello
HELLO
GOODBYE
Background session 'hello' exited with status 0.
$ ops bglogs nosuch
There is no background session for the action 'nosuch'.
$ 

Follow logs

ops bglogs -f hello should follow the logs from the background session for hello.

$ ops bglogs -f hello
Following logs from background session for 'hello'...
HELLO
<terminal waits for more output until user presses CTRL+C or process terminates>

If the user enters any input, keystrokes are not sent to the app. CTRL+C terminates the ops bglogs -f session, not the app.

Attach

It should be possible for the user to attach their console to a background session.

$ ops attach hello
Attaching to background session for action 'hello'...
HELLO
<terminal waits for more output until user presses CTRL+C or process terminates>

If the user enters any input, it is sent to the app. CTRL+C terminates the app. Any other signals generated from the keyboard are sent to the app.

Env-specific commands

Sometimes an action should be performed differently in different environments.

E.g. if you're developing on MacOS, but production runs linux, different commands are required to start or stop services.

Support this syntax:

actions:
  start:
    development:
      command: brew service start myapp
   command: sudo service start myapp

actions.start.command will be the default in all environments unless overridden by the presence of an action.start.#{environment}.command.

Return better error when invalid key present in config

If you accidentally write something like this:

actions:
  deploy:
    comand: ./deploy-foo.sh
    description: Deploy foo

In which the command key is misspelled comand, such as due to a typographical error, ops returns:

 Running '' from ops.yml in environment 'ci'...
 /usr/local/bundle/gems/ops_team-0.9.7/lib/action.rb:16:in `exec': No such file or directory -  (Errno::ENOENT)
 	from /usr/local/bundle/gems/ops_team-0.9.7/lib/action.rb:16:in `run'
 	from /usr/local/bundle/gems/ops_team-0.9.7/lib/ops.rb:62:in `run_action'
 	from /usr/local/bundle/gems/ops_team-0.9.7/lib/ops.rb:38:in `run'
 	from /usr/local/bundle/gems/ops_team-0.9.7/bin/ops:8:in `<top (required)>'
 	from /usr/local/bundle/bin/ops:23:in `load'
 	from /usr/local/bundle/bin/ops:23:in `<main>'
(exits with retval 1)

ops should provide a better syntax error and possibly have a "Did you mean: command?`" for cases like this (similar to #34)

Allow easy adding of passphrase-protected SSH key to keychain

With the sshkey dependency, one can set a passphrase which is stored in a secret that is loaded into an ENV var. E.g.:

options:
  sshkey:
    passphrase: $SSH_KEY_PASSPHRASE
    load_secrets: true

This works well insofar as generating a protected key that I can check in, and storing the passphrase securely, so it can be checked in as well.

However, when using the SSH key, there is not easy way. I need to:

  • copy the passphrase from my secrets file (using ejson decrypt if it's an ejson file)
  • run ssh-add keys/..., pasting the copied passphrase

I could have an ops command to load the key as well, which would prevent me from having to know where the key is stored, but I would still need to provide the passphrase.

However it works, the solution needs to:

  • ensure ops up doesn't hang waiting for input
  • ensure that duplicate keys are not added to the agent
  • allow the user to disable adding keys with an option
  • figure out how to feed input to ssh-add, which seems determined to only take input from the user

Options

Have an ops builtin that adds the ssh key

ops sshadd could automatically load all configured SSH keys with the configured passphrase. This could also be run from ops up. No input, and if ops is configured to generate passphrase-protected keys, this should still work because sshkey options contain the secret to use as the passphrase.

To do this, ops should first parse - in builtin names, e.g. ssh-add would load the class SshAdd. This could be retrofitted onto the sshkey dependency, making it ssh-key.

Auto-load key as another feature of the ssh-key dependency

Check for $SSH_AUTH_SOCK. If it's set, automatically add the key to the agent. ssh-add will not work without $SSH_AUTH_SOCK, so this is probably a good indicator of whether the user would like the key added automatically.

Key adding could be disabled if the following option is set:

options:
  sshkey:
    add_keys: false

Use an expect script inline in ops.yml

Eww. But it would work.

Add shell completion config

It would be nice to have shell tab-completion for ops actions and builtins.

Partially entered actions or builtins would be completed for the user when they press .

Add support for multiple commands per action

Like #18, but allowing multiple commands to be specified as a typical YAML collection, to be executed in order / from the top down.

Considerations

Should having multiple commands in one action behave like command1; command2; command3 or command1 && command2 && command3? Based on similar YAML command runners, probably the latter. The former could still be possible with something like ignore_errors: true on the action.

Write "end-to-end" tests

Implement tests that actually define ops.yml files, run ops commands from each of them, and check for the expected effects.

Each test suite can be in its own directory under spec/e2e.

Try to focus on testing that new changes don't break existing functionality. Not every new changes needs e2e tests added, and not every code branch needs an e2e test.

Write docs for builtins

It would be nice to have docs for all builtins. E.g.:

  • Apt and Gem have some version pinning behaviour that would be good to document (since Gem can handle version specs like '>=1.2.3' but Apt cannot)
  • lots of builtins support Options, like apt.use_sudo, that aren't documented at the moment

Possibly use rdoc or similar so the documentation can go in the code and be built automatically.

Docs would just go into the repo, and need to be browsable.

Allow specifying version in brew dependency

Looks like brew just uses git to pull packages and build them, and ops could check out a specific commit hash or branch:

https://stackoverflow.com/questions/39187812/homebrew-how-to-install-older-versions

dependencies:
  brew:
    - tflint@311029c24de3de608e78afe0ee4f2413ea7a792b # this is tflint release 0.18.0 in brew's formulae

Usage of @

Using the @ might be tricky, because there are actually some brew packages that have names with @ in them to denote versions, e.g.:

$ pwd
/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core
$ ls Formula/openssl*
Formula/[email protected]

Option 1

ops could look for a package like this, and only try to use it as a git ref if a package with that name doesn't already exist in Homebrew. This might be too much trouble.

Option 2

Another option is to allow an expanded format of the depedency:

dependencies:
  brew:
    -
      name: tflint
      git_ref: 311029c24de3de608e78afe0ee4f2413ea7a792b
    - [email protected]

One brew dependency there is a hash, and one is a string. This also might be too much trouble.

Option 3

A third option is to use a character other than @. While ruby uses @, pip uses ==, and apt uses =. It might not be counterintuitive for developers to use =.

Return better error when `ejson decrypt` fails

This was caused by an empty value in an ejson file:

[10:52 AM] Jack Harold
    ackharold@Jacks-MacBook-Pro heliograf % ops up
[Brew] terraform                                   OK
[Brew] ansible                                     OK
[Dir] app/connections                              OK
[Dir] app/ssl                                      OK
[Dir] provisioning                                 OK
[Sshkey] keys/$environment/rcgtadmin@heliograf     Decryption failed: invalid message format
Traceback (most recent call last):
        12: from /usr/local/bin/ops:23:in `<main>'
        11: from /usr/local/bin/ops:23:in `load'
        10: from /Library/Ruby/Gems/2.6.0/gems/ops_team-0.8.8/bin/ops:8:in `<top (required)>'
         9: from /Library/Ruby/Gems/2.6.0/gems/ops_team-0.8.8/lib/ops.rb:32:in `run'
         8: from /Library/Ruby/Gems/2.6.0/gems/ops_team-0.8.8/lib/ops.rb:54:in `run_action'
         7: from /Library/Ruby/Gems/2.6.0/gems/ops_team-0.8.8/lib/builtins/up.rb:20:in `run'
         6: from /Library/Ruby/Gems/2.6.0/gems/ops_team-0.8.8/lib/builtins/up.rb:30:in `meet_dependencies'
         5: from /Library/Ruby/Gems/2.6.0/gems/ops_team-0.8.8/lib/builtins/up.rb:30:in `each'
         4: from /Library/Ruby/Gems/2.6.0/gems/ops_team-0.8.8/lib/builtins/up.rb:36:in `block in meet_dependencies'
         3: from /Library/Ruby/Gems/2.6.0/gems/ops_team-0.8.8/lib/builtins/up.rb:42:in `meet_dependency'
         2: from /Library/Ruby/Gems/2.6.0/gems/ops_team-0.8.8/lib/dependencies/sshkey.rb:19:in `meet'
         1: from /Library/Ruby/Gems/2.6.0/gems/ops_team-0.8.8/lib/app_config.rb:6:in `load'
/Library/Ruby/Gems/2.6.0/gems/ops_team-0.8.8/lib/app_config.rb:25:in `load': undefined method `[]' for nil:NilClass (NoMethodError)

Add support for multiple aliases per action

Right now, an ops action can have one alias. Often, you only ever need one alias to make running an action faster: you would have a command and a short-form alias, e.g. ops log, with ops l aliased to it. However, sometimes you also might want to have more, especially if a developer's intuition is to run a synonym of a command or a slight variation on the word. An example of this would be wanting to run ops log instead of ops logs where the only alias available is already ops l. In the interest of allowing ops to work the first time every time for a developer that hasn't written or read the config, it would be nice to be able to configure actions to have multiple aliases, and allowing more aliases to be added for an action as necessary to increase productivity / accelerate workflow.

The existing functionality for a single alias should still work:

actions:
  logs:
    command: tail -f /var/log/messages
    alias: l

But it would also be possible to use aliases as a collection:

actions:
  logs:
    command: tail -f /var/log/messages
    aliases:
      - l
      - log
      - history

Maybe alias and aliases could be used in the same way as each other, i.e., the two would be interchangeable and using aliases / alias would not force you to use or not use a collections of aliases. This way, if you go from two aliases down to just one, or one alias to two aliases, you won't have to change the keyword.

Make it easier to implement e2e specs

There are a few things that make implementing an e2e spec more work and less DRY than it should be.

This is an example:

RSpec.describe "ssh key with passphrase var" do
  include_context "ops e2e"

  before(:all) do
    Dir.chdir(__dir__)

    remove_untracked_files

    @output, @output_file, @exit_status = run_ops("../../../../bin/ops up")
  end

  # actual tests goes here
end

Things that could be improved:

  • doing a chdir: must be done in every spec, correctly, or the tests will run with a) no ops.yml or b) worse: the wrong ops.yml
  • removing untracked files: must be done in every spec (so far; maybe someday a spec won't need it)
  • setting local variables: I'm not sure how legit this is to do in an rspec spec; maybe there's a better way
  • knowing the path back to the ops installation: maybe a method in "ops e2e" context could handle this, if you pass an array like ["up", "my_action", "down"]

Basically, having to copy and paste this block to every spec and tweaking the path to bin/ops is not ideal.

Add argument interpolation inside commands

ops will append any command-line arguments to the end of the command when executing an action. For example, with this config:

Current behaviour

actions:
  say_it:
    command: echo

and this command:

ops say_it hi there

the output will be:

hi there

Problem

However, that doesn't allow for commands like this:

actions:
  scp:
    command: scp -i keys/my_private_key $1 user@host:

In this command, the user doesn't need the command-line args to ops appended to the command; it needs one of the args to be inserted at the given position, and not appended.

The variable $1 will be empty, because it is not referring to arguments to ops, but to arguments to the system shell created by Ruby, of which there are none.

Feature

ops could perform interpolation on the command of an action, and replace numeric variable references with the corresponding arguments to ops. If any of these replacements were made, ops could not append any of its command-line arguments to the command.

ops should set ENV["1"], ENV["2"], etc. and let the shell handle $*. That way, $@ also works, and any number of other shell features that depend on the arguments given.

E.g.:

actions:
  was_here:
    command: echo "$1 was here"
$ ops was_here nick
nick was here
$ ops was_here nick some_other_guy
nick was here

Note that the second time was_here was executed, two arguments were given to ops. However, since the second argument ($2) was not referenced in the command string, it was not included in the call to echo.

The feature could be implemented in such a way that unused arguments are appended to the command, but a use case for this isn't clear at this point. Commands that take variable numbers of arguments can use $* (see below) to append arguments not referenced directly to the command.

The feature should be implemented such that the shell handles $* (see Feature above).

$0

ops should set $0 to the name of the action. In the above example were_here, $0 would be set to were_here.

This could also be ops, but it seems counterintuitive to run ops one two and have $0 be ops and $1 be two.

Warn if ssh key has no passphrase, even if file exists

If an SSH key already exists on disk, ops will not print the "No SSH passphrase set for key" warning.

For security, this would be a good idea, so users who just run ops up a second time and see the warning disappear don't think they've fixed the issue.

Print available builtins + actions when run with no args

$ ops
Available commands:

- up: installs dependencies and starts services on which this project depends
- down: ...
- start: Starts the container
- stop: ...

Add a description field to Action so the user can define a description, which ops will print when run with no args. If description is not defined, fall back to printing the command.

Add builtin to diff config and secrets between environments

Trying to figure out what I need to add to config/dev based on changes to config/production is annoying. It would be nice to be able to:

ops envdiff production

and see the diff of all config and secrets (maybe secrets have values masked, although that shouldn't be necessary since people should be encrypting real secrets) between the current environment and production.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.