capistrano-nomad 0.16.1 → 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +3 -1
- data/README.md +12 -1
- data/Rakefile +8 -1
- data/capistrano-nomad.gemspec +2 -2
- data/lib/capistrano/nomad/helpers/dsl.rb +5 -0
- data/lib/capistrano/nomad/helpers/nomad.rb +33 -8
- data/test/upload_plan_test.rb +142 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56224de9c341668aba37b58d2b0b404decfce7ee5f488364c325319fea262572
|
|
4
|
+
data.tar.gz: 49275ee4444defa39b4de8e6d193342d63b8a1edd08deb6f4946451c8c4b9cf2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 10e0218ac88de63102cb0af504e7eec43985535516d6fcd91c5404224003b4a652e500bef48895707f4532ca83ebac128aef143d6b38571e490eca632bac91c2
|
|
7
|
+
data.tar.gz: 7b53eee7196177a352c757527911ccfb601c8075918f49c8a3557cfc61da571fb707341802affb138fbcb903b13c92e09a619431a6f657ee2bc651ddced4df5f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.16.3]
|
|
4
|
+
|
|
5
|
+
- Add a generated `:force` task to launch a periodic child through the configured manager host and Nomad token
|
|
6
|
+
- Reject non-periodic and disabled periodic jobs before submitting a force request
|
|
7
|
+
- Fail the Capistrano command when job inspection or periodic force fails
|
|
8
|
+
|
|
9
|
+
## [0.16.2]
|
|
10
|
+
|
|
11
|
+
- Fail `upload_plan` and other plan operations when Nomad rejects a job or cannot determine the plan
|
|
12
|
+
- Keep successful plans with allocation changes successful despite Nomad's exit code 1
|
|
13
|
+
- Include CLI regression tests in the gem
|
|
14
|
+
|
|
3
15
|
## [0.16.1]
|
|
4
16
|
|
|
5
17
|
- Pass every configured variable file to `nomad plan` and `nomad run`
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
capistrano-nomad (0.16.
|
|
4
|
+
capistrano-nomad (0.16.3)
|
|
5
5
|
activesupport (<= 7.0.8)
|
|
6
6
|
byebug
|
|
7
7
|
capistrano (~> 3.0)
|
|
@@ -52,9 +52,11 @@ GEM
|
|
|
52
52
|
PLATFORMS
|
|
53
53
|
arm64-darwin-21
|
|
54
54
|
arm64-darwin-23
|
|
55
|
+
x86_64-linux-gnu
|
|
55
56
|
|
|
56
57
|
DEPENDENCIES
|
|
57
58
|
capistrano-nomad!
|
|
59
|
+
minitest (~> 5.0)
|
|
58
60
|
|
|
59
61
|
BUNDLED WITH
|
|
60
62
|
2.4.20
|
data/README.md
CHANGED
|
@@ -145,6 +145,15 @@ cap production nomad:analytics:grafana:restart
|
|
|
145
145
|
cap production nomad:postgres:status
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
+
Force a periodic job without uploading, deploying, or restarting its parent:
|
|
149
|
+
|
|
150
|
+
```shell
|
|
151
|
+
cap production nomad:maintenance:force
|
|
152
|
+
cap production nomad:analytics:archive:force
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The per-job `:force` task checks the deployed job before running `nomad job periodic force`. Non-periodic and disabled periodic jobs fail with a clear error. Inspection and force failures produce a nonzero Capistrano exit. The task uses the configured manager host and `:nomad_token`, like other job actions.
|
|
156
|
+
|
|
148
157
|
Tasks can apply across all namespaces or be filtered by namespaces or tags
|
|
149
158
|
|
|
150
159
|
```shell
|
|
@@ -213,7 +222,9 @@ cap production nomad:version
|
|
|
213
222
|
|
|
214
223
|
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
215
224
|
|
|
216
|
-
|
|
225
|
+
Run `bundle exec rake test` to test the generated Capistrano `upload_plan` and `force` tasks through the CLI. The tests use local transport and controlled Nomad exit codes. Force coverage checks the job, namespace, token, rejected requests, and non-periodic jobs. To also check rejection of invalid HCL with a real Nomad binary, run `NOMAD_REAL_BINARY=/path/to/nomad bundle exec rake test`.
|
|
226
|
+
|
|
227
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version in `capistrano-nomad.gemspec`, run `bundle install`, and update `CHANGELOG.md`. Run `bundle exec rake test` and commit the release changes. Then run `bundle exec rake release`, which creates the version tag, pushes the release commit and tag, and publishes the gem to [RubyGems](https://rubygems.org).
|
|
217
228
|
|
|
218
229
|
## Contributing
|
|
219
230
|
|
data/Rakefile
CHANGED
data/capistrano-nomad.gemspec
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Gem::Specification.new do |spec|
|
|
4
4
|
spec.name = "capistrano-nomad"
|
|
5
|
-
spec.version = "0.16.
|
|
5
|
+
spec.version = "0.16.3"
|
|
6
6
|
spec.authors = ["James Hu"]
|
|
7
7
|
|
|
8
8
|
spec.summary = "Capistrano plugin for deploying and managing Nomad jobs"
|
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
# Specify which files should be added to the gem when it is released. The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
19
19
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
20
20
|
%x(git ls-files -z).split("\x0").reject do |f|
|
|
21
|
-
(f == __FILE__) || f.match(%r{\A(?:(?:bin|
|
|
21
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
spec.bindir = "exe"
|
|
@@ -91,6 +91,11 @@ def nomad_job(name, attributes = {})
|
|
|
91
91
|
capistrano_nomad_run_jobs([name], namespace: namespace, is_detached: capistrano_nomad_job_detached_overridden?)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
desc("Force a new periodic child of #{description_name} job")
|
|
95
|
+
task(:force) do
|
|
96
|
+
capistrano_nomad_force_job(name, namespace: namespace)
|
|
97
|
+
end
|
|
98
|
+
|
|
94
99
|
desc("Purge and run #{description_name} job again")
|
|
95
100
|
task(:rerun) do
|
|
96
101
|
capistrano_nomad_rerun_jobs([name], namespace: namespace, is_detached: capistrano_nomad_job_detached_overridden?)
|
|
@@ -138,7 +138,11 @@ def capistrano_nomad_build_release_var_file_path(name, **options)
|
|
|
138
138
|
"#{release_path}#{capistrano_nomad_ensure_absolute_path(capistrano_nomad_build_base_var_file_path(name, **options))}"
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
def
|
|
141
|
+
def capistrano_nomad_plan_command?(args)
|
|
142
|
+
args.first.to_s == "plan" || args.take(2).map(&:to_s) == ["job", "plan"]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def capistrano_nomad_run_nomad_command(kind, *args, raise_on_non_zero_exit: false)
|
|
142
146
|
converted_args = args.each_with_object([]) do |arg, collection|
|
|
143
147
|
# If hash then convert it as options
|
|
144
148
|
if arg.is_a?(Hash)
|
|
@@ -165,24 +169,34 @@ def capistrano_nomad_run_nomad_command(kind, *args)
|
|
|
165
169
|
end
|
|
166
170
|
|
|
167
171
|
with(env_vars) do
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
if capistrano_nomad_plan_command?(args)
|
|
173
|
+
# Nomad returns 1 for a successful plan with changes; all other nonzero exits are errors
|
|
174
|
+
public_send(kind, :nomad, *converted_args, '; status=$?; if [ "$status" -eq 1 ]; then exit 0; else exit "$status"; fi', raise_on_non_zero_exit: true)
|
|
175
|
+
else
|
|
176
|
+
# Preserve unchecked execution unless the caller requires failures to propagate
|
|
177
|
+
public_send(kind, :nomad, *converted_args, raise_on_non_zero_exit: raise_on_non_zero_exit)
|
|
178
|
+
end
|
|
170
179
|
end
|
|
171
180
|
end
|
|
172
181
|
|
|
173
|
-
def capistrano_nomad_execute_nomad_command(*args, **options)
|
|
182
|
+
def capistrano_nomad_execute_nomad_command(*args, raise_on_non_zero_exit: false, **options)
|
|
174
183
|
capistrano_nomad_run_remotely do |host|
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
if capistrano_nomad_plan_command?(args) || raise_on_non_zero_exit
|
|
185
|
+
# Use checked execution when failures must propagate because the interactive backend does not raise
|
|
186
|
+
capistrano_nomad_run_nomad_command(:execute, *args, options, raise_on_non_zero_exit: raise_on_non_zero_exit)
|
|
187
|
+
else
|
|
188
|
+
run_interactively(host) do
|
|
189
|
+
capistrano_nomad_run_nomad_command(:execute, *args, options)
|
|
190
|
+
end
|
|
177
191
|
end
|
|
178
192
|
end
|
|
179
193
|
end
|
|
180
194
|
|
|
181
|
-
def capistrano_nomad_capture_nomad_command(*args, **options)
|
|
195
|
+
def capistrano_nomad_capture_nomad_command(*args, raise_on_non_zero_exit: false, **options)
|
|
182
196
|
output = nil
|
|
183
197
|
|
|
184
198
|
capistrano_nomad_run_remotely do
|
|
185
|
-
output = capistrano_nomad_run_nomad_command(:capture, *args,
|
|
199
|
+
output = capistrano_nomad_run_nomad_command(:capture, *args, options, raise_on_non_zero_exit: raise_on_non_zero_exit)
|
|
186
200
|
end
|
|
187
201
|
|
|
188
202
|
output
|
|
@@ -620,6 +634,17 @@ def capistrano_nomad_restart_jobs(names, **options)
|
|
|
620
634
|
end
|
|
621
635
|
end
|
|
622
636
|
|
|
637
|
+
def capistrano_nomad_force_job(name, **options)
|
|
638
|
+
capistrano_nomad_ensure_options!(options)
|
|
639
|
+
job = JSON.parse(capistrano_nomad_capture_nomad_command(:job, :inspect, options, name, raise_on_non_zero_exit: true))
|
|
640
|
+
|
|
641
|
+
unless job.dig("Periodic", "Enabled")
|
|
642
|
+
raise ArgumentError, "Job #{options[:namespace]}/#{name} is not an enabled periodic job"
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
capistrano_nomad_execute_nomad_command(:job, :periodic, :force, options, name, raise_on_non_zero_exit: true)
|
|
646
|
+
end
|
|
647
|
+
|
|
623
648
|
def capistrano_nomad_start_jobs(names, **options)
|
|
624
649
|
capistrano_nomad_ensure_options!(options)
|
|
625
650
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
|
|
8
|
+
class UploadPlanTest < Minitest::Test
|
|
9
|
+
def test_upload_plan_succeeds_for_accepted_plans_and_fails_for_rejected_plans
|
|
10
|
+
Dir.mktmpdir("capistrano-nomad") do |root|
|
|
11
|
+
FileUtils.mkdir_p(["#{root}/config/deploy", "#{root}/nomad/jobs", "#{root}/bin"])
|
|
12
|
+
File.write("#{root}/Capfile", <<~RUBY)
|
|
13
|
+
require "capistrano/setup"
|
|
14
|
+
require "capistrano/deploy"
|
|
15
|
+
require "capistrano/nomad"
|
|
16
|
+
install_plugin Capistrano::Nomad
|
|
17
|
+
RUBY
|
|
18
|
+
File.write("#{root}/config/deploy.rb", <<~RUBY)
|
|
19
|
+
set :application, "plan-test"
|
|
20
|
+
set :deploy_to, #{"#{root}/remote".inspect}
|
|
21
|
+
set :root, #{root.inspect}
|
|
22
|
+
set :current_revision, "test"
|
|
23
|
+
set :nomad_docker_image_types, {}
|
|
24
|
+
SSHKit.config.command_map[:nomad] = #{"#{root}/bin/nomad".inspect}
|
|
25
|
+
nomad_job :example
|
|
26
|
+
set :nomad_token, "force-test-token"
|
|
27
|
+
nomad_namespace :spleencheese do
|
|
28
|
+
nomad_job :"myanonamouse-maintenance"
|
|
29
|
+
end
|
|
30
|
+
RUBY
|
|
31
|
+
File.write("#{root}/config/deploy/test.rb", <<~RUBY)
|
|
32
|
+
server "localhost", roles: [:manager]
|
|
33
|
+
set :sshkit_backend, SSHKit::Backend::Local
|
|
34
|
+
RUBY
|
|
35
|
+
File.write("#{root}/bin/ssh", <<~SH)
|
|
36
|
+
#!/bin/sh
|
|
37
|
+
for argument do command="$argument"; done
|
|
38
|
+
exec /bin/sh -c "$command"
|
|
39
|
+
SH
|
|
40
|
+
FileUtils.chmod(0755, "#{root}/bin/ssh")
|
|
41
|
+
File.write("#{root}/bin/nomad", <<~SH)
|
|
42
|
+
#!/bin/sh
|
|
43
|
+
if [ "$1" = job ]; then
|
|
44
|
+
[ "$NOMAD_TOKEN" = force-test-token ] || exit 98
|
|
45
|
+
printf '%s\n' "$*" >> #{root}/commands
|
|
46
|
+
if [ "$2" = inspect ]; then
|
|
47
|
+
printf '%s\n' "$NOMAD_TEST_JOB"
|
|
48
|
+
exit "${NOMAD_TEST_INSPECT_EXIT:-0}"
|
|
49
|
+
fi
|
|
50
|
+
[ "$2 $3" = 'periodic force' ] || exit 99
|
|
51
|
+
if [ "$NOMAD_TEST_EXIT" = 0 ]; then
|
|
52
|
+
echo 'Created periodic child'
|
|
53
|
+
else
|
|
54
|
+
echo 'Periodic force rejected' >&2
|
|
55
|
+
fi
|
|
56
|
+
exit "$NOMAD_TEST_EXIT"
|
|
57
|
+
fi
|
|
58
|
+
[ "$1" = plan ] || exit 99
|
|
59
|
+
cat "$2"
|
|
60
|
+
case "$NOMAD_TEST_EXIT" in
|
|
61
|
+
0|1) echo 'Scheduler dry-run: accepted';;
|
|
62
|
+
*) echo 'Error determining plan results' >&2;;
|
|
63
|
+
esac
|
|
64
|
+
exit "$NOMAD_TEST_EXIT"
|
|
65
|
+
SH
|
|
66
|
+
FileUtils.chmod(0755, "#{root}/bin/nomad")
|
|
67
|
+
|
|
68
|
+
[0, 1, 255, 2].each do |exit_code|
|
|
69
|
+
job = exit_code < 2 ? 'job "example" {}' : 'job "example" {'
|
|
70
|
+
File.write("#{root}/nomad/jobs/example.hcl", job)
|
|
71
|
+
output, status = Open3.capture2e(
|
|
72
|
+
{ "NOMAD_TEST_EXIT" => exit_code.to_s, "PATH" => "#{root}/bin:#{ENV.fetch("PATH")}" },
|
|
73
|
+
"bundle", "exec", "cap", "test", "nomad:example:upload_plan",
|
|
74
|
+
chdir: root,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
assert_includes(output, job, output)
|
|
78
|
+
if exit_code < 2
|
|
79
|
+
assert(status.success?, output)
|
|
80
|
+
assert_includes(output, "Scheduler dry-run: accepted")
|
|
81
|
+
else
|
|
82
|
+
refute(status.success?, output)
|
|
83
|
+
assert_includes(output, "Error determining plan results")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
[0, 1, 2].each do |exit_code|
|
|
88
|
+
File.write("#{root}/commands", "")
|
|
89
|
+
output, status = Open3.capture2e(
|
|
90
|
+
{ "NOMAD_TEST_EXIT" => exit_code.to_s, "NOMAD_TEST_JOB" => '{"Periodic":{"Enabled":true}}' },
|
|
91
|
+
"bundle", "exec", "cap", "test", "nomad:spleencheese:myanonamouse-maintenance:force",
|
|
92
|
+
chdir: root,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert_equal("job inspect -namespace=spleencheese myanonamouse-maintenance\njob periodic force -namespace=spleencheese myanonamouse-maintenance\n", File.read("#{root}/commands"), output)
|
|
96
|
+
assert_equal(exit_code.zero?, status.success?, output)
|
|
97
|
+
assert_includes(output, exit_code.zero? ? "Created periodic child" : "Periodic force rejected")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
['{"Periodic":null}', '{"Periodic":{"Enabled":false}}'].each do |job|
|
|
101
|
+
File.write("#{root}/commands", "")
|
|
102
|
+
output, status = Open3.capture2e(
|
|
103
|
+
{ "NOMAD_TEST_JOB" => job },
|
|
104
|
+
"bundle", "exec", "cap", "test", "nomad:example:force",
|
|
105
|
+
chdir: root,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
refute(status.success?, output)
|
|
109
|
+
assert_includes(output, "Job default/example is not an enabled periodic job")
|
|
110
|
+
assert_equal("job inspect -namespace=default example\n", File.read("#{root}/commands"), output)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
File.write("#{root}/commands", "")
|
|
114
|
+
output, status = Open3.capture2e(
|
|
115
|
+
{ "NOMAD_TEST_JOB" => '{"Periodic":{"Enabled":true}}', "NOMAD_TEST_INSPECT_EXIT" => "1" },
|
|
116
|
+
"bundle", "exec", "cap", "test", "nomad:example:force",
|
|
117
|
+
chdir: root,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
refute(status.success?, output)
|
|
121
|
+
assert_equal("job inspect -namespace=default example\n", File.read("#{root}/commands"), output)
|
|
122
|
+
|
|
123
|
+
File.write("#{root}/commands", "")
|
|
124
|
+
output, status = Open3.capture2e(
|
|
125
|
+
{ "NOMAD_TEST_JOB" => '{"Periodic":{"Enabled":true}}', "NOMAD_TEST_EXIT" => "0" },
|
|
126
|
+
"bundle", "exec", "cap", "test", "nomad:example:force",
|
|
127
|
+
chdir: root,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
assert(status.success?, output)
|
|
131
|
+
assert_equal("job inspect -namespace=default example\njob periodic force -namespace=default example\n", File.read("#{root}/commands"), output)
|
|
132
|
+
|
|
133
|
+
if ENV["NOMAD_REAL_BINARY"]
|
|
134
|
+
FileUtils.cp(ENV.fetch("NOMAD_REAL_BINARY"), "#{root}/bin/nomad")
|
|
135
|
+
output, status = Open3.capture2e("bundle", "exec", "cap", "test", "nomad:example:upload_plan", chdir: root)
|
|
136
|
+
|
|
137
|
+
refute(status.success?, output)
|
|
138
|
+
assert_includes(output, "Error parsing job file")
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: capistrano-nomad
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.16.
|
|
4
|
+
version: 0.16.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James Hu
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -130,6 +130,7 @@ files:
|
|
|
130
130
|
- lib/capistrano/nomad/helpers/nomad.rb
|
|
131
131
|
- lib/capistrano/nomad/scm/git_local.rb
|
|
132
132
|
- lib/capistrano/nomad/tasks/nomad.rake
|
|
133
|
+
- test/upload_plan_test.rb
|
|
133
134
|
homepage: https://github.com/axsuul/capistrano-nomad
|
|
134
135
|
licenses:
|
|
135
136
|
- MIT
|
|
@@ -152,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
152
153
|
- !ruby/object:Gem::Version
|
|
153
154
|
version: '0'
|
|
154
155
|
requirements: []
|
|
155
|
-
rubygems_version: 3.4.
|
|
156
|
+
rubygems_version: 3.4.19
|
|
156
157
|
signing_key:
|
|
157
158
|
specification_version: 4
|
|
158
159
|
summary: Capistrano plugin for deploying and managing Nomad jobs
|