Compare commits

...

25 Commits
v1.0.2 ... main

Author SHA1 Message Date
Jeff Wong 332921814b
Version bump (#34)
* version bump for 1.3.0 release

* update changelog
2025-07-08 17:19:03 -07:00
Jeff Wong f91de7519a
FEATURE: add a --params option (#33)
* FEATURE: add a --params option

Add a --params option that dynamically adds or modifies params of a pups run

usage:

`pups --params foo=bar,baz=other config.yml`

This would dynamically set or override any params set in config.yml for the run.

DEV: linting, and rubocop
update ruby and checkout actions
2025-07-08 08:07:10 -07:00
Jeff Wong 078edb6407
DEV: fix readme (#31)
tags and skip tags should go under --ignore details
2023-10-24 09:22:05 -07:00
Jeff Wong 5436aec99e
FIX: skip tags (#30)
fix skip-tags options typo
2023-10-23 12:27:42 -07:00
Jeff Wong 1276ccd44d
FIX: github actions, add auto gem publish (#29)
* FIX: github actions, add auto gem publish

adds auto publish to ruby gems on push, update auto push branch to main.

* DEV: move linting to CI workflows, rename workflow from test to CI

* DEV: update readme copy
2023-10-23 08:15:34 -07:00
Jeff Wong d51da1fc1d
FEATURE: add --tags and --skip-tags options (#28)
* FEATURE: add --tags and --skip-tags options

Allow config manifests to be tagged, so a pups run can apply a subset of run commands.

Update to ruby 3.2.
Lots of linting. Added rubocop lint exception for Eval.
Fixing test imports, update MiniTest::Test -> Minitest::Test.
2023-10-22 17:42:24 -07:00
Michael Fitz-Payne e0ff889553 1.1.1: bump new version to include bugfix. 2021-06-17 11:16:21 +10:00
Michael Fitz-Payne 3f02d19ad1
FIX: Remove quotes from escaped strings. (#27)
When strings have been escaped and contain a space, we don't want to
quote them because the escape character is then treated as a literal.

For example:
```
$ export var1="hello there"
$ export var2="hello\ there"
$ export var3=hello\ there
$ printf "$var1\n$var2\n$var3\n"
hello there
hello\ there
hello there
```
2021-06-17 10:43:44 +10:00
Michael Fitz-Payne 9a9514810e 1.1.0: bump new version 2021-06-16 13:29:00 +10:00
Michael Fitz-Payne 193242003d
FEATURE: Support generating docker cli arguments. (#22)
The --gen-docker-env-args argument makes pups process any template
environment variables and generate the command line arguments suitable
for the docker run command. The intention is to expand support for
configuring container runtime into pups such that configuration
templates can be more generally useful.

Bash special characters are safely escaped. This prevents issues where
variables contain special characters that are parsed by the calling shell.
A script which uses the pups output may have unexpected side effects
without escaping the special characters.

Note that only `env`, `label`, and `volume` config variables are escaped
as these are the most likely to contain special chacters. No attempt is
made to validate config variables against the allowed characters by
docker itself.

Other changes:
- Change some exit calls to return to prevent the tests prematurely
exiting.
2021-06-15 16:14:21 +10:00
Michael Fitz-Payne ebe1069395 Add --ignore option for specific config elements. (#26)
There are use cases where we may want pups to ignore particular
configuration elements at runtime. For example, we may want to skip over
hooks in certain circumstances or define environment variables via the
process at runtime that are already defined in a template.

The follow example demonstrates the usage (note the last log line):

```
$ cat /tmp/test.yml
env:
  MY_VAR: a_word
run:
  - exec: 'echo repeating $MY_VAR'

$ bin/pups --ignore env /tmp/test.yml
I, [2021-06-09T12:03:46.864770 #30369]  INFO -- : Reading from /tmp/test.yml
I, [2021-06-09T12:03:46.865009 #30369]  INFO -- : > echo repeating $MY_VAR
I, [2021-06-09T12:03:46.865824 #30369]  INFO -- : repeating

$ bin/pups /tmp/test.yml
I, [2021-06-09T12:03:50.694739 #30380]  INFO -- : Reading from /tmp/test.yml
I, [2021-06-09T12:03:50.694980 #30380]  INFO -- : > echo repeating $MY_VAR
I, [2021-06-09T12:03:50.695730 #30380]  INFO -- : repeating a_word
```

This will become more useful once the docker run argument generation
functionality is implemented. For example, options like `expose` may
want to be ignored if pups is being used to generate runtime arguments
for more the one container on the same machine (as published port numbers
cannot overlap between containers). Without the `--ignore` lever, all
runtime arguments will be produced all the time which limits use cases.
2021-06-10 09:02:49 +10:00
Michael Fitz-Payne 44198d7704
Add --quiet option to silence logs. (#25)
This prevents any output from pups being printed. Exceptions will still
be output when they occur.

Slightly restructured the cli class so that the options are able to be
parsed separately to the run method so we can print the usage message in
the event of invalid input.
2021-06-09 10:12:18 +10:00
Michael Fitz-Payne 2069d66f39
Add lint step to CI, fix all lint issues. (#24)
The fixes were performend rubocop -A functionality for autocorrecting.
The linting is enforced according to the rubocop-discourse rules.
2021-06-03 12:22:29 +10:00
Michael Fitz-Payne a5c7d9c9c2
Add CI step for running tests. (#23) 2021-06-03 10:58:08 +10:00
Michael Fitz-Payne 17f04ecde5
Add support for templating environment variables. (#20)
Templated variables can be used to parameterise container creation. This
will ensure any ENV variables are templated during config initialisation
prior to any use.

There are situations where we want to provide and config template at
runtime rather than ahead-of-time. This adds support for specifying such
a variable via the process environment, prefixed with
env_template_<name>=value.
2021-05-20 13:09:39 +10:00
Michael Fitz-Payne f9af90d645
cli: update option parsing to OptionParser. (#21)
Change to proper argument handling in preparation of adding
new command-line arguments to the pups CLI.

Tests added arguments parsing and also for when a file is
specified (not stdin).
2021-05-20 08:46:21 +10:00
Sam Saffron b2e71c594d
update summary / description 2021-04-09 09:27:53 +10:00
Sam Saffron d1db0303d0
cut a new version 2021-04-09 09:25:31 +10:00
David Taylor 05db637f26
Revert "FIX: coerce env to strings" (#19)
This reverts commit 262d7eb4f8 and 8f353a3778

discourse_docker's launcher, and potentially other systems, manipulate environment variables before applying them to a `docker run` command. At the moment, those tools do not necessarily apply those same manipulations to the YAML file passed to pups.

Specifically, in discourse_docker's launcher, the string `{{config}}` in container labels or environment variables is substituted with the filename of the YAML file. Launcher does not perform this replacement on the file passed to pups. Previously, that was not an issue, because pups didn't use the `env` or `labels` from the file. Now, it causes pups to run its setup using incorrect environment variables.

Reverting this until:
  - We update discourse_docker's launcher so that it performs the replacement on the YAML file passed to pups
  - We come up with a way to avoid breaking old discourse_docker installs (right now they use the latest `master` version of discourse/pups on every build)
2021-04-08 13:42:52 +01:00
Sam Saffron 262d7eb4f8
FIX: coerce env to strings
env only ever accepts strings
2021-04-08 18:06:09 +10:00
Sam dadedef18e
Merge pull request #18 from discourse/add-env-support
Add 'env' property support to configuration.
2021-04-08 16:59:47 +10:00
Sam 936c3f5da7
Merge pull request #17 from discourse/update-test-raketask
Fix for running tests.
2021-04-08 16:59:00 +10:00
Michael Fitz-Payne 8f353a3778 Add 'env' property support to configuration.
Variables specified under the `env` property in the root of the yaml
configuration file will now be set in the pups environment during the
initialization process. Any existing environment variables are
overwritten by matching variables specified in the config `env` property.

The integration test was updated to exercise the params and env
codepath as this covers the new functionality.
2021-04-08 16:32:00 +10:00
Michael Fitz-Payne 7db5398b78 Fix for running tests.
Update Rakefile to format specified in rake docs, ensure required
minitest class is included.
2021-04-08 16:07:53 +10:00
Andrew Schleifer 4cfff79a83 feature: new `file` parameter to chown 2019-07-31 09:37:57 +08:00
28 changed files with 1636 additions and 554 deletions

62
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: CI
on:
push:
branches:
- main
pull_request: {}
jobs:
lint:
name: "pups lint"
runs-on: ${{ matrix.os }}
timeout-minutes: 5
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
ruby: ["3.3"]
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: bundle exec rubocop
test:
name: "pups tests"
runs-on: ${{ matrix.os }}
timeout-minutes: 5
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
ruby: ["3.3"]
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run minitest
run: |
rake test
publish:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Release Gem
uses: discourse/publish-rubygems-action@v2
env:
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
GIT_EMAIL: team@discourse.org
GIT_NAME: discoursebot

2
.rubocop.yml Normal file
View File

@ -0,0 +1,2 @@
inherit_gem:
rubocop-discourse: default.yml

11
CHANGELOG Normal file
View File

@ -0,0 +1,11 @@
1.3.0 - 07-08-2025
- Add --params option
1.2.0 - 10-22-2023
- Add --tags and --skip-tags options
1.0.3 - 09-04-2021
- Started changelog - release to rubygems

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
source 'https://rubygems.org'
# Specify your gem's dependencies in pups.gemspec

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
guard :minitest do
# with Minitest::Unit
watch(%r{^test/(.*)\/?(.*)_test\.rb$})
watch(%r{^test/(.*)/?(.*)_test\.rb$})
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[2]}_test.rb" }
watch(%r{^test/test_helper\.rb$}) { 'test' }
end

View File

@ -20,6 +20,21 @@ Or install it yourself as:
pups is a small library that allows you to automate the process of creating Unix images.
```
Usage: pups [options] [FILE|--stdin]
--stdin Read input from stdin.
--quiet Don't print any logs.
--ignore <elements> Ignore specific configuration elements, multiple elements can be provided (comma-delimited).
Useful if you want to skip over config in a pups execution.
e.g. `--ignore env,params`.
--tags <elements> Only run tagged commands.
--skip-tags <elements> Run all but listed tagged commands.
--gen-docker-run-args Output arguments from the pups configuration for input into a docker run command. All other pups config is ignored.
-h, --help
```
pups requires input either via a stdin stream or a filename. The entire input is parsed prior to any templating or command execution.
Example:
```
@ -35,6 +50,60 @@ Running: `pups somefile.yaml` will execute the shell script resulting in a file
### Features
#### Filtering run commands by tags
The `--tags` and `--skip-tags` argument allows pups to target a subset of commands listed in the somefile.yaml. To use this, you may tag your commands in the runblock. `--tags` will only run commands when commands have a matching tag. `--skip-tags` will skip when commands have a matching tag.
Note, hooks from tagged commands will be present or absent depending on if the tag is filtered out or not as well. A command filtered out by targeting tag will also filter out the command's `before_` and `after_` hooks.
Example:
```
# somefile.yaml
run:
- exec:
cmd: /bin/bash -c 'echo hello >> hello'
tag: sometag
- exec:
cmd: /bin/bash -c 'echo hi >> hello'
tag: anothertag
- exec:
cmd: /bin/bash -c 'echo goodbye >> hello'
tag: thirdtag
```
Running: `pups --tags="sometag,anothertag" somefile.yaml` will not run the echo goodbye statement.
Running: `pups --skip-tags="sometag,anothertag" somefile.yaml` will ONLY run the echo goodbye statement.
#### Parameter overriding
The `--params` argument allows pups to dynamically override params set within a configuration for the single pups run.
Note, it is expected to be of the form `key=value`. If it is malformed, a warning will be thrown.
Example:
```
# somefile.yaml
params:
param1: false_prophet
param2: also overridden
run:
- exec:
cmd: /bin/bash -c 'echo $param1 $param2 >> hello'
```
Running `pups --params="param1=true_value,param2=other_true_value" somefile.yaml` will overwrite param1 and param2 with true_value and other_true_value respectively
#### Docker run argument generation
The `--gen-docker-run-args` argument is used to make pups output arguments be in the format of `docker run <arguments output>`. Specifically, pups
will take any `env`, `volume`, `labels`, `links`, and `expose` configuration, and coerce that into the format expected by `docker run`. This can be useful
when pups is being used to configure an image (e.g. by executing a series of commands) that is then going to be run as a container. That way, the runtime and image
configuration can be specified within the same yaml files.
#### Environment Variables
By default, pups automatically imports your environment variables and includes them as params.
@ -149,15 +218,31 @@ Will merge the yaml file with the inline contents.
#### A common environment
This is implemented in discourse_docker's launcher, not in pups - therefore it does not work in standalone pups.
Environment variables can be specified under the `env` key, which will be included in the environment for the template.
```
env:
MY_ENV: 1
MY_ENV: "a couple of words"
run:
- exec: echo $MY_ENV > tmpfile
```
All executions will get this environment set up
`tmpfile` will contain `a couple of words`.
You can also specify variables to be templated within the environment, such as:
```
env:
greeting: "hello, {{location}}!"
env_template:
location: world
```
In this example, the `greeting` environment variable will be set to `hello, world!` during initialisation as the `{{location}}` variable will be templated as `world`.
Pups will also look in the environment itself at runtime for template variables, prefixed with `env_template_<variable name>`.
Note that strings should be quoted to prevent YAML from parsing the `{ }` characters.
All commands executed will inherit the environment once parsing and variable interpolation has been completed.
## Contributing

View File

@ -1,6 +1,12 @@
require "bundler/gem_tasks"
require "rake/testtask"
# frozen_string_literal: true
require 'bundler/gem_tasks'
require 'rake/testtask'
Rake::TestTask.new do |t|
t.pattern = "test/*_test.rb"
t.libs << 'test'
t.libs << 'lib'
t.test_files = FileList['test/*_test.rb']
end
task default: :test

View File

@ -1,9 +1,9 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
$LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require "pups"
require "pups/cli"
require 'pups'
require 'pups/cli'
Pups::Cli.run(ARGV)

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require "logger"
require "yaml"
@ -8,7 +10,7 @@ require "pups/exec_command"
require "pups/merge_command"
require "pups/replace_command"
require "pups/file_command"
require "pups/docker"
require "pups/runit"
module Pups
@ -18,10 +20,16 @@ module Pups
def self.log
# at the moment docker likes this
@logger ||= Logger.new(STDERR)
@logger ||= Logger.new($stderr)
end
def self.log=(logger)
@logger = logger
end
def self.silence
@logger.close if @logger
@logger = Logger.new(File.open(File::NULL, "w"))
end
end

View File

@ -1,35 +1,101 @@
class Pups::Cli
# frozen_string_literal: true
def self.usage
puts "Usage: pups FILE or pups --stdin"
exit 1
end
def self.run(args)
if args.length != 1
usage
end
require "optparse"
Pups.log.info("Loading #{args[0]}")
if args[0] == "--stdin"
conf = STDIN.readlines.join
split = conf.split("_FILE_SEPERATOR_")
conf = nil
split.each do |data|
current = YAML.load(data.strip)
if conf
conf = Pups::MergeCommand.deep_merge(conf, current, :merge_arrays)
else
conf = current
module Pups
class Cli
def self.opts
OptionParser.new do |opts|
opts.banner = "Usage: pups [FILE|--stdin]"
opts.on("--stdin", "Read input from stdin.")
opts.on("--quiet", "Don't print any logs.")
opts.on(
"--ignore <element(s)>",
Array,
"Ignore these template configuration elements, multiple elements can be provided (comma-delimited)."
)
opts.on(
"--gen-docker-run-args",
"Output arguments from the pups configuration for input into a docker run command. All other pups config is ignored."
)
opts.on("--tags <tag(s)>", Array, "Only run tagged commands.")
opts.on(
"--skip-tags <tag(s)>",
Array,
"Run all but listed tagged commands."
)
opts.on(
"--params <param(s)>",
Array,
"Replace params in the config with params listed."
)
opts.on("-h", "--help") do
puts opts
exit
end
end
config = Pups::Config.new(conf)
else
config = Pups::Config.load_file(args[0])
end
config.run
ensure
Pups::ExecCommand.terminate_async
def self.parse_args(args)
options = {}
opts.parse!(args, into: options)
options
end
def self.run(args)
options = parse_args(args)
input_file = options[:stdin] ? "stdin" : args.last
unless input_file
puts opts.parse!(%w[--help])
exit
end
Pups.silence if options[:quiet]
Pups.log.info("Reading from #{input_file}")
if options[:stdin]
conf = $stdin.readlines.join
split = conf.split("_FILE_SEPERATOR_")
conf = nil
split.each do |data|
current = YAML.safe_load(data.strip)
conf =
if conf
Pups::MergeCommand.deep_merge(conf, current, :merge_arrays)
else
current
end
end
config =
Pups::Config.new(
conf,
options[:ignore],
tags: options[:tags],
skip_tags: options[:"skip-tags"],
extra_params: options[:params]
)
else
config =
Pups::Config.load_file(
input_file,
options[:ignore],
tags: options[:tags],
skip_tags: options[:"skip-tags"],
extra_params: options[:params]
)
end
if options[:"gen-docker-run-args"]
print config.generate_docker_run_arguments
return
end
config.run
ensure
Pups::ExecCommand.terminate_async
end
end
end

View File

@ -1,17 +1,22 @@
class Pups::Command
# frozen_string_literal: true
def self.run(command,params)
case command
when String then self.from_str(command,params).run
when Hash then self.from_hash(command,params).run
module Pups
class Command
def self.run(command, params)
case command
when String
from_str(command, params).run
when Hash
from_hash(command, params).run
end
end
def self.interpolate_params(cmd, params)
Pups::Config.interpolate_params(cmd, params)
end
def interpolate_params(cmd)
Pups::Command.interpolate_params(cmd, @params)
end
end
def self.interpolate_params(cmd,params)
Pups::Config.interpolate_params(cmd,params)
end
def interpolate_params(cmd)
Pups::Command.interpolate_params(cmd,@params)
end
end

View File

@ -1,128 +1,256 @@
class Pups::Config
# frozen_string_literal: true
attr_reader :config, :params
module Pups
class Config
attr_reader :config, :params
def self.load_file(config_file)
begin
new YAML.load_file(config_file)
def initialize(
config,
ignored = nil,
tags: nil,
skip_tags: nil,
extra_params: nil
)
@config = config
# remove any ignored config elements prior to any more processing
ignored&.each { |e| @config.delete(e) }
filter_tags(include_tags: tags, exclude_tags: skip_tags)
# set some defaults to prevent checks in various functions
%w[env_template env labels params].each do |key|
@config[key] = {} unless @config.has_key?(key)
end
# Order here is important.
Pups::Config.combine_template_and_process_env(@config, ENV)
Pups::Config.prepare_env_template_vars(@config["env_template"], ENV)
# Templating is supported in env and label variables.
Pups::Config.transform_config_with_templated_vars(
@config["env_template"],
ENV
)
Pups::Config.transform_config_with_templated_vars(
@config["env_template"],
@config["env"]
)
Pups::Config.transform_config_with_templated_vars(
@config["env_template"],
@config["labels"]
)
@params = @config["params"]
if extra_params
extra_params.each do |val|
key_val = val.split("=", 2)
if key_val.length == 2
@params[key_val[0]] = key_val[1]
else
warn "Malformed param #{val}. Expected param to be of the form `key=value`"
end
end
end
ENV.each { |k, v| @params["$ENV_#{k}"] = v }
inject_hooks
end
def self.load_file(
config_file,
ignored = nil,
tags: nil,
skip_tags: nil,
extra_params: nil
)
Config.new(
YAML.load_file(config_file),
ignored,
tags: tags,
skip_tags: skip_tags,
extra_params: extra_params
)
rescue Exception
STDERR.puts "Failed to parse #{config_file}"
STDERR.puts "This is probably a formatting error in #{config_file}"
STDERR.puts "Cannot continue. Edit #{config_file} and try again."
warn "Failed to parse #{config_file}"
warn "This is probably a formatting error in #{config_file}"
warn "Cannot continue. Edit #{config_file} and try again."
raise
end
end
def self.load_config(config)
new YAML.load(config)
end
def initialize(config)
@config = config
validate!(@config)
@params = @config["params"]
@params ||= {}
ENV.each do |k,v|
@params["$ENV_#{k}"] = v
def self.load_config(
config,
ignored = nil,
tags: nil,
skip_tags: nil,
extra_params: nil
)
Config.new(
YAML.safe_load(config),
ignored,
tags: tags,
skip_tags: skip_tags,
extra_params: extra_params
)
end
inject_hooks
end
def validate!(conf)
# raise proper errors if nodes are missing etc
end
def inject_hooks
return unless hooks = @config["hooks"]
run = @config["run"]
positions = {}
run.each do |row|
if Hash === row
command = row.first
if Hash === command[1]
hook = command[1]["hook"]
positions[hook] = row if hook
def self.prepare_env_template_vars(env_template, env)
# Merge env_template variables from env and templates.
env.each do |k, v|
if k.include?("env_template_")
key = k.gsub("env_template_", "")
env_template[key] = v.to_s
end
end
end
hooks.each do |full, list|
offset = nil
name = nil
if full =~ /^after_/
name = full[6..-1]
offset = 1
end
if full =~ /^before_/
name = full[7..-1]
offset = 0
end
index = run.index(positions[name])
if index && index >= 0
run.insert(index + offset, *list)
else
Pups.log.info "Skipped missing #{full} hook"
end
end
end
def run
run_commands
rescue => e
exit_code = 1
if Pups::ExecError === e
exit_code = e.exit_code
end
unless exit_code == 77
puts
puts
puts "FAILED"
puts "-" * 20
puts "#{e.class}: #{e}"
puts "Location of failure: #{e.backtrace[0]}"
if @last_command
puts "#{@last_command[:command]} failed with the params #{@last_command[:params].inspect}"
def self.transform_config_with_templated_vars(env_template, to_transform)
# Transform any templated variables prior to copying to params.
# This has no effect if no env_template was provided.
env_template.each do |k, v|
to_transform.each do |key, val|
if val.to_s.include?("{{#{k}}}")
to_transform[key] = val.gsub("{{#{k}}}", v.to_s)
end
end
end
end
exit exit_code
end
def run_commands
@config["run"].each do |item|
item.each do |k,v|
type = case k
when "exec" then Pups::ExecCommand
when "merge" then Pups::MergeCommand
when "replace" then Pups::ReplaceCommand
when "file" then Pups::FileCommand
else raise SyntaxError.new("Invalid run command #{k}")
end
def self.combine_template_and_process_env(config, env)
# Merge all template env variables and process env variables, so that env
# variables can be provided both by configuration and runtime variables.
config["env"].each { |k, v| env[k] = v.to_s }
end
@last_command = { command: k, params: v }
type.run(v, @params)
# Filter run commands by tag: by default, keep all commands that contain tags.
# If skip_tags argument is true, keep all commands that DO NOT contain tags.
def filter_tags(include_tags: nil, exclude_tags: nil)
if include_tags
@config["run"] = @config["run"].select do |row|
keep = false
command = row.first
if command[1].is_a?(Hash)
tag = command[1]["tag"]
keep = include_tags.include?(tag)
end
keep
end
end
if exclude_tags
@config["run"] = @config["run"].select do |row|
keep = true
command = row.first
if command[1].is_a?(Hash)
tag = command[1]["tag"]
keep = !exclude_tags.include?(tag)
end
keep
end
end
end
end
def interpolate_params(cmd)
self.class.interpolate_params(cmd,@params)
end
def inject_hooks
return unless hooks = @config["hooks"]
def self.interpolate_params(cmd, params)
return unless cmd
processed = cmd.dup
params.each do |k,v|
processed.gsub!("$#{k}", v.to_s)
run = @config["run"]
positions = {}
run.each do |row|
next unless row.is_a?(Hash)
command = row.first
if command[1].is_a?(Hash)
hook = command[1]["hook"]
positions[hook] = row if hook
end
end
hooks.each do |full, list|
offset = nil
name = nil
if full =~ /^after_/
name = full[6..-1]
offset = 1
end
if full =~ /^before_/
name = full[7..-1]
offset = 0
end
index = run.index(positions[name])
if index && index >= 0
run.insert(index + offset, *list)
else
Pups.log.info "Skipped missing #{full} hook"
end
end
end
processed
end
def generate_docker_run_arguments
output = []
output << Pups::Docker.generate_env_arguments(config["env"])
output << Pups::Docker.generate_link_arguments(config["links"])
output << Pups::Docker.generate_expose_arguments(config["expose"])
output << Pups::Docker.generate_volume_arguments(config["volumes"])
output << Pups::Docker.generate_label_arguments(config["labels"])
output.sort!.join(" ").strip
end
def run
run_commands
rescue StandardError => e
exit_code = 1
exit_code = e.exit_code if e.is_a?(Pups::ExecError)
unless exit_code == 77
puts
puts
puts "FAILED"
puts "-" * 20
puts "#{e.class}: #{e}"
puts "Location of failure: #{e.backtrace[0]}"
if @last_command
puts "#{@last_command[:command]} failed with the params #{@last_command[:params].inspect}"
end
end
exit exit_code
end
def run_commands
@config["run"]&.each do |item|
item.each do |k, v|
type =
case k
when "exec"
Pups::ExecCommand
when "merge"
Pups::MergeCommand
when "replace"
Pups::ReplaceCommand
when "file"
Pups::FileCommand
else
raise SyntaxError, "Invalid run command #{k}"
end
@last_command = { command: k, params: v }
type.run(v, @params)
end
end
end
def interpolate_params(cmd)
self.class.interpolate_params(cmd, @params)
end
def self.interpolate_params(cmd, params)
return unless cmd
processed = cmd.dup
params.each { |k, v| processed.gsub!("$#{k}", v.to_s) }
processed
end
end
end

66
lib/pups/docker.rb Normal file
View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
require "shellwords"
class Pups::Docker
class << self
def generate_env_arguments(config)
output = []
config&.each do |k, v|
if !v.to_s.empty?
output << "--env #{k}=#{escape_user_string_literal(v)}"
end
end
normalize_output(output)
end
def generate_link_arguments(config)
output = []
config&.each do |c|
output << "--link #{c["link"]["name"]}:#{c["link"]["alias"]}"
end
normalize_output(output)
end
def generate_expose_arguments(config)
output = []
config&.each do |c|
if c.to_s.include?(":")
output << "--publish #{c}"
else
output << "--expose #{c}"
end
end
normalize_output(output)
end
def generate_volume_arguments(config)
output = []
config&.each do |c|
output << "--volume #{c["volume"]["host"]}:#{c["volume"]["guest"]}"
end
normalize_output(output)
end
def generate_label_arguments(config)
output = []
config&.each do |k, v|
output << "--label #{k}=#{escape_user_string_literal(v)}"
end
normalize_output(output)
end
private
def escape_user_string_literal(str)
# We need to escape the following strings as they are more likely to contain
# special characters than any of the other config variables on a Linux system:
# - the value side of an environment variable
# - the value side of a label.
Shellwords.escape(str)
end
def normalize_output(output)
output.empty? ? "" : output.join(" ")
end
end
end

View File

@ -1,124 +1,142 @@
require 'timeout'
# frozen_string_literal: true
class Pups::ExecCommand < Pups::Command
attr_reader :commands, :cd
attr_accessor :background, :raise_on_fail, :stdin, :stop_signal
require "timeout"
require "English"
def self.terminate_async(opts={})
module Pups
class ExecCommand < Pups::Command
attr_reader :commands, :cd
attr_accessor :background, :raise_on_fail, :stdin, :stop_signal
return unless defined? @@asyncs
def self.terminate_async(opts = {})
return unless defined?(@@asyncs)
Pups.log.info("Terminating async processes")
Pups.log.info("Terminating async processes")
@@asyncs.each do |async|
Pups.log.info("Sending #{async[:stop_signal]} to #{async[:command]} pid: #{async[:pid]}")
Process.kill(async[:stop_signal],async[:pid]) rescue nil
@@asyncs.each do |async|
Pups.log.info(
"Sending #{async[:stop_signal]} to #{async[:command]} pid: #{async[:pid]}"
)
begin
Process.kill(async[:stop_signal], async[:pid])
rescue StandardError
nil
end
end
@@asyncs
.map do |async|
Thread.new do
Timeout.timeout(opts[:wait] || 10) do
Process.wait(async[:pid])
rescue StandardError
nil
end
rescue Timeout::Error
Pups.log.info(
"#{async[:command]} pid:#{async[:pid]} did not terminate cleanly, forcing termination!"
)
begin
Process.kill("KILL", async[:pid])
Process.wait(async[:pid])
rescue Errno::ESRCH
rescue Errno::ECHILD
end
end
end
.each(&:join)
end
@@asyncs.map do |async|
Thread.new do
begin
Timeout.timeout(opts[:wait] || 10) do
Process.wait(async[:pid]) rescue nil
end
rescue Timeout::Error
Pups.log.info("#{async[:command]} pid:#{async[:pid]} did not terminate cleanly, forcing termination!")
def self.from_hash(hash, params)
cmd = new(params, hash["cd"])
case c = hash["cmd"]
when String
cmd.add(c)
when Array
c.each { |i| cmd.add(i) }
end
cmd.background = hash["background"]
cmd.stop_signal = hash["stop_signal"] || "TERM"
cmd.raise_on_fail = hash["raise_on_fail"] if hash.key? "raise_on_fail"
cmd.stdin = interpolate_params(hash["stdin"], params)
cmd
end
def self.from_str(str, params)
cmd = new(params)
cmd.add(str)
cmd
end
def initialize(params, cd = nil)
@commands = []
@params = params
@cd = interpolate_params(cd)
@raise_on_fail = true
end
def add(cmd)
@commands << process_params(cmd)
end
def run
commands.each do |command|
Pups.log.info("> #{command}")
pid = spawn(command)
Pups.log.info(@result.readlines.join("\n")) if @result
end
rescue StandardError
raise if @raise_on_fail
end
def spawn(command)
if background
pid = Process.spawn(command)
(@@asyncs ||= []) << {
pid: pid,
command: command,
stop_signal: (stop_signal || "TERM")
}
Thread.new do
begin
Process.kill("KILL",async[:pid])
Process.wait(async[:pid])
rescue Errno::ESRCH
Process.wait(pid)
rescue Errno::ECHILD
# already exited so skip
end
@@asyncs.delete_if { |async| async[:pid] == pid }
end
return pid
end
IO.popen(command, "w+") do |f|
if stdin
# need a way to get stdout without blocking
Pups.log.info(stdin)
f.write stdin
f.close
else
Pups.log.info(f.readlines.join)
end
end
end.each(&:join)
end
def self.from_hash(hash, params)
cmd = new(params, hash["cd"])
case c = hash["cmd"]
when String then cmd.add(c)
when Array then c.each{|i| cmd.add(i)}
end
cmd.background = hash["background"]
cmd.stop_signal = hash["stop_signal"] || "TERM"
cmd.raise_on_fail = hash["raise_on_fail"] if hash.key? "raise_on_fail"
cmd.stdin = interpolate_params(hash["stdin"], params)
cmd
end
def self.from_str(str, params)
cmd = new(params)
cmd.add(str)
cmd
end
def initialize(params, cd = nil)
@commands = []
@params = params
@cd = interpolate_params(cd)
@raise_on_fail = true
end
def add(cmd)
@commands << process_params(cmd)
end
def run
commands.each do |command|
Pups.log.info("> #{command}")
pid = spawn(command)
Pups.log.info(@result.readlines.join("\n")) if @result
pid
end
rescue
raise if @raise_on_fail
end
def spawn(command)
if background
pid = Process.spawn(command)
(@@asyncs ||= []) << {pid: pid, command: command, stop_signal: (stop_signal || "TERM")}
Thread.new do
begin
Process.wait(pid)
rescue Errno::ECHILD
# already exited so skip
end
@@asyncs.delete_if{|async| async[:pid] == pid}
unless $CHILD_STATUS == 0
err =
Pups::ExecError.new(
"#{command} failed with return #{$CHILD_STATUS.inspect}"
)
err.exit_code = $CHILD_STATUS.exitstatus
raise err
end
return pid
nil
end
IO.popen(command, "w+") do |f|
if stdin
# need a way to get stdout without blocking
Pups.log.info(stdin)
f.write stdin
f.close
else
Pups.log.info(f.readlines.join)
end
def process_params(cmd)
processed = interpolate_params(cmd)
@cd ? "cd #{cd} && #{processed}" : processed
end
unless $? == 0
err = Pups::ExecError.new("#{command} failed with return #{$?.inspect}")
err.exit_code = $?.exitstatus
raise err
end
nil
end
def process_params(cmd)
processed = interpolate_params(cmd)
@cd ? "cd #{cd} && #{processed}" : processed
end
end

View File

@ -1,37 +1,33 @@
class Pups::FileCommand < Pups::Command
attr_accessor :path, :contents, :params, :type, :chmod
# frozen_string_literal: true
def self.from_hash(hash, params)
command = new
command.path = hash["path"]
command.contents = hash["contents"]
command.chmod = hash["chmod"]
command.params = params
module Pups
class FileCommand < Pups::Command
attr_accessor :path, :contents, :params, :type, :chmod, :chown
command
end
def self.from_hash(hash, params)
command = new
command.path = hash["path"]
command.contents = hash["contents"]
command.chmod = hash["chmod"]
command.chown = hash["chown"]
command.params = params
def initialize
@params = {}
@type = :bash
end
def params=(p)
@params = p
end
def run
path = interpolate_params(@path)
`mkdir -p #{File.dirname(path)}`
File.open(path, "w") do |f|
f.write(interpolate_params(contents))
command
end
if @chmod
`chmod #{@chmod} #{path}`
end
Pups.log.info("File > #{path} chmod: #{@chmod}")
end
def initialize
@params = {}
@type = :bash
end
def run
path = interpolate_params(@path)
`mkdir -p #{File.dirname(path)}`
File.open(path, "w") { |f| f.write(interpolate_params(contents)) }
`chmod #{@chmod} #{path}` if @chmod
`chown #{@chown} #{path}` if @chown
Pups.log.info("File > #{path} chmod: #{@chmod} chown: #{@chown}")
end
end
end

View File

@ -1,48 +1,53 @@
class Pups::MergeCommand < Pups::Command
attr_reader :filename
attr_reader :merge_hash
# frozen_string_literal: true
def self.from_str(command, params)
new(command,params)
end
module Pups
class MergeCommand < Pups::Command
attr_reader :filename, :merge_hash
def self.parse_command(command)
split = command.split(" ")
raise ArgumentError.new("Invalid merge command #{command}") unless split[-1][0] == "$"
def self.from_str(command, params)
new(command, params)
end
[split[0..-2].join(" ") , split[-1][1..-1]]
end
def initialize(command, params)
@params = params
filename, target_param = Pups::MergeCommand.parse_command(command)
@filename = interpolate_params(filename)
@merge_hash = params[target_param]
end
def run
merged = self.class.deep_merge(YAML.load_file(@filename), @merge_hash)
File.open(@filename,"w"){|f| f.write(merged.to_yaml) }
Pups.log.info("Merge: #{@filename} with: \n#{@merge_hash.inspect}")
end
def self.deep_merge(first,second, *args)
args ||= []
merge_arrays = args.include? :merge_arrays
merger = proc { |key, v1, v2|
if Hash === v1 && Hash === v2
v1.merge(v2, &merger)
elsif Array === v1 && Array === v2
merge_arrays ? v1 + v2 : v2
elsif NilClass === v2
v1
else
v2
def self.parse_command(command)
split = command.split(" ")
unless split[-1][0] == "$"
raise ArgumentError, "Invalid merge command #{command}"
end
}
first.merge(second, &merger)
end
[split[0..-2].join(" "), split[-1][1..-1]]
end
def initialize(command, params)
@params = params
filename, target_param = Pups::MergeCommand.parse_command(command)
@filename = interpolate_params(filename)
@merge_hash = params[target_param]
end
def run
merged = self.class.deep_merge(YAML.load_file(@filename), @merge_hash)
File.open(@filename, "w") { |f| f.write(merged.to_yaml) }
Pups.log.info("Merge: #{@filename} with: \n#{@merge_hash.inspect}")
end
def self.deep_merge(first, second, *args)
args ||= []
merge_arrays = args.include? :merge_arrays
merger =
proc do |_key, v1, v2|
if v1.is_a?(Hash) && v2.is_a?(Hash)
v1.merge(v2, &merger)
elsif v1.is_a?(Array) && v2.is_a?(Array)
merge_arrays ? v1 + v2 : v2
elsif v2.is_a?(NilClass)
v1
else
v2
end
end
first.merge(second, &merger)
end
end
end

View File

@ -1,43 +1,45 @@
class Pups::ReplaceCommand < Pups::Command
attr_accessor :text, :from, :to, :filename, :direction, :global
# frozen_string_literal: true
def self.from_hash(hash, params)
replacer = new(params)
replacer.from = guess_replace_type(hash["from"])
replacer.to = guess_replace_type(hash["to"])
replacer.text = File.read(hash["filename"])
replacer.filename = hash["filename"]
replacer.direction = hash["direction"].to_sym if hash["direction"]
replacer.global = hash["global"].to_s == "true"
replacer
end
module Pups
class ReplaceCommand < Pups::Command
attr_accessor :text, :from, :to, :filename, :direction, :global
def self.guess_replace_type(item)
# evaling to get all the regex flags easily
item[0] == "/" ? eval(item) : item
end
def initialize(params)
@params = params
end
def replaced_text
new_to = to
if String === to
new_to = interpolate_params(to)
def self.from_hash(hash, params)
replacer = new(params)
replacer.from = guess_replace_type(hash["from"])
replacer.to = guess_replace_type(hash["to"])
replacer.text = File.read(hash["filename"])
replacer.filename = hash["filename"]
replacer.direction = hash["direction"].to_sym if hash["direction"]
replacer.global = hash["global"].to_s == "true"
replacer
end
if global
text.gsub(from,new_to)
elsif direction == :reverse
index = text.rindex(from)
text[0..index-1] << text[index..-1].sub(from,new_to)
else
text.sub(from,new_to)
end
end
def run
Pups.log.info("Replacing #{from.to_s} with #{to.to_s} in #{filename}")
File.open(filename, "w"){|f| f.write replaced_text }
def self.guess_replace_type(item)
# evaling to get all the regex flags easily
item[0] == "/" ? eval(item) : item # rubocop:disable Security/Eval
end
def initialize(params)
@params = params
end
def replaced_text
new_to = to
new_to = interpolate_params(to) if to.is_a?(String)
if global
text.gsub(from, new_to)
elsif direction == :reverse
index = text.rindex(from)
text[0..index - 1] << text[index..-1].sub(from, new_to)
else
text.sub(from, new_to)
end
end
def run
Pups.log.info("Replacing #{from} with #{to} in #{filename}")
File.open(filename, "w") { |f| f.write replaced_text }
end
end
end

View File

@ -1,40 +1,35 @@
class Pups::Runit
# frozen_string_literal: true
attr_accessor :env, :exec, :cd, :name
module Pups
class Runit
attr_accessor :env, :exec, :cd, :name
def initialize(name)
@name = name
end
def setup
`mkdir -p /etc/service/#{name}`
run = "/etc/service/#{name}/run"
File.open(run, "w") do |f|
f.write(run_script)
def initialize(name)
@name = name
end
`chmod +x #{run}`
end
def run_script
"#!/bin/bash
def setup
`mkdir -p /etc/service/#{name}`
run = "/etc/service/#{name}/run"
File.open(run, "w") { |f| f.write(run_script) }
`chmod +x #{run}`
end
def run_script
"#!/bin/bash
exec 2>&1
#{env_script}
#{cd_script}
#{exec}
"
end
end
def cd_script
"cd #{@cd}" if @cd
end
def cd_script
"cd #{@cd}" if @cd
end
def env_script
if @env
@env.map do |k,v|
"export #{k}=#{v}"
end.join("\n")
def env_script
@env&.map { |k, v| "export #{k}=#{v}" }&.join("\n")
end
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Pups
VERSION = "1.0.2"
VERSION = "1.3.0"
end

View File

@ -1,26 +1,31 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
# frozen_string_literal: true
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) if !$LOAD_PATH.include?(lib)
require 'pups/version'
Gem::Specification.new do |spec|
spec.name = "pups"
spec.name = 'pups'
spec.version = Pups::VERSION
spec.authors = ["Sam Saffron"]
spec.email = ["sam.saffron@gmail.com"]
spec.description = %q{Process orchestrator}
spec.summary = %q{Process orchestrator}
spec.homepage = ""
spec.license = "MIT"
spec.authors = ['Sam Saffron']
spec.email = ['sam.saffron@gmail.com']
spec.description = 'Simple docker image creator'
spec.summary = 'Toolkit for orchestrating a composed docker image'
spec.homepage = ''
spec.license = 'MIT'
spec.files = `git ls-files`.split($/)
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ["lib"]
spec.require_paths = ['lib']
spec.add_development_dependency "bundler", "~> 1.3"
spec.add_development_dependency "rake"
spec.add_development_dependency "minitest"
spec.add_development_dependency "guard"
spec.add_development_dependency "guard-minitest"
spec.add_development_dependency 'bundler'
spec.add_development_dependency 'guard'
spec.add_development_dependency 'guard-minitest'
spec.add_development_dependency 'minitest'
spec.add_development_dependency 'rake'
spec.add_development_dependency 'rubocop'
spec.add_development_dependency 'rubocop-discourse'
spec.add_development_dependency 'rubocop-minitest'
spec.add_development_dependency 'rubocop-rake'
end

192
test/cli_test.rb Normal file
View File

@ -0,0 +1,192 @@
# frozen_string_literal: true
require "test_helper"
require "tempfile"
require "stringio"
module Pups
class CliTest < ::Minitest::Test
def test_cli_option_parsing_stdin
options = Cli.parse_args(["--stdin"])
assert_equal(true, options[:stdin])
end
def test_cli_option_parsing_none
options = Cli.parse_args([])
assert_nil(options[:stdin])
end
def test_cli_read_config_from_file
# for testing output
f = Tempfile.new("test_output")
f.close
# for testing input
cf = Tempfile.new("test_config")
cf.puts <<~YAML
params:
run: #{f.path}
run:
- exec: echo hello world >> #{f.path}
YAML
cf.close
Cli.run([cf.path])
assert_equal("hello world", File.read(f.path).strip)
end
def test_cli_ignore_config_element
# for testing output
f = Tempfile.new("test_output")
f.close
# for testing input
cf = Tempfile.new("test_config")
cf.puts <<~YAML
env:
MY_IGNORED_VAR: a_word
params:
a_param_var: another_word
run:
- exec: echo repeating $MY_IGNORED_VAR and also $a_param_var >> #{f.path}
YAML
cf.close
Cli.run(["--ignore", "env,params", cf.path])
assert_equal("repeating and also", File.read(f.path).strip)
end
def test_cli_gen_docker_run_args_ignores_other_config
# When generating the docker run arguments it should ignore other template configuration
# like 'run' directives.
# for testing output
f = Tempfile.new("test_output")
f.close
# for testing input
cf = Tempfile.new("test_config")
cf.puts <<~YAML
env:
foo: 1
bar: 5
baz: 'hello_{{spam}}'
env_template:
spam: 'eggs'
config: my_app
params:
run: #{f.path}
run:
- exec: echo hello world >> #{f.path}
expose:
- "2222:22"
- "127.0.0.1:20080:80"
- 5555
volumes:
- volume:
host: /var/discourse/shared
guest: /shared
- volume:
host: /bar
guest: /baz
links:
- link:
name: postgres
alias: postgres
- link:
name: foo
alias: bar
labels:
monitor: "true"
app_name: "{{config}}_discourse"
YAML
cf.close
expected = []
expected << "--env foo=1 --env bar=5 --env baz=hello_eggs"
expected << "--publish 2222:22 --publish 127.0.0.1:20080:80 --expose 5555"
expected << "--volume /var/discourse/shared:/shared --volume /bar:/baz"
expected << "--link postgres:postgres --link foo:bar"
expected << "--label monitor=true --label app_name=my_app_discourse"
expected.sort!
assert_equal("", File.read(f.path).strip)
assert_output(expected.join(" ")) do
Cli.run(["--gen-docker-run-args", cf.path])
end
end
def test_cli_tags
# for testing output
f = Tempfile.new("test_output")
f.close
# for testing input
cf = Tempfile.new("test_config")
cf.puts <<~YAML
run:
- exec:
tag: '1'
cmd: echo 1 >> #{f.path}
- exec:
tag: '2'
cmd: echo 2 >> #{f.path}
- exec:
tag: '3'
cmd: echo 3 >> #{f.path}
YAML
cf.close
Cli.run(["--tags", "1,3", cf.path])
assert_equal("1\n3", File.read(f.path).strip)
end
def test_cli_skip_tags
# for testing output
f = Tempfile.new("test_output")
f.close
# for testing input
cf = Tempfile.new("test_config")
cf.puts <<~YAML
run:
- exec:
tag: '1'
cmd: echo 1 >> #{f.path}
- exec:
tag: '2'
cmd: echo 2 >> #{f.path}
- exec:
tag: '3'
cmd: echo 3 >> #{f.path}
YAML
cf.close
Cli.run(["--skip-tags", "1,3", cf.path])
assert_equal("2", File.read(f.path).strip)
end
def test_cli_params
# for testing output
f = Tempfile.new("test_output")
f.close
# for testing input
cf = Tempfile.new("test_config")
cf.puts <<~YAML
params:
one: 0
two: 0
run:
- exec:
cmd: echo $one >> #{f.path}
- exec:
cmd: echo $two >> #{f.path}
YAML
cf.close
Cli.run(["--params", "one=1,two=2", cf.path])
assert_equal("1\n2", File.read(f.path).strip)
end
end
end

View File

@ -1,55 +1,297 @@
require 'test_helper'
require 'tempfile'
# frozen_string_literal: true
require "test_helper"
require "tempfile"
module Pups
class ConfigTest < MiniTest::Test
class ConfigTest < ::Minitest::Test
def test_config_from_env
ENV["HELLO"] = "world"
config = Config.new({})
assert_equal("world", config.params["$ENV_HELLO"])
end
def test_integration
def test_env_param
ENV["FOO"] = "BAR"
config = <<~YAML
env:
BAR: baz
hello: WORLD
one: 1
YAML
config = Config.new(YAML.safe_load(config))
%w[BAR hello one].each { |e| ENV.delete(e) }
assert_equal("BAR", config.params["$ENV_FOO"])
assert_equal("baz", config.params["$ENV_BAR"])
assert_equal("WORLD", config.params["$ENV_hello"])
assert_equal("1", config.params["$ENV_one"])
end
def test_env_with_template
ENV["FOO"] = "BAR"
config = <<~YAML
env:
greeting: "{{hello}}, {{planet}}!"
one: 1
other: "where are we on {{planet}}?"
env_template:
planet: pluto
hello: hola
YAML
config_hash = YAML.safe_load(config)
config = Config.new(config_hash)
%w[greeting one other].each { |e| ENV.delete(e) }
assert_equal("hola, pluto!", config.params["$ENV_greeting"])
assert_equal("1", config.params["$ENV_one"])
assert_equal("BAR", config.params["$ENV_FOO"])
assert_equal("where are we on pluto?", config.params["$ENV_other"])
end
def test_label_with_template
ENV["FOO"] = "BAR"
config = <<~YAML
env:
greeting: "{{hello}}, {{planet}}!"
one: 1
other: "where are we on {{planet}}?"
env_template:
planet: pluto
hello: hola
config: various
labels:
app_name: "{{config}}_discourse"
YAML
config_hash = YAML.load(config)
config = Config.new(config_hash)
%w[greeting one other].each { |e| ENV.delete(e) }
assert_equal("various_discourse", config.config["labels"]["app_name"])
end
def test_env_with_ENV_templated_variable
ENV["env_template_config"] = "my_application"
config = <<~YAML
env:
greeting: "{{hello}}, {{planet}}!"
one: 1
other: "building {{config}}"
env_template:
planet: pluto
hello: hola
YAML
config_hash = YAML.safe_load(config)
config = Config.new(config_hash)
%w[greeting one other].each { |e| ENV.delete(e) }
assert_equal("hola, pluto!", config.params["$ENV_greeting"])
assert_equal("1", config.params["$ENV_one"])
assert_equal("building my_application", config.params["$ENV_other"])
ENV["env_template_config"] = nil
end
def test_integration
f = Tempfile.new("test")
f.close
config = <<YAML
params:
run: #{f.path}
run:
- exec: echo hello world >> #{f.path}
YAML
config = <<~YAML
env:
PLANET: world
params:
run: #{f.path}
greeting: hello
run:
- exec: echo $greeting $PLANET >> #{f.path}
YAML
Config.new(YAML.load(config)).run
Config.new(YAML.safe_load(config)).run
ENV.delete("PLANET")
assert_equal("hello world", File.read(f.path).strip)
ensure
f.unlink
end
def test_hooks
yaml = <<YAML
run:
- exec: 1
- exec:
hook: middle
cmd: 2
- exec: 3
hooks:
after_middle:
- exec: 2.1
before_middle:
- exec: 1.9
YAML
yaml = <<~YAML
run:
- exec: 1
- exec:
hook: middle
cmd: 2
- exec: 3
hooks:
after_middle:
- exec: 2.1
before_middle:
- exec: 1.9
YAML
config = Config.load_config(yaml).config
assert_equal({ "exec" => 1.9 }, config["run"][1])
assert_equal({ "exec" => 2.1 }, config["run"][3])
end
def test_ignored_elements
f = Tempfile.new("test")
f.close
yaml = <<~YAML
env:
PLANET: world
params:
greeting: hello
run:
- exec: 1
- exec:
hook: middle
cmd: 2
- exec: 3
- exec: echo $greeting $PLANET >> #{f.path}
hooks:
after_middle:
- exec: 2.1
before_middle:
- exec: 1.9
YAML
conf = Config.load_config(yaml, %w[hooks params])
config = conf.config
assert_equal({ "exec" => 1 }, config["run"][0])
assert_equal(
{ "exec" => { "hook" => "middle", "cmd" => 2 } },
config["run"][1]
)
assert_equal({ "exec" => 3 }, config["run"][2])
assert_equal(
{ "exec" => "echo $greeting $PLANET >> #{f.path}" },
config["run"][3]
)
# $greet from params will be an empty var as it was ignored
conf.run
ENV.delete("PLANET")
assert_equal("world", File.read(f.path).strip)
end
def test_generate_docker_run_arguments
yaml = <<~YAML
env:
foo: 1
bar: 2
baz: 'hello_{{spam}}'
env_template:
spam: 'eggs'
config: my_app
expose:
- "2222:22"
- "127.0.0.1:20080:80"
- 5555
volumes:
- volume:
host: /var/discourse/shared
guest: /shared
- volume:
host: /bar
guest: /baz
links:
- link:
name: postgres
alias: postgres
- link:
name: foo
alias: bar
labels:
monitor: "true"
app_name: "{{config}}_discourse"
YAML
config = Config.load_config(yaml)
args = config.generate_docker_run_arguments
expected = []
expected << "--env foo=1 --env bar=2 --env baz=hello_eggs"
expected << "--publish 2222:22 --publish 127.0.0.1:20080:80 --expose 5555"
expected << "--volume /var/discourse/shared:/shared --volume /bar:/baz"
expected << "--link postgres:postgres --link foo:bar"
expected << "--label monitor=true --label app_name=my_app_discourse"
expected.sort!
assert_equal(expected.join(" "), args)
end
def test_tag_filtering
f = Tempfile.new("test")
f.close
yaml = <<~YAML
run:
- exec: 1
- exec:
hook: middle
cmd: 2
tag: one_tag
- exec:
cmd: 3
tag: two_tag
hooks:
after_middle:
- exec: 2.1
before_middle:
- exec: 1.9
YAML
# No tagging loads everything
conf = Config.load_config(yaml)
config = conf.config
assert_equal({ "exec" => 1 }, config["run"][0])
assert_equal({ "exec" => 1.9 }, config["run"][1])
assert_equal(
{ "exec" => { "hook" => "middle", "cmd" => 2, "tag" => "one_tag" } },
config["run"][2]
)
assert_equal({ "exec" => 2.1 }, config["run"][3])
assert_equal(
{ "exec" => { "cmd" => 3, "tag" => "two_tag" } },
config["run"][4]
)
# hooks get applied if hook command is not filtered
conf = Config.load_config(yaml, tags: ["one_tag"])
config = conf.config
assert_equal({ "exec" => 1.9 }, config["run"][0])
assert_equal(
{ "exec" => { "hook" => "middle", "cmd" => 2, "tag" => "one_tag" } },
config["run"][1]
)
assert_equal({ "exec" => 2.1 }, config["run"][2])
# hooks get filtered out if the main hook command is filtered
conf = Config.load_config(yaml, tags: ["two_tag"])
config = conf.config
assert_equal(
{ "exec" => { "cmd" => 3, "tag" => "two_tag" } },
config["run"][0]
)
# skip tags filter out commands with tags
conf = Config.load_config(yaml, skip_tags: ["one_tag"])
config = conf.config
assert_equal({ "exec" => 1 }, config["run"][0])
assert_equal(
{ "exec" => { "cmd" => 3, "tag" => "two_tag" } },
config["run"][1]
)
end
def test_extra_params
config = <<~YAML
params:
one: 1
YAML
config = Config.new(YAML.safe_load(config), extra_params: %w[one=2 two=2])
assert_equal("2", config.params["one"])
assert_equal("2", config.params["two"])
end
end
end

193
test/docker_test.rb Normal file
View File

@ -0,0 +1,193 @@
# frozen_string_literal: true
require "test_helper"
require "tempfile"
require "shellwords"
module Pups
class DockerTest < ::Minitest::Test
def test_gen_env_arguments
yaml = <<~YAML
env:
foo: 1
bar: 2
baz: 'hello_{{spam}}'
env_template:
spam: 'eggs'
YAML
config = Config.load_config(yaml)
Config.transform_config_with_templated_vars(
config.config["env_template"],
config.config["env"]
)
args = Docker.generate_env_arguments(config.config["env"])
assert_equal("--env foo=1 --env bar=2 --env baz=hello_eggs", args)
end
def test_gen_env_arguments_empty
yaml = <<~YAML
env:
foo: 1
bar: 2
baz: ''
YAML
config = Config.load_config(yaml)
Config.transform_config_with_templated_vars(
config.config["env_template"],
config.config["env"]
)
args = Docker.generate_env_arguments(config.config["env"])
assert_equal("--env foo=1 --env bar=2", args)
end
def test_gen_env_arguments_escaped
yaml = <<~YAML
env:
password: "{{spam}}*`echo`@e$t| = >>$()&list;#"
env_template:
spam: 'eggs'
YAML
config = Config.load_config(yaml)
Config.transform_config_with_templated_vars(
config.config["env_template"],
config.config["env"]
)
args = Docker.generate_env_arguments(config.config["env"])
assert_equal(
"--env password=#{Shellwords.escape("eggs*`echo`@e$t| = >>$()&list;#")}",
args
)
end
def test_gen_env_arguments_quoted_with_a_space
yaml = <<~YAML
env:
a_variable: here is a sentence
YAML
config = Config.load_config(yaml)
Config.transform_config_with_templated_vars(
config.config["env_template"],
config.config["env"]
)
args = Docker.generate_env_arguments(config.config["env"])
assert_equal('--env a_variable=here\ is\ a\ sentence', args)
end
def test_gen_env_arguments_newline
pw = <<~PW
this password is
a weird one
PW
yaml = <<~YAML
env:
password: "#{pw}"
env_template:
spam: 'eggs'
YAML
config = Config.load_config(yaml)
Config.transform_config_with_templated_vars(
config.config["env_template"],
config.config["env"]
)
args = Docker.generate_env_arguments(config.config["env"])
assert_equal('--env password=this\ password\ is\ a\ weird\ one\ ', args)
end
def test_gen_expose_arguments
yaml = <<~YAML
expose:
- "2222:22"
- "127.0.0.1:20080:80"
- 5555
YAML
config = Config.load_config(yaml)
args = Docker.generate_expose_arguments(config.config["expose"])
assert_equal(
"--publish 2222:22 --publish 127.0.0.1:20080:80 --expose 5555",
args
)
end
def test_gen_volume_arguments
yaml = <<~YAML
volumes:
- volume:
host: /var/discourse/shared
guest: /shared
- volume:
host: /bar
guest: /baz
YAML
config = Config.load_config(yaml)
args = Docker.generate_volume_arguments(config.config["volumes"])
assert_equal(
"--volume /var/discourse/shared:/shared --volume /bar:/baz",
args
)
end
def test_gen_link_arguments
yaml = <<~YAML
links:
- link:
name: postgres
alias: postgres
- link:
name: foo
alias: bar
YAML
config = Config.load_config(yaml)
args = Docker.generate_link_arguments(config.config["links"])
assert_equal("--link postgres:postgres --link foo:bar", args)
end
def test_gen_label_arguments
yaml = <<~YAML
env_template:
config: my_app
labels:
monitor: "true"
app_name: "{{config}}_discourse"
YAML
config = Config.load_config(yaml)
Config.transform_config_with_templated_vars(
config.config["env_template"],
config.config["labels"]
)
args = Docker.generate_label_arguments(config.config["labels"])
assert_equal(
"--label monitor=true --label app_name=my_app_discourse",
args
)
end
def test_gen_label_arguments_escaped
yaml = <<~YAML
labels:
app_name: "{{config}}'s_di$course"
env_template:
config: my_app
YAML
config = Config.load_config(yaml)
Config.transform_config_with_templated_vars(
config.config["env_template"],
config.config["labels"]
)
args = Docker.generate_label_arguments(config.config["labels"])
assert_equal(
"--label app_name=#{Shellwords.escape("my_app's_di$course")}",
args
)
end
end
end

View File

@ -1,52 +1,50 @@
require 'test_helper'
require 'tempfile'
# frozen_string_literal: true
require "test_helper"
require "tempfile"
module Pups
class ExecCommandTest < MiniTest::Test
def from_str(str, params={})
class ExecCommandTest < ::Minitest::Test
def from_str(str, params = {})
ExecCommand.from_str(str, params).commands
end
def from_hash(hash, params={})
def from_hash(hash, params = {})
ExecCommand.from_hash(hash, params).commands
end
def test_simple_str_command
assert_equal(["do_something"],
from_str("do_something"))
assert_equal(["do_something"], from_str("do_something"))
end
def test_simple_str_command_with_param
assert_equal(["hello world"],
from_str("hello $bob", {"bob" => "world"}))
assert_equal(
["hello world"],
from_str("hello $bob", { "bob" => "world" })
)
end
def test_nested_command
assert_equal(["first"],
from_hash("cmd" => "first"))
assert_equal(["first"], from_hash("cmd" => "first"))
end
def test_multi_commands
assert_equal(["first","second"],
from_hash("cmd" => ["first","second"]))
assert_equal(%w[first second], from_hash("cmd" => %w[first second]))
end
def test_multi_commands_with_home
assert_equal(["cd /home/sam && first",
"cd /home/sam && second"],
from_hash("cmd" => ["first","second"],
"cd" => "/home/sam"))
assert_equal(
["cd /home/sam && first", "cd /home/sam && second"],
from_hash("cmd" => %w[first second], "cd" => "/home/sam")
)
end
def test_exec_works
ExecCommand.from_str("ls",{}).run
ExecCommand.from_str("ls", {}).run
end
def test_fails_for_bad_command
assert_raises(Errno::ENOENT) do
ExecCommand.from_str("boom",{}).run
end
assert_raises(Errno::ENOENT) { ExecCommand.from_str("boom", {}).run }
end
def test_backgroud_task_do_not_fail
@ -64,7 +62,6 @@ module Pups
end
def test_stdin
`touch test_file`
cmd = ExecCommand.new({})
cmd.add("read test ; echo $test > test_file")
@ -72,42 +69,34 @@ module Pups
cmd.run
assert_equal("hello\n", File.read("test_file"))
ensure
File.delete("test_file")
end
def test_fails_for_non_zero_exit
assert_raises(Pups::ExecError) do
ExecCommand.from_str("chgrp -a",{}).run
ExecCommand.from_str("chgrp -a", {}).run
end
end
def test_can_terminate_async
cmd = ExecCommand.new({})
cmd.background = true
pid = cmd.spawn("sleep 10 && exit 1")
ExecCommand.terminate_async
assert_raises(Errno::ECHILD) do
Process.waitpid(pid,Process::WNOHANG)
end
assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
end
def test_can_terminate_rogues
cmd = ExecCommand.new({})
cmd.background = true
pid = cmd.spawn("trap \"echo TERM && sleep 100\" TERM ; sleep 100")
pid = cmd.spawn('trap "echo TERM && sleep 100" TERM ; sleep 100')
# we need to give bash enough time to trap
sleep 0.01
ExecCommand.terminate_async(wait: 0.1)
assert_raises(Errno::ECHILD) do
Process.waitpid(pid,Process::WNOHANG)
end
assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
end
end
end

View File

@ -1,27 +1,25 @@
require 'test_helper'
require 'tempfile'
# frozen_string_literal: true
require "test_helper"
require "tempfile"
module Pups
class FileCommandTest < MiniTest::Test
class FileCommandTest < ::Minitest::Test
def test_simple_file_creation
tmp = Tempfile.new("test")
tmp.write("x")
tmp.close
cmd = FileCommand.new
cmd.path = tmp.path
cmd.contents = "hello $world"
cmd.params = {"world" => "world"}
cmd.params = { "world" => "world" }
cmd.run
assert_equal("hello world",
File.read(tmp.path))
assert_equal("hello world", File.read(tmp.path))
ensure
tmp.close
tmp.unlink
end
end
end

View File

@ -1,56 +1,59 @@
require 'test_helper'
require 'tempfile'
# frozen_string_literal: true
require "test_helper"
require "tempfile"
module Pups
class MergeCommandTest < MiniTest::Test
class MergeCommandTest < ::Minitest::Test
def test_deep_merge_arrays
a = {a: {a: ["hi",1]}}
b = {a: {a: ["hi",2]}}
c = {a: {}}
a = { a: { a: ["hi", 1] } }
b = { a: { a: ["hi", 2] } }
c = { a: {} }
d = Pups::MergeCommand.deep_merge(a,b,:merge_arrays)
d = Pups::MergeCommand.deep_merge(d,c,:merge_arrays)
d = Pups::MergeCommand.deep_merge(a, b, :merge_arrays)
d = Pups::MergeCommand.deep_merge(d, c, :merge_arrays)
assert_equal(["hi", 1,"hi", 2], d[:a][:a])
assert_equal(["hi", 1, "hi", 2], d[:a][:a])
end
def test_merges
source = <<YAML
user:
name: "bob"
password: "xyz"
YAML
source = <<~YAML
user:
name: "bob"
password: "xyz"
YAML
f = Tempfile.new("test")
f.write source
f.close
merge = <<YAML
user:
name: "bob2"
YAML
merge = <<~YAML
user:
name: "bob2"
YAML
MergeCommand.from_str("#{f.path} $yaml", {"yaml" => YAML.load(merge) }).run
MergeCommand.from_str(
"#{f.path} $yaml",
{ "yaml" => YAML.safe_load(merge) }
).run
changed = YAML.load_file(f.path)
changed = YAML.load_file(f.path)
assert_equal({"user" => {
"name" => "bob2",
"password" => "xyz"
}}, changed)
assert_equal(
{ "user" => { "name" => "bob2", "password" => "xyz" } },
changed
)
def test_deep_merge_nil
a = {param: {venison: "yes please"}}
b = {param: nil}
def test_deep_merge_nil
a = { param: { venison: "yes please" } }
b = { param: nil }
r1 = Pups::MergeCommand.deep_merge(a,b)
r2 = Pups::MergeCommand.deep_merge(b,a)
assert_equal({venison: "yes please"}, r1[:param])
assert_equal({venison: "yes please"}, r2[:param])
end
r1 = Pups::MergeCommand.deep_merge(a, b)
r2 = Pups::MergeCommand.deep_merge(b, a)
assert_equal({ venison: "yes please" }, r1[:param])
assert_equal({ venison: "yes please" }, r2[:param])
end
ensure
f.unlink
end

View File

@ -1,9 +1,10 @@
require 'test_helper'
require 'tempfile'
# frozen_string_literal: true
require "test_helper"
require "tempfile"
module Pups
class ReplaceCommandTest < MiniTest::Test
class ReplaceCommandTest < ::Minitest::Test
def test_simple
command = ReplaceCommand.new({})
command.text = "hello world"
@ -14,11 +15,11 @@ module Pups
end
def test_reverse
source = <<SCR
1 one thousand 1
1 one thousand 1
1 one thousand 1
SCR
source = <<~SCR
1 one thousand 1
1 one thousand 1
1 one thousand 1
SCR
f = Tempfile.new("test")
f.write source
@ -33,17 +34,20 @@ SCR
command = ReplaceCommand.from_hash(hash, {})
assert_equal("1 one thousand 1\n1 one thousand 1\n1 hello world 1\n", command.replaced_text)
assert_equal(
"1 one thousand 1\n1 one thousand 1\n1 hello world 1\n",
command.replaced_text
)
ensure
f.unlink
end
def test_global
source = <<SCR
one
one
one
SCR
source = <<~SCR
one
one
one
SCR
f = Tempfile.new("test")
f.write source
@ -61,7 +65,6 @@ SCR
assert_equal("two\ntwo\ntwo\n", command.replaced_text)
ensure
f.unlink
end
def test_replace_with_env
@ -71,26 +74,20 @@ SCR
f.write source
f.close
hash = {
"filename" => f.path,
"from" => "123",
"to" => "hello $hellos"
}
hash = { "filename" => f.path, "from" => "123", "to" => "hello $hellos" }
command = ReplaceCommand.from_hash(hash, {"hello" => "world"})
command = ReplaceCommand.from_hash(hash, { "hello" => "world" })
assert_equal("hello worlds", command.replaced_text)
ensure
f.unlink
end
def test_parse
source = <<SCR
this {
is a test
}
SCR
source = <<~SCR
this {
is a test
}
SCR
f = Tempfile.new("test")
f.write source

View File

@ -1,4 +1,6 @@
require 'pups'
require 'minitest/pride'
# frozen_string_literal: true
require "pups"
require "pups/cli"
require "minitest/autorun"
require "minitest/pride"