capistrano-nomad 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9766d262aebd461e4801a9bdde77735250dfeb7625faf9c4d6771f20db0516ee
4
+ data.tar.gz: 4afe079c96f410f5998adad6347e22cb73453d44d9a7a3b550503b5ad03166b1
5
+ SHA512:
6
+ metadata.gz: ce0f93781fdd9ea9f4a97130cff43421bd272052f588a9240aa1718cbf8640c0271f14fdb2fd14a52007f19a05eb9746dfdd02ec0f2ed623f586032a8e3bbc18
7
+ data.tar.gz: 3e5df7d419965dd0e289170ebb3c292d46d86dcfd7dee5fb534ea272f4e2f09b5912c2c5d09e1b30c0eaab5b1a367789615eccfb590bd5192ce59eff16d10da5
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-09-09
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in capistrano-nomad.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,55 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ capistrano-nomad (0.1.0)
5
+ activesupport (= 7.0.8)
6
+ byebug
7
+ capistrano (= 3.14.1)
8
+ git (= 1.10.2)
9
+ rake (~> 13.0)
10
+ sshkit (= 1.21.2)
11
+ sshkit-interactive (= 0.3.0)
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ activesupport (7.0.8)
17
+ concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ tzinfo (~> 2.0)
21
+ airbrussh (1.5.0)
22
+ sshkit (>= 1.6.1, != 1.7.0)
23
+ byebug (11.1.3)
24
+ capistrano (3.14.1)
25
+ airbrussh (>= 1.0.0)
26
+ i18n
27
+ rake (>= 10.0.0)
28
+ sshkit (>= 1.9.0)
29
+ concurrent-ruby (1.2.2)
30
+ git (1.10.2)
31
+ rchardet (~> 1.8)
32
+ i18n (1.14.1)
33
+ concurrent-ruby (~> 1.0)
34
+ minitest (5.20.0)
35
+ net-scp (4.0.0)
36
+ net-ssh (>= 2.6.5, < 8.0.0)
37
+ net-ssh (7.2.0)
38
+ rake (13.0.6)
39
+ rchardet (1.8.0)
40
+ sshkit (1.21.2)
41
+ net-scp (>= 1.1.2)
42
+ net-ssh (>= 2.8.0)
43
+ sshkit-interactive (0.3.0)
44
+ sshkit (~> 1.12)
45
+ tzinfo (2.0.6)
46
+ concurrent-ruby (~> 1.0)
47
+
48
+ PLATFORMS
49
+ arm64-darwin-21
50
+
51
+ DEPENDENCIES
52
+ capistrano-nomad!
53
+
54
+ BUNDLED WITH
55
+ 2.3.5
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 James Hu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # capistrano-nomad
2
+
3
+ Capistrano plugin for deploying and managing [Nomad](http://nomadproject.io) jobs
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "capistrano-nomad"
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install capistrano-nomad
20
+
21
+ ## Usage
22
+
23
+ ## Development
24
+
25
+ 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.
26
+
27
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/axsuul/capistrano-nomad.
32
+
33
+ ## License
34
+
35
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/capistrano/nomad/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "capistrano-nomad"
7
+ spec.version = Capistrano::Nomad::VERSION
8
+ spec.authors = ["James Hu"]
9
+
10
+ spec.summary = "Capistrano plugin for deploying and managing Nomad jobs"
11
+ spec.description = "Capistrano plugin for deploying and managing Nomad jobs"
12
+ spec.homepage = "https://github.com/axsuul/capistrano-nomad"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 2.6.0"
15
+
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/axsuul/capistrano-nomad"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
25
+ end
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ # Uncomment to register a new dependency of your gem
32
+ spec.add_dependency "activesupport", "7.0.8"
33
+ spec.add_dependency "byebug"
34
+ spec.add_dependency "capistrano", "3.14.1"
35
+ spec.add_dependency "git", "1.10.2"
36
+ spec.add_dependency "rake", "~> 13.0"
37
+ spec.add_dependency "sshkit", "1.21.2"
38
+ spec.add_dependency "sshkit-interactive", "0.3.0"
39
+
40
+ # For more information and examples about making a new gem, check out our
41
+ # guide at: https://bundler.io/guides/creating_gem.html
42
+ end
@@ -0,0 +1,3 @@
1
+ def capistrano_nomad_deep_symbolize_hash_keys(hash)
2
+ JSON.parse(JSON[hash], symbolize_names: true)
3
+ end
@@ -0,0 +1,134 @@
1
+ def capistrano_nomad_docker_image_types_manifest_path
2
+ shared_path.join("docker-image-types.json")
3
+ end
4
+
5
+ def capistrano_nomad_read_docker_image_types_manifest
6
+ manifest = {}
7
+
8
+ on(roles(:manager)) do |_host|
9
+ # Ensure file exists
10
+ execute("mkdir -p #{shared_path}")
11
+ execute("touch #{capistrano_nomad_docker_image_types_manifest_path}")
12
+
13
+ output = capture "cat #{capistrano_nomad_docker_image_types_manifest_path}"
14
+
15
+ unless output.blank?
16
+ manifest = JSON.parse(output)
17
+ end
18
+ end
19
+
20
+ capistrano_nomad_deep_symbolize_hash_keys(manifest)
21
+ end
22
+
23
+ def capistrano_nomad_update_docker_image_types_manifest(image_type, properties = {})
24
+ on(roles(:manager)) do |_host|
25
+ # Read and update manifest
26
+ manifest = capistrano_nomad_read_docker_image_types_manifest
27
+ manifest[image_type] = (manifest[image_type] || {}).merge(properties.stringify_keys)
28
+
29
+ io = StringIO.new(JSON.pretty_generate(manifest))
30
+
31
+ # Write to manifest
32
+ upload!(io, capistrano_nomad_docker_image_types_manifest_path)
33
+ end
34
+ end
35
+
36
+ def capistrano_nomad_build_docker_image_alias(image_type, ref)
37
+ # Define nomad_docker_image_alias has a proc to return image alias
38
+ fetch(:nomad_docker_image_alias).call(image_type: image_type, ref: ref)
39
+ end
40
+
41
+ # Fetch image alias from manifest
42
+ def current_docker_service_image_alias(image_type)
43
+ image_alias =
44
+ capistrano_nomad_read_docker_image_types_manifest
45
+ .try(:[], image_type.to_sym)
46
+ .try(:[], :image_alias)
47
+
48
+ return image_alias if image_alias
49
+
50
+ capistrano_nomad_build_docker_image_alias(image_type, capistrano_nomad_fetch_git_commit_id)
51
+ end
52
+
53
+ # Builds docker image from image type
54
+ #
55
+ # @param image_type [String, Symbol]
56
+ def capistrano_nomad_build_docker_image(image_type, path, *args)
57
+ run_locally do
58
+ git_commit_id = capistrano_nomad_fetch_git_commit_id
59
+
60
+ [
61
+ capistrano_nomad_build_docker_image_alias(image_type, git_commit_id),
62
+ capistrano_nomad_build_docker_image_alias(image_type, "latest"),
63
+ "trunk/#{fetch(:stage)}/#{image_type}:latest",
64
+ ].each do |tag|
65
+ args << "--tag #{tag}"
66
+ end
67
+
68
+ # If any of these files exist then we're in the middle of rebase so we should interrupt
69
+ if ["rebase-merge", "rebase-apply"].any? { |f| File.exist?("#{fetch(:git).dir.path}/.git/#{f}") }
70
+ raise StandardError, "still in the middle of git rebase, interrupting docker image build"
71
+ end
72
+
73
+ system "docker build #{args.join(' ')} #{path}"
74
+ end
75
+ end
76
+
77
+ def capistrano_nomad_build_docker_image_for_type(image_type)
78
+ image_type = image_type.to_sym
79
+
80
+ attributes = fetch(:nomad_docker_image_types)[image_type]
81
+
82
+ return unless attributes
83
+
84
+ args = [
85
+ # Ensure images are built for x86_64 which is production env otherwise it will default to local development env
86
+ # which can be arm64 (Apple Silicon)
87
+ "--platform linux/amd64",
88
+ ]
89
+
90
+ build_args =
91
+ if attributes[:build_args].is_a?(Proc)
92
+ proc_args = attributes[:build_args].arity == 1 ? [capistrano_nomad_fetch_git_commit_id] : []
93
+
94
+ attributes[:build_args].call(*proc_args)
95
+ else
96
+ attributes[:build_args]
97
+ end
98
+
99
+ if (target = attributes[:target])
100
+ args << "--target #{target}"
101
+ end
102
+
103
+ (build_args || []).each do |key, value|
104
+ args << "--build-arg #{key}=#{value}"
105
+ end
106
+
107
+ capistrano_nomad_build_docker_image(image_type, attributes[:path], args)
108
+ end
109
+
110
+ def capistrano_nomad_push_docker_image_for_type(image_type, is_manifest_updated: true)
111
+ # Allows end user to add hooks
112
+ invoke("nomad:docker_images:push")
113
+
114
+ run_locally do
115
+ git_commit_id = capistrano_nomad_fetch_git_commit_id
116
+ revision_image_alias = capistrano_nomad_build_docker_image_alias(image_type, git_commit_id)
117
+ latest_image_alias = capistrano_nomad_build_docker_image_alias(image_type, "latest")
118
+
119
+ [revision_image_alias, latest_image_alias].each do |image_alias|
120
+ # We should not proceed if image cannot be pushed
121
+ unless system("docker push #{image_alias}")
122
+ raise StandardError, "Docker image push unsuccessful!"
123
+ end
124
+ end
125
+
126
+ return unless is_manifest_updated
127
+
128
+ # Update image type manifest
129
+ capistrano_nomad_update_docker_image_types_manifest(image_type,
130
+ ref: git_commit_id,
131
+ image_alias: revision_image_alias
132
+ )
133
+ end
134
+ end
@@ -0,0 +1,127 @@
1
+ def nomad_docker_image_type(image_type, attributes = {})
2
+ docker_image_types = fetch(:nomad_docker_image_types) || {}
3
+ docker_image_types[image_type] = attributes
4
+
5
+ set(:nomad_docker_image_types, docker_image_types)
6
+ end
7
+
8
+ def nomad_namespace(namespace, &block)
9
+ @nomad_namespace = namespace
10
+
11
+ instance_eval(&block)
12
+
13
+ @nomad_namespace = nil
14
+
15
+ true
16
+ end
17
+
18
+ def nomad_job(name, attributes = {})
19
+ nomad_jobs = fetch(:nomad_jobs) || Hash.new { |h, n| h[n] = {} }
20
+ nomad_jobs[@nomad_namespace][name] = attributes
21
+
22
+ set(:nomad_jobs, nomad_jobs)
23
+
24
+ define_tasks = lambda do |namespace: nil|
25
+ description_name = ""
26
+ description_name << "#{namespace}/" if namespace
27
+ description_name << name.to_s
28
+
29
+ namespace(name) do
30
+ desc "Build #{description_name} job Docker images"
31
+ task :build do
32
+ capistrano_nomad_build_jobs_docker_images([name], namespace: namespace)
33
+ end
34
+
35
+ desc "Push #{description_name} job Docker images"
36
+ task :push do
37
+ capistrano_nomad_push_jobs_docker_images([name], namespace: namespace)
38
+ end
39
+
40
+ desc "Build and push #{description_name} job Docker images"
41
+ task :assemble do
42
+ capistrano_nomad_build_jobs_docker_images([name], namespace: namespace)
43
+ capistrano_nomad_push_jobs_docker_images([name], namespace: namespace)
44
+ end
45
+
46
+ desc "Upload #{description_name} job and related files"
47
+ task :upload do
48
+ capistrano_nomad_upload_jobs([name], namespace: namespace)
49
+ end
50
+
51
+ desc "Run #{description_name} job"
52
+ task :run do
53
+ capistrano_nomad_run_jobs([name], namespace: namespace)
54
+ end
55
+
56
+ desc "Purge and run #{description_name} job again"
57
+ task :rerun do
58
+ capistrano_nomad_rerun_jobs([name], namespace: namespace)
59
+ end
60
+
61
+ desc "Purge #{description_name} job"
62
+ task :purge do
63
+ capistrano_nomad_purge_jobs([name], namespace: namespace, is_detached: false)
64
+ end
65
+
66
+ desc "Display status of #{description_name} job"
67
+ task :status do
68
+ capistrano_nomad_display_job_status(name, namespace: namespace)
69
+ end
70
+
71
+ desc "Upload and plan #{description_name} job"
72
+ task :upload_plan do
73
+ capistrano_nomad_upload_plan_jobs([name], namespace: namespace)
74
+ end
75
+
76
+ desc "Upload and run #{description_name} job"
77
+ task :upload_run do
78
+ capistrano_nomad_upload_run_jobs([name], namespace: namespace)
79
+ end
80
+
81
+ desc "Upload and re-run #{description_name} job"
82
+ task :upload_rerun do
83
+ capistrano_nomad_upload_rerun_jobs([name], namespace: namespace)
84
+ end
85
+
86
+ desc "Deploy #{description_name} job"
87
+ task :deploy do
88
+ capistrano_nomad_deploy_jobs([name], namespace: namespace)
89
+ end
90
+
91
+ desc "Restart #{description_name} job"
92
+ task :restart do
93
+ on roles :manager do
94
+ # We can't restart the job directly so we'll need to fetch all its running allocs and restart each of one
95
+ # individually instead
96
+ running_alloc_ids_output = capture(
97
+ "nomad job allocs " \
98
+ "-t '{{range .}}{{if eq .ClientStatus \"running\"}}{{ println .ID}}{{end}}{{end}}' " \
99
+ "#{name}",
100
+ )
101
+
102
+ running_alloc_ids = running_alloc_ids_output.strip.split("\n")
103
+
104
+ running_alloc_ids.each do |alloc_id|
105
+ capistrano_nomad_execute_nomad_command(:alloc, :restart, alloc_id)
106
+ end
107
+ end
108
+ end
109
+
110
+ desc "Open console to #{description_name} job"
111
+ task :console do
112
+ capistrano_nomad_exec_within_job(name, "/bin/bash", namespace: namespace)
113
+ end
114
+ end
115
+ end
116
+
117
+ namespace(:nomad) do
118
+ # Define tasks for service
119
+ if @nomad_namespace
120
+ namespace(@nomad_namespace) do
121
+ define_tasks.call(namespace: @nomad_namespace)
122
+ end
123
+ else
124
+ define_tasks.call
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,9 @@
1
+ require "git"
2
+
3
+ def capistrano_nomad_git
4
+ Git.open(".")
5
+ end
6
+
7
+ def capistrano_nomad_fetch_git_commit_id
8
+ capistrano_nomad_git.log.first.sha
9
+ end
@@ -0,0 +1,275 @@
1
+ require "active_support/core_ext/string"
2
+ require "sshkit/interactive"
3
+
4
+ class NomadErbNamespace
5
+ def initialize(context:, vars: {})
6
+ @context = context
7
+
8
+ vars.each do |key, value|
9
+ instance_variable_set("@#{key}", value)
10
+ end
11
+ end
12
+
13
+ # Use default value passed in unless `nomad_job_task_cpu_resource` is set
14
+ def build_nomad_job_task_cpu_resource(default:)
15
+ nomad_job_task_cpu_resource == "null" ? default : nomad_job_task_cpu_resource
16
+ end
17
+
18
+ # Use default value passed in unless `nomad_job_task_memory_resource` is set
19
+ def build_nomad_job_task_memory_resource(default:)
20
+ nomad_job_task_memory_resource == "null" ? default : nomad_job_task_memory_resource
21
+ end
22
+
23
+ # rubocop:disable Style/MissingRespondToMissing
24
+ def method_missing(name, *args)
25
+ instance_variable = "@#{name}"
26
+
27
+ # First try to see if it's a variable we're trying to access which is stored in an instance variable otherwise try
28
+ # to see if there's a local method defineds
29
+ if instance_variable_defined?(instance_variable)
30
+ instance_variable_get(instance_variable)
31
+ elsif respond_to?(name)
32
+ send(name, *args)
33
+ end
34
+ end
35
+ # rubocop:enable Style/MissingRespondToMissing
36
+ end
37
+
38
+ def capistrano_nomad_build_file_path(parent_path, basename, namespace: nil)
39
+ # Remove initial slash if it exists
40
+ parent_path = parent_path[1..] if parent_path[0] == "/"
41
+
42
+ segments = [parent_path]
43
+ segments << namespace if namespace
44
+ segments << "#{basename}.hcl"
45
+
46
+ segments.join("/")
47
+ end
48
+
49
+ def capistrano_nomad_build_base_job_path(*args)
50
+ capistrano_nomad_build_file_path(fetch(:nomad_jobs_path), *args)
51
+ end
52
+
53
+ def capistrano_nomad_build_base_var_file_path(*args)
54
+ capistrano_nomad_build_file_path(fetch(:nomad_var_files_path), *args)
55
+ end
56
+
57
+ def capistrano_nomad_build_local_job_path(name, *args)
58
+ local_path = fetch(:root).join(capistrano_nomad_build_base_job_path(name, *args))
59
+
60
+ # Determine if it has .erb appended or not
61
+ [local_path, "#{local_path}.erb"].find { |path| File.exist?(path) }
62
+ end
63
+
64
+ def capistrano_nomad_build_release_job_path(*args)
65
+ "#{release_path}/#{capistrano_nomad_build_base_job_path(*args)}"
66
+ end
67
+
68
+ def capistrano_nomad_build_release_var_file_path(*args)
69
+ "#{release_path}/#{capistrano_nomad_build_base_var_file_path(*args)}"
70
+ end
71
+
72
+ def capistrano_nomad_execute_nomad_command(*args)
73
+ converted_args = args.each_with_object([]) do |arg, collection|
74
+ # If hash then convert it as options
75
+ if arg.is_a?(Hash)
76
+ arg.each do |key, value|
77
+ next unless value
78
+
79
+ option = "-#{key.to_s.dasherize}"
80
+
81
+ # Doesn't need a value if it's just meant to be a flag
82
+ option << "=#{value}" unless value == true
83
+
84
+ collection << option
85
+ end
86
+ else
87
+ collection << arg
88
+ end
89
+ end
90
+
91
+ on(roles(:manager)) do |host|
92
+ run_interactively(host) do
93
+ # Ignore errors
94
+ execute(:nomad, *converted_args, raise_on_non_zero_exit: false)
95
+ end
96
+ end
97
+ end
98
+
99
+ def capistrano_nomad_exec_within_job(name, command, namespace: nil)
100
+ capistrano_nomad_execute_nomad_command(:alloc, :exec, { namespace: namespace, task: name, job: name }, command)
101
+ end
102
+
103
+ def capistrano_nomad_upload_file(local_path:, remote_path:, erb_vars: {})
104
+ docker_image_types = fetch(:nomad_docker_image_types)
105
+ docker_image_types_manifest = capistrano_nomad_read_docker_image_types_manifest
106
+
107
+ # Merge manifest into image types
108
+ docker_image_types_manifest.each do |manifest_image_type, manifest_attributes|
109
+ docker_image_types[manifest_image_type]&.merge!(manifest_attributes) || {}
110
+ end
111
+
112
+ # Parse manifest files using ERB
113
+ erb = ERB.new(File.open(local_path).read, trim_mode: "-")
114
+
115
+ final_erb_vars = {
116
+ git_commit_id: fetch(:current_revision) || capistrano_nomad_fetch_git_commit_id,
117
+ docker_image_types: docker_image_types,
118
+ }
119
+
120
+ # Add global ERB vars
121
+ final_erb_vars.merge!(fetch(:nomad_erb_vars) || {})
122
+
123
+ # Add job-specific ERB vars
124
+ final_erb_vars.merge!(erb_vars)
125
+
126
+ # We use a custom namespace class so that we can include helper methods into the namespace to make them available for
127
+ # template to access
128
+ namespace = NomadErbNamespace.new(
129
+ context: self,
130
+ vars: final_erb_vars,
131
+ )
132
+ erb_io = StringIO.new(erb.result(namespace.instance_eval { binding }))
133
+
134
+ on(roles(:manager)) do
135
+ execute(:mkdir, "-p", File.dirname(remote_path))
136
+ upload!(erb_io, remote_path)
137
+ end
138
+ end
139
+
140
+ def capistrano_nomad_fetch_job_options(name, *args, namespace: nil)
141
+ fetch(:nomad_jobs).dig(namespace, name.to_sym, *args)
142
+ end
143
+
144
+ def capistrano_nomad_fetch_job_var_files(name, *args)
145
+ capistrano_nomad_fetch_job_options(name, :var_files, *args) || []
146
+ end
147
+
148
+ def capistrano_nomad_fetch_jobs_names_by_namespace
149
+ fetch(:nomad_jobs).transform_values(&:keys)
150
+ end
151
+
152
+ def capistrano_nomad_fetch_jobs_docker_image_types(names, namespace: nil)
153
+ names.map { |n| fetch(:nomad_jobs).dig(namespace, n.to_sym, :image_types) }.flatten.compact.uniq
154
+ end
155
+
156
+ def capistrano_nomad_build_jobs_docker_images(names, *args)
157
+ image_types = capistrano_nomad_fetch_jobs_docker_image_types(names, *args)
158
+
159
+ return false if image_types.empty?
160
+
161
+ image_types.each { |i| capistrano_nomad_build_docker_image_for_type(i, *args) }
162
+ end
163
+
164
+ def capistrano_nomad_push_jobs_docker_images(names, *args)
165
+ image_types = capistrano_nomad_fetch_jobs_docker_image_types(names, *args)
166
+
167
+ return false if image_types.empty?
168
+
169
+ image_types.each { |i| capistrano_nomad_push_docker_image_for_type(i, *args) }
170
+ end
171
+
172
+ def capistrano_nomad_assemble_jobs_docker_images(names, *args)
173
+ capistrano_nomad_build_jobs_docker_images(names, *args)
174
+ capistrano_nomad_push_jobs_docker_images(names, *args)
175
+ end
176
+
177
+ def capistrano_nomad_upload_jobs(names, *args)
178
+ # Var files can be shared between jobs so don't upload duplicates
179
+ uniq_var_files = names.map { |n| capistrano_nomad_fetch_job_var_files(n, *args) }.flatten.uniq
180
+
181
+ uniq_var_files.each do |var_file|
182
+ capistrano_nomad_upload_file(
183
+ local_path: capistrano_nomad_build_base_var_file_path(var_file, *args),
184
+ remote_path: capistrano_nomad_build_release_var_file_path(var_file, *args),
185
+ )
186
+ end
187
+
188
+ run_locally do
189
+ names.each do |name|
190
+ nomad_job_options = capistrano_nomad_fetch_job_options(name, *args)
191
+
192
+ # Can set job-specific ERB vars
193
+ erb_vars = nomad_job_options[:erb_vars] || {}
194
+
195
+ # Can set a custom template instead
196
+ file_basename = nomad_job_options[:template] || name
197
+
198
+ capistrano_nomad_upload_file(
199
+ local_path: capistrano_nomad_build_base_job_path(file_basename, *args),
200
+ remote_path: capistrano_nomad_build_release_job_path(name, *args),
201
+ erb_vars: erb_vars,
202
+ )
203
+ end
204
+ end
205
+ end
206
+
207
+ def capistrano_nomad_plan_jobs(names, *args)
208
+ names.each do |name|
209
+ args = [capistrano_nomad_build_release_job_path(name, *args)]
210
+
211
+ capistrano_nomad_execute_nomad_command(:plan, *args)
212
+ end
213
+ end
214
+
215
+ def capistrano_nomad_run_jobs(names, namespace: nil)
216
+ names.each do |name|
217
+ run_options = {
218
+ namespace: namespace,
219
+ detach: true,
220
+
221
+ # Don't reset counts since they may have been scaled
222
+ preserve_counts: true,
223
+ }
224
+
225
+ capistrano_nomad_fetch_job_var_files(name, namespace: namespace).each do |var_file|
226
+ run_options[:var_file] = capistrano_nomad_build_release_var_file_path(var_file, namespace: namespace)
227
+ end
228
+
229
+ capistrano_nomad_execute_nomad_command(
230
+ :run,
231
+ run_options,
232
+ capistrano_nomad_build_release_job_path(name, namespace: namespace),
233
+ )
234
+ end
235
+ end
236
+
237
+ # Remove job and run again
238
+ def capistrano_nomad_rerun_jobs(names)
239
+ names.each do |name|
240
+ # Wait for jobs to be purged before running again
241
+ capistrano_nomad_purge_jobs([name], is_detached: false)
242
+
243
+ capistrano_nomad_run_jobs([name])
244
+ end
245
+ end
246
+
247
+ def capistrano_nomad_upload_plan_jobs(names, *args)
248
+ capistrano_nomad_upload_jobs(names, *args)
249
+ capistrano_nomad_plan_jobs(names, *args)
250
+ end
251
+
252
+ def capistrano_nomad_upload_run_jobs(names, *args)
253
+ capistrano_nomad_upload_jobs(names, *args)
254
+ capistrano_nomad_run_jobs(names, *args)
255
+ end
256
+
257
+ def capistrano_nomad_upload_rerun_jobs(names, *args)
258
+ capistrano_nomad_upload_jobs(names, *args)
259
+ capistrano_nomad_rerun_jobs(names, *args)
260
+ end
261
+
262
+ def capistrano_nomad_deploy_jobs(names, *args)
263
+ capistrano_nomad_assemble_jobs_docker_images(names, *args)
264
+ capistrano_nomad_upload_run_jobs(names, *args)
265
+ end
266
+
267
+ def capistrano_nomad_purge_jobs(names, namespace: nil, is_detached: true)
268
+ names.each do |name|
269
+ capistrano_nomad_execute_nomad_command(:stop, { namespace: namespace, purge: true, detach: is_detached }, name)
270
+ end
271
+ end
272
+
273
+ def capistrano_nomad_display_job_status(name, *args)
274
+ capistrano_nomad_execute_nomad_command(:status, *args, name)
275
+ end
@@ -0,0 +1,25 @@
1
+ require "capistrano/scm/plugin"
2
+ require_relative "../helpers/git"
3
+
4
+ class Capistrano::SCM::GitLocal < Capistrano::SCM::Plugin
5
+ def define_tasks
6
+ namespace :git_local do
7
+ task :set_current_revision do
8
+ on release_roles :manager do
9
+ set :current_revision, capistrano_nomad_fetch_git_commit_id
10
+ end
11
+ end
12
+
13
+ task :create_release do
14
+ on release_roles :manager do
15
+ execute :mkdir, "-p", release_path
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ def register_hooks
22
+ before "deploy:set_current_revision", "git_local:set_current_revision"
23
+ after "deploy:new_release_path", "git_local:create_release"
24
+ end
25
+ end
@@ -0,0 +1,70 @@
1
+ namespace :nomad do
2
+ desc "Show version"
3
+ task :version do
4
+ capistrano_nomad_execute_nomad_command(:version)
5
+ end
6
+
7
+ namespace :docker_images do
8
+ desc "Used for adding hooks before or after pushing Docker images"
9
+ task :push
10
+ end
11
+
12
+ namespace :jobs do
13
+ desc "Build all job Docker images"
14
+ task :build do
15
+ capistrano_nomad_fetch_jobs_names_by_namespace.each do |namespace, names|
16
+ capistrano_nomad_push_jobs_docker_images(names, namespace: namespace)
17
+ end
18
+ end
19
+
20
+ desc "Push all job Docker images"
21
+ task :push do
22
+ capistrano_nomad_fetch_jobs_names_by_namespace.each do |namespace, names|
23
+ capistrano_nomad_push_jobs_docker_images(names, namespace: namespace)
24
+ end
25
+ end
26
+
27
+ desc "Build and push all job Docker images"
28
+ task :assemble do
29
+ capistrano_nomad_fetch_jobs_names_by_namespace.each do |namespace, names|
30
+ capistrano_nomad_assemble_jobs_docker_images(names, namespace: namespace)
31
+ end
32
+ end
33
+
34
+ desc "Upload and run all jobs"
35
+ task :upload_run do
36
+ capistrano_nomad_fetch_jobs_names_by_namespace.each do |namespace, names|
37
+ capistrano_nomad_upload_run_jobs(names, namespace: namespace)
38
+ end
39
+ end
40
+
41
+ desc "Deploy all jobs"
42
+ task :deploy do
43
+ capistrano_nomad_fetch_jobs_names_by_namespace.each do |namespace, names|
44
+ capistrano_nomad_deploy_jobs(names, namespace: namespace)
45
+ end
46
+ end
47
+
48
+ desc "Rerun all jobs"
49
+ task :rerun do
50
+ capistrano_nomad_fetch_jobs_names_by_namespace.each do |namespace, names|
51
+ capistrano_nomad_rerun_jobs(names, namespace: namespace)
52
+ end
53
+ end
54
+
55
+ desc "Purge all jobs"
56
+ task :purge do
57
+ capistrano_nomad_fetch_jobs_names_by_namespace.each do |namespace, names|
58
+ capistrano_nomad_purge_jobs(names, namespace: namespace)
59
+ end
60
+ end
61
+ end
62
+
63
+ namespace :system do
64
+ desc "Clean up Nomad"
65
+ task :clean do
66
+ capistrano_nomad_execute_nomad_command(:system, :gc)
67
+ capistrano_nomad_execute_nomad_command(:system, :reconcile, :summaries)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,6 @@
1
+ module Capistrano
2
+ module Nomad
3
+ VERSION = "0.1.0".freeze
4
+ end
5
+ end
6
+
@@ -0,0 +1,20 @@
1
+ require "capistrano/plugin"
2
+ require_relative "nomad/helpers/base"
3
+ require_relative "nomad/helpers/docker"
4
+ require_relative "nomad/helpers/dsl"
5
+ require_relative "nomad/helpers/git"
6
+ require_relative "nomad/helpers/nomad"
7
+
8
+ module Capistrano
9
+ class Nomad < Capistrano::Plugin
10
+ def set_defaults
11
+ set_if_empty(:nomad_jobs_path, "/nomad/jobs")
12
+ set_if_empty(:nomad_var_files_path, "/nomad/var_files")
13
+ set_if_empty(:nomad_docker_image_alias, ->(**) {})
14
+ end
15
+
16
+ def define_tasks
17
+ eval_rakefile(File.expand_path("nomad/tasks/nomad.rake", __dir__))
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ module Capistrano
2
+ module Nomad
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: capistrano-nomad
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - James Hu
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-10-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.8
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.8
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: capistrano
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 3.14.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 3.14.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: git
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.10.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 1.10.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '13.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '13.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sshkit
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 1.21.2
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 1.21.2
97
+ - !ruby/object:Gem::Dependency
98
+ name: sshkit-interactive
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: 0.3.0
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: 0.3.0
111
+ description: Capistrano plugin for deploying and managing Nomad jobs
112
+ email:
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - CHANGELOG.md
118
+ - Gemfile
119
+ - Gemfile.lock
120
+ - LICENSE.txt
121
+ - README.md
122
+ - Rakefile
123
+ - capistrano-nomad.gemspec
124
+ - lib/capistrano/nomad.rb
125
+ - lib/capistrano/nomad/helpers/base.rb
126
+ - lib/capistrano/nomad/helpers/docker.rb
127
+ - lib/capistrano/nomad/helpers/dsl.rb
128
+ - lib/capistrano/nomad/helpers/git.rb
129
+ - lib/capistrano/nomad/helpers/nomad.rb
130
+ - lib/capistrano/nomad/scm/git_local.rb
131
+ - lib/capistrano/nomad/tasks/nomad.rake
132
+ - lib/capistrano/nomad/version.rb
133
+ - sig/capistrano/nomad.rbs
134
+ homepage: https://github.com/axsuul/capistrano-nomad
135
+ licenses:
136
+ - MIT
137
+ metadata:
138
+ allowed_push_host: https://rubygems.org
139
+ homepage_uri: https://github.com/axsuul/capistrano-nomad
140
+ source_code_uri: https://github.com/axsuul/capistrano-nomad
141
+ post_install_message:
142
+ rdoc_options: []
143
+ require_paths:
144
+ - lib
145
+ required_ruby_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: 2.6.0
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ requirements: []
156
+ rubygems_version: 3.1.6
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: Capistrano plugin for deploying and managing Nomad jobs
160
+ test_files: []