nickthecook / ops Goto Github PK
View Code? Open in Web Editor NEWThe operations team for your project.
License: GNU General Public License v3.0
The operations team for your project.
License: GNU General Public License v3.0
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.
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:
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
.
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"
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.
There are a couple of ways to implement this, at a high level:
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
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.
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.
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.
Should print the version of ops
.
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.
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.
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.
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.
action
from a custom
dependencyThis 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:
load_secrets
option highly visible to users adding or modifying dependenciescustom
dependenciesUse 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.
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:
ops down
would stop the service (right now there's no way to write a custom command to be run on ops down
)which brew
or uname | something
, and could support multiple variants of linux service management (e.g. service
, systemd
, etc.)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:
{}
, []
)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'}
.
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.
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.
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)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.
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
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.
ops
tries to meet apt
dependencies if the uname
is Linux
. Therefore, on Alpine, ops
tries to meet apt
dependencies.
Make apt
like apk
: it should only try to meet apt
dependencies if the apt
command is available.
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
ORoptions.apt.sudo == false
For debugging, it would be handy to be able to see what command is run by an action, with all the variables resolved.
Something like ops -p some_action
, or ops --pretend some_action
.
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.
Consider a project in which there is:
ops.yml
, that can be run on a development machine or on deployed infrastructureIt'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:
"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?
It just hangs.
Create a VM in some cloud computing platform on which to run e2e tests.
Benefits:
apt
outside unit testsservice
if it's implementedbackground
if it's implementedrequire 'english'
vs. require 'English'
)ops
has some built-in templates (e.g. ruby
, terraform
). If a user wants a different template, they have to either:
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.
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:
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
.
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.
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.
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.
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...?
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
.
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:
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.
ops
run start
in a background process, or dictate that the ops start
action must start a service and return?ops
use an action other than start
, so that the action used to start projects does not conflict with a commonly-used action name?ops
use a builtin to handle this, e.g. ops service start
that will run ops start
in the background?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?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.
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.
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:
ops
looks for secrets files, and whether it finds themops
looks for app config files, and whether is finds themRight 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...
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
.
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
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:
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
.
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
.
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'.
$
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.
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.
When an action runs any command, the output has an extra space within the single quotes that shouldn't be there.
Running 'command ' from ops.yml...
Running 'command' from ops.yml...
Line 56 in 95d43fc
Whatever #{action}
returns probably has an extra space.
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
.
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)
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:
ejson decrypt
if it's an ejson
file)ssh-add keys/...
, pasting the copied passphraseI 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:
ops up
doesn't hang waiting for inputssh-add
, which seems determined to only take input from the userops
builtin that adds the ssh keyops 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
.
ssh-key
dependencyCheck 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
expect
script inline in ops.yml
Eww. But it would work.
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 .
Like #18, but allowing multiple commands to be specified as a typical YAML collection, to be executed in order / from the top down.
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.
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.
It would be nice to have docs for all builtins. E.g.:
apt.use_sudo
, that aren't documented at the momentPossibly 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.
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
@
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]
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.
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.
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 =
.
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)
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.
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:
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
rspec
spec; maybe there's a better way["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.
ops
will append any command-line arguments to the end of the command when executing an action. For example, with this config:
actions:
say_it:
command: echo
and this command:
ops say_it hi there
the output will be:
hi there
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.
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
.
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.
$ 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
.
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
.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.