Top Level Namespace

Defined Under Namespace

Modules: Capistrano Classes: CapistranoNomadDockerPushImageInteractionHandler, CapistranoNomadErbNamespace

Instance Method Summary collapse

Instance Method Details

#capistrano_nomad_assemble_jobs_docker_images(names, **options) ⇒ Object



418
419
420
421
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 418

def capistrano_nomad_assemble_jobs_docker_images(names, **options)
  capistrano_nomad_build_jobs_docker_images(names, **options)
  capistrano_nomad_push_jobs_docker_images(names, **options)
end

#capistrano_nomad_build_base_job_path(*args, **options) ⇒ Object



57
58
59
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 57

def capistrano_nomad_build_base_job_path(*args, **options)
  capistrano_nomad_build_file_path(fetch(:nomad_jobs_path), *args, **options)
end

#capistrano_nomad_build_base_var_file_path(*args, **options) ⇒ Object



61
62
63
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 61

def capistrano_nomad_build_base_var_file_path(*args, **options)
  capistrano_nomad_build_file_path(fetch(:nomad_var_files_path), *args, **options)
end

#capistrano_nomad_build_docker_image_alias(image_type) ⇒ Object

Raises:

  • (StandardError)


52
53
54
55
56
57
58
59
60
61
62
# File 'lib/capistrano/nomad/helpers/docker.rb', line 52

def capistrano_nomad_build_docker_image_alias(image_type)
  image_alias = fetch(:nomad_docker_image_types).dig(image_type, :alias)
  image_alias = image_alias.call(image_type: image_type) if image_alias&.is_a?(Proc)

  raise StandardError, ":alias not defined for #{image_type}" unless image_alias

  # Add :latest if there's no tag
  image_alias << ":latest" if image_alias.split(":").count == 1

  image_alias
end

#capistrano_nomad_build_docker_image_for_type(image_type) ⇒ Object

Builds docker image from image type

Parameters:

  • image_type (String, Symbol)


67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/capistrano/nomad/helpers/docker.rb', line 67

def capistrano_nomad_build_docker_image_for_type(image_type)
  image_type = image_type.to_sym
  attributes = fetch(:nomad_docker_image_types)[image_type]
  command = fetch(:nomad_docker_build_command) || "docker build"
  options = Array(fetch(:nomad_docker_build_command_options)) || []

  return unless attributes

  # No need to build if there's no path
  return unless attributes[:path]

  # Ensure images are built for x86_64 which is production env otherwise it will default to local development env which
  # can be arm64 (Apple Silicon)
  options << "--platform linux/amd64"

  if (target = attributes[:target])
    options << "--target #{target}"
  end

  build_args = attributes[:build_args]
  build_args = build_args.call if build_args&.is_a?(Proc)

  (build_args || []).each do |key, value|
    # Escape single quotes so that we can properly pass in build arg values that have spaces and special characters
    # e.g. Don't escape strings (#123) => 'Don'\''t escape strings (#123)'
    value_escaped = value.gsub("'", "\'\\\\'\'")
    options << "--build-arg #{key}='#{value_escaped}'"
  end

  docker_build_command = lambda do |path|
    build_options = options.dup

    [capistrano_nomad_build_docker_image_alias(image_type)]
      .compact
      .each do |tag|
        build_options << "--tag #{tag}"
      end

    "#{command} #{build_options.join(' ')} #{path}"
  end

  case attributes[:strategy]

  # We need to build Docker container locally
  when :local_build, :local_push
    run_locally do
      # If any of these files exist then we're in the middle of rebase so we should interrupt
      if ["rebase-merge", "rebase-apply"].any? { |f| File.exist?("#{capistrano_nomad_git.dir.path}/.git/#{f}") }
        raise StandardError, "Still in the middle of git rebase, interrupting docker image build"
      end

      execute(docker_build_command.call(capistrano_nomad_root.join(attributes[:path])))
    end

  # We need to build Docker container remotely
  when :remote_build, :remote_push
    remote_path = Pathname.new(release_path).join(attributes[:path])
    capistrano_nomad_upload(local_path: attributes[:path], remote_path: remote_path)

    capistrano_nomad_run_remotely do
      execute(docker_build_command.call(remote_path))
    end
  end
end

#capistrano_nomad_build_file_path(parent_path, basename, kind: nil, **options) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 32

def capistrano_nomad_build_file_path(parent_path, basename, kind: nil, **options)
  capistrano_nomad_ensure_options!(options)
  namespace = options[:namespace]
  segments = [parent_path]

  unless namespace == :default
    case kind

    # Always upload to namespace folder on remote
    when :release
      segments << namespace

    # Otherwise path can be overriden of where files belonging to namespace are stored locally
    else
      namespace_options = capistrano_nomad_fetch_namespace_options(namespace: namespace)

      segments << (namespace_options[:path] || namespace)
    end
  end

  segments << "#{basename}.hcl"

  segments.join("/")
end

#capistrano_nomad_build_jobs_docker_images(names, **options) ⇒ Object



402
403
404
405
406
407
408
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 402

def capistrano_nomad_build_jobs_docker_images(names, **options)
  image_types = capistrano_nomad_fetch_jobs_docker_image_types(names, **options)

  return false if image_types.empty?

  image_types.each { |i| capistrano_nomad_build_docker_image_for_type(i) }
end

#capistrano_nomad_build_local_job_path(name, **options) ⇒ Object



76
77
78
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 76

def capistrano_nomad_build_local_job_path(name, **options)
  capistrano_nomad_build_local_path(capistrano_nomad_build_base_job_path(name, **options))
end

#capistrano_nomad_build_local_path(path) ⇒ Object

Raises:

  • (StandardError)


65
66
67
68
69
70
71
72
73
74
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 65

def capistrano_nomad_build_local_path(path)
  local_path = capistrano_nomad_root.join(path)

  # Determine if it has .erb appended or not
  found_local_path = [local_path, "#{local_path}.erb"].find { |each_local_path| File.exist?(each_local_path) }

  raise StandardError, "Could not find local path: #{path}" unless found_local_path

  found_local_path
end

#capistrano_nomad_build_local_var_file_path(name, **options) ⇒ Object



80
81
82
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 80

def capistrano_nomad_build_local_var_file_path(name, **options)
  capistrano_nomad_build_local_path(capistrano_nomad_build_base_var_file_path(name, **options))
end

#capistrano_nomad_build_release_job_path(name, **options) ⇒ Object



84
85
86
87
88
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 84

def capistrano_nomad_build_release_job_path(name, **options)
  options[:kind] = :release

  "#{release_path}#{capistrano_nomad_ensure_absolute_path(capistrano_nomad_build_base_job_path(name, **options))}"
end

#capistrano_nomad_build_release_var_file_path(name, **options) ⇒ Object



90
91
92
93
94
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 90

def capistrano_nomad_build_release_var_file_path(name, **options)
  options[:kind] = :release

  "#{release_path}#{capistrano_nomad_ensure_absolute_path(capistrano_nomad_build_base_var_file_path(name, **options))}"
end

#capistrano_nomad_capture_nomad_command(*args, **options) ⇒ Object



127
128
129
130
131
132
133
134
135
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 127

def capistrano_nomad_capture_nomad_command(*args, **options)
  output = nil

  capistrano_nomad_run_remotely do
    output = capistrano_nomad_run_nomad_command(:capture, *args, **options)
  end

  output
end

#capistrano_nomad_deep_symbolize_hash_keys(hash) ⇒ Object



5
6
7
# File 'lib/capistrano/nomad/helpers/base.rb', line 5

def capistrano_nomad_deep_symbolize_hash_keys(hash)
  JSON.parse(JSON[hash], symbolize_names: true)
end

#capistrano_nomad_define_group_tasks(namespace:) ⇒ Object



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 306

def capistrano_nomad_define_group_tasks(namespace:)
  define_tasks = lambda do |nomad_namespace: nil|
    desc("Build #{nomad_namespace} job Docker images")
    task(:build) do
      capistrano_nomad_fetch_jobs_names_by_namespace(namespace: nomad_namespace).each do |jobs_namespace, names|
        capistrano_nomad_push_jobs_docker_images(names, namespace: jobs_namespace)
      end
    end

    desc("Push #{nomad_namespace} job Docker images")
    task(:push) do
      capistrano_nomad_fetch_jobs_names_by_namespace(namespace: nomad_namespace).each do |jobs_namespace, names|
        capistrano_nomad_push_jobs_docker_images(names, namespace: jobs_namespace)
      end
    end

    desc("Build and push #{nomad_namespace} job Docker images")
    task(:assemble) do
      capistrano_nomad_fetch_jobs_names_by_namespace(namespace: nomad_namespace).each do |jobs_namespace, names|
        capistrano_nomad_assemble_jobs_docker_images(names, namespace: jobs_namespace)
      end
    end

    desc("Upload #{nomad_namespace} jobs")
    task(:upload) do
      capistrano_nomad_fetch_jobs_names_by_namespace(namespace: nomad_namespace).each do |jobs_namespace, names|
        capistrano_nomad_upload_jobs(names, namespace: jobs_namespace)
      end
    end

    desc("Run #{nomad_namespace} jobs")
    task(:run) do
      capistrano_nomad_fetch_jobs_names_by_namespace(namespace: nomad_namespace).each do |jobs_namespace, names|
        capistrano_nomad_run_jobs(names, namespace: jobs_namespace)
      end
    end

    desc("Upload and run #{nomad_namespace} jobs")
    task(:upload_run) do
      capistrano_nomad_fetch_jobs_names_by_namespace(namespace: nomad_namespace).each do |jobs_namespace, names|
        capistrano_nomad_upload_run_jobs(names, namespace: jobs_namespace)
      end
    end

    desc("Deploy #{nomad_namespace} jobs")
    task(:deploy) do
      capistrano_nomad_fetch_jobs_names_by_namespace(namespace: nomad_namespace).each do |jobs_namespace, names|
        capistrano_nomad_deploy_jobs(names, namespace: jobs_namespace)
      end
    end

    desc("Rerun #{nomad_namespace} jobs")
    task(:rerun) do
      capistrano_nomad_fetch_jobs_names_by_namespace(namespace: nomad_namespace).each do |jobs_namespace, names|
        capistrano_nomad_rerun_jobs(names, namespace: jobs_namespace)
      end
    end

    desc("Restart #{nomad_namespace} jobs")
    task(:restart) do
      capistrano_nomad_fetch_jobs_names_by_namespace(namespace: nomad_namespace).each do |jobs_namespace, names|
        capistrano_nomad_restart_jobs(names, namespace: jobs_namespace)
      end
    end

    desc("Revert #{nomad_namespace} jobs")
    task(:revert) do
      capistrano_nomad_fetch_jobs_names_by_namespace(namespace: nomad_namespace).each do |jobs_namespace, names|
        capistrano_nomad_revert_jobs(names, namespace: jobs_namespace)
      end
    end

    desc("Stop #{nomad_namespace} jobs")
    task(:stop) do
      capistrano_nomad_fetch_jobs_names_by_namespace(namespace: nomad_namespace).each do |jobs_namespace, names|
        capistrano_nomad_stop_jobs(names, namespace: jobs_namespace)
      end
    end

    desc("Purge #{nomad_namespace} jobs")
    task(:purge) do
      capistrano_nomad_fetch_jobs_names_by_namespace(namespace: nomad_namespace).each do |jobs_namespace, names|
        capistrano_nomad_purge_jobs(names, namespace: jobs_namespace)
      end
    end
  end

  if namespace
    namespace(namespace) do
      define_tasks.call(nomad_namespace: namespace)
    end
  else
    define_tasks.call
  end
end

#capistrano_nomad_deploy_jobs(names, **options) ⇒ Object



516
517
518
519
520
521
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 516

def capistrano_nomad_deploy_jobs(names, **options)
  general_options = options.slice!(:is_detached)

  capistrano_nomad_assemble_jobs_docker_images(names, **general_options)
  capistrano_nomad_upload_run_jobs(names, **general_options.merge(options))
end

#capistrano_nomad_display_job_history(name, **options) ⇒ Object



585
586
587
588
589
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 585

def capistrano_nomad_display_job_history(name, **options)
  capistrano_nomad_ensure_options!(options)

  capistrano_nomad_capture_nomad_command(:job, :history, options, name)
end

#capistrano_nomad_display_job_logs(name, **options) ⇒ Object



597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 597

def capistrano_nomad_display_job_logs(name, **options)
  if (task_details = capistrano_nomad_find_job_task_details(name, **options.slice(:namespace).reverse_merge(task: ENV["TASK"])
  ))
    capistrano_nomad_execute_nomad_command(
      :alloc,
      :logs,
      options.reverse_merge(task: task_details[:name]),
      task_details[:alloc_id],
    )
  else
    # If task can't be determined choose a random allocation
    capistrano_nomad_execute_nomad_command(
      :alloc,
      :logs,
      options.reverse_merge(job: true),
      name,
    )
  end
end

#capistrano_nomad_display_job_status(name, **options) ⇒ Object



591
592
593
594
595
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 591

def capistrano_nomad_display_job_status(name, **options)
  capistrano_nomad_ensure_options!(options)

  capistrano_nomad_execute_nomad_command(:status, options, name)
end

#capistrano_nomad_docker_image_types_manifest_pathObject



17
18
19
# File 'lib/capistrano/nomad/helpers/docker.rb', line 17

def capistrano_nomad_docker_image_types_manifest_path
  shared_path.join("docker-image-types.json")
end

#capistrano_nomad_ensure_absolute_path(path) ⇒ Object



28
29
30
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 28

def capistrano_nomad_ensure_absolute_path(path)
  path[0] == "/" ? path : "/#{path}"
end

#capistrano_nomad_ensure_options!(options) ⇒ Object



254
255
256
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 254

def capistrano_nomad_ensure_options!(options)
  options[:namespace] ||= :default
end

#capistrano_nomad_exec_within_job(name, command, task: nil, **options) ⇒ Object



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 180

def capistrano_nomad_exec_within_job(name, command, task: nil, **options)
  capistrano_nomad_ensure_options!(options)

  capistrano_nomad_run_remotely do
    if (task_details = capistrano_nomad_find_job_task_details(name, task: task, **options))
      capistrano_nomad_execute_nomad_command(
        :alloc,
        :exec,
        options.merge(task: task_details[:name]),
        task_details[:alloc_id],
        command,
      )
    else
      # If alloc can't be determined then choose at random
      capistrano_nomad_execute_nomad_command(
        :alloc,
        :exec,
        options.merge(job: true),
        task,
        command,
      )
    end
  end
end

#capistrano_nomad_execute_nomad_command(*args, **options) ⇒ Object



119
120
121
122
123
124
125
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 119

def capistrano_nomad_execute_nomad_command(*args, **options)
  capistrano_nomad_run_remotely do |host|
    run_interactively(host) do
      capistrano_nomad_run_nomad_command(:execute, *args, **options)
    end
  end
end

#capistrano_nomad_fetch_job_options(name, *args, **options) ⇒ Object



264
265
266
267
268
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 264

def capistrano_nomad_fetch_job_options(name, *args, **options)
  capistrano_nomad_ensure_options!(options)

  fetch(:nomad_jobs).dig(options[:namespace], name.to_sym, *args)
end

#capistrano_nomad_fetch_job_var_files(name, **options) ⇒ Object



270
271
272
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 270

def capistrano_nomad_fetch_job_var_files(name, **options)
  capistrano_nomad_fetch_job_options(name, :var_files, **options) || []
end

#capistrano_nomad_fetch_jobs_docker_image_types(names, **options) ⇒ Object



300
301
302
303
304
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 300

def capistrano_nomad_fetch_jobs_docker_image_types(names, **options)
  capistrano_nomad_ensure_options!(options)

  names.map { |n| fetch(:nomad_jobs).dig(options[:namespace], n.to_sym, :docker_image_types) }.flatten.compact.uniq
end

#capistrano_nomad_fetch_jobs_names_by_namespace(**options) ⇒ Object



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 274

def capistrano_nomad_fetch_jobs_names_by_namespace(**options)
  capistrano_nomad_ensure_options!(options)
  namespace = options[:namespace]

  # Can pass tags via command line (e.g. TAG=foo or TAGS=foo,bar)
  tags =
    [ENV["TAG"], ENV["TAGS"]].map do |tag_args|
      next unless tag_args.presence

      tag_args.split(",").map(&:presence).compact.map(&:to_sym)
    end
      .flatten
      .compact

  fetch(:nomad_jobs).each_with_object({}) do |(jobs_namespace, jobs_options), hash|
    next if !namespace.nil? && namespace != jobs_namespace

    hash[jobs_namespace] = jobs_options.each_with_object([]) do |(job_name, job_options), collection|
      # Filter jobs by tags if specified
      next if tags.any? && ((job_options[:tags]&.map(&:to_sym) || []) & tags).empty?

      collection << job_name
    end
  end
end

#capistrano_nomad_fetch_namespace_options(**options) ⇒ Object



258
259
260
261
262
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 258

def capistrano_nomad_fetch_namespace_options(**options)
  capistrano_nomad_ensure_options!(options)

  fetch(:nomad_namespaces)&.dig(options[:namespace])
end

#capistrano_nomad_find_job_task_details(name, task: nil, **options) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 137

def capistrano_nomad_find_job_task_details(name, task: nil, **options)
  capistrano_nomad_ensure_options!(options)
  task = task.presence || name

  # Find alloc id that contains task that is also running
  allocs_output = capistrano_nomad_capture_nomad_command(
    :job,
    :allocs,
    options.merge(t: "'{{range .}}{{ .ID }},{{ .ClientStatus }},{{ .TaskGroup }}|{{end}}'"),
    name,
  )
  alloc_id = allocs_output
    .split("|")
    .map { |s| s.split(",") }
    .find { |_, s, t| s == "running" && t == task.to_s }
    &.first

  # Can't continue if we can't choose an alloc id
  return unless alloc_id

  tasks_output = capistrano_nomad_capture_nomad_command(
    :alloc,
    :status,
    options.merge(t: "'{{range $key, $value := .TaskStates}}{{ $key }},{{ .State }}|{{end}}'"),
    alloc_id,
  )
  tasks_by_score = tasks_output.split("|").each_with_object({}) do |task_output, hash|
    task, state = task_output.split(",")

    score = 0
    score += 5 if state == "running"
    score += 5 unless task.match?(/connect-proxy/)

    hash[task] = score
  end
  task = tasks_by_score.max_by { |_, v| v }.first

  {
    alloc_id: alloc_id,
    name: task,
  }
end

#capistrano_nomad_gitObject



3
4
5
# File 'lib/capistrano/nomad/helpers/git.rb', line 3

def capistrano_nomad_git
  @capistrano_nomad_git ||= Git.open(".")
end

#capistrano_nomad_git_commitObject



7
8
9
# File 'lib/capistrano/nomad/helpers/git.rb', line 7

def capistrano_nomad_git_commit
  @capistrano_nomad_git_commit ||= capistrano_nomad_git.log.first
end

#capistrano_nomad_git_commit_idObject



11
12
13
# File 'lib/capistrano/nomad/helpers/git.rb', line 11

def capistrano_nomad_git_commit_id
  @capistrano_nomad_git_commit_id ||= capistrano_nomad_git_commit.sha
end

#capistrano_nomad_open_job_ui(name, namespace: nil) ⇒ Object



621
622
623
624
625
626
627
628
629
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 621

def capistrano_nomad_open_job_ui(name, namespace: nil)
  run_locally do
    url = "#{fetch(:nomad_ui_url)}/ui/jobs/#{name}"
    url += "@#{namespace}" if namespace

    # Only macOS supported for now
    execute(:open, url)
  end
end

#capistrano_nomad_plan_jobs(names, **options) ⇒ Object



453
454
455
456
457
458
459
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 453

def capistrano_nomad_plan_jobs(names, **options)
  names.each do |name|
    args = [capistrano_nomad_build_release_job_path(name, **options)]

    capistrano_nomad_execute_nomad_command(:plan, *args)
  end
end

#capistrano_nomad_purge_jobs(names, is_detached: true, **options) ⇒ Object



541
542
543
544
545
546
547
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 541

def capistrano_nomad_purge_jobs(names, is_detached: true, **options)
  capistrano_nomad_ensure_options!(options)

  names.each do |name|
    capistrano_nomad_execute_nomad_command(:stop, options.reverse_merge(purge: true, detach: is_detached), name)
  end
end

#capistrano_nomad_push_docker_image_for_type(image_type, is_manifest_updated: true) ⇒ Object



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/capistrano/nomad/helpers/docker.rb', line 132

def capistrano_nomad_push_docker_image_for_type(image_type, is_manifest_updated: true)
  attributes = fetch(:nomad_docker_image_types)[image_type]
  alias_digest = attributes&.dig(:alias_digest)

  return false unless [:local_push, :remote_push].include?(attributes[:strategy])

  run_locally do
    # Only push Docker image if it was built from path
    if attributes[:path]
      interaction_handler = CapistranoNomadDockerPushImageInteractionHandler.new
      image_alias = capistrano_nomad_build_docker_image_alias(image_type)

      # We should not proceed if image cannot be pushed
      unless execute("docker push #{image_alias}", interaction_handler: interaction_handler)
        raise StandardError, "Docker image push unsuccessful!"
      end

      return unless is_manifest_updated

      # Has the @sha256:xxxx appended so we have the ability to also target by digest
      alias_digest = "#{image_alias}@#{interaction_handler.last_digest}"
    end

    # Update image type manifest
    capistrano_nomad_update_docker_image_types_manifest(image_type,
      alias: image_alias,
      alias_digest: alias_digest,
    )
  end
end

#capistrano_nomad_push_jobs_docker_images(names, **options) ⇒ Object



410
411
412
413
414
415
416
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 410

def capistrano_nomad_push_jobs_docker_images(names, **options)
  image_types = capistrano_nomad_fetch_jobs_docker_image_types(names, **options)

  return false if image_types.empty?

  image_types.each { |i| capistrano_nomad_push_docker_image_for_type(i) }
end

#capistrano_nomad_read_docker_image_types_manifestObject



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/capistrano/nomad/helpers/docker.rb', line 21

def capistrano_nomad_read_docker_image_types_manifest
  manifest = {}

  capistrano_nomad_run_remotely do
    # Ensure file exists
    execute("mkdir", "-p", shared_path)
    execute("touch", capistrano_nomad_docker_image_types_manifest_path)

    output = capture("cat #{capistrano_nomad_docker_image_types_manifest_path}")

    unless output.blank?
      manifest = JSON.parse(output)
    end
  end

  capistrano_nomad_deep_symbolize_hash_keys(manifest)
end

#capistrano_nomad_rerun_jobs(names, **options) ⇒ Object

Remove job and run again



486
487
488
489
490
491
492
493
494
495
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 486

def capistrano_nomad_rerun_jobs(names, **options)
  general_options = options.slice!(:is_detached)

  names.each do |name|
    # Wait for jobs to be purged before running again
    capistrano_nomad_purge_jobs([name], **general_options.merge(is_detached: false))

    capistrano_nomad_run_jobs([name], **general_options.merge(options))
  end
end

#capistrano_nomad_restart_jobs(names, **options) ⇒ Object



523
524
525
526
527
528
529
530
531
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 523

def capistrano_nomad_restart_jobs(names, **options)
  capistrano_nomad_ensure_options!(options)

  names.each do |name|
    # Automatic yes to prompts. If set, the command automatically restarts multi-region jobs only in the region targeted
    # by the command, ignores batch errors, and automatically proceeds with the remaining batches without waiting
    capistrano_nomad_execute_nomad_command(:job, :restart, options.reverse_merge(yes: true), name)
  end
end

#capistrano_nomad_revert_jobs(names, version: nil, docker_image: nil, **options) ⇒ Object



549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 549

def capistrano_nomad_revert_jobs(names, version: nil, docker_image: nil, **options)
  capistrano_nomad_ensure_options!(options)
  versions_by_job_name = {}

  names.each do |name|
    history_output_json = capistrano_nomad_display_job_history(name, **options.reverse_merge(json: true))
    history_output = JSON.parse(history_output_json)
    versions_by_job_name[name] = if version.presence
      version
    elsif docker_image.presence
      # Find job history with matching docker image
      docker_image_job_history = history_output.find do |job_history|
        task_images = job_history.dig("TaskGroups")
          .map { |g| g.dig("Tasks").map { |t| t.dig("Config", "image") } }
          .flatten
          .compact

        task_images.any? { |image| image.include?(docker_image) }
      end

      unless docker_image_job_history
        raise ArgumentError, "No job history found for job #{name} with docker image: #{docker_image}"
      end

      docker_image_job_history.dig("Version")
    # Revert to previous version if nothing specified
    else
      history_output[1].dig("Version")
    end
  end

  versions_by_job_name.each do |name, version|
    capistrano_nomad_execute_nomad_command(:job, :revert, options, name, version)
  end
end

#capistrano_nomad_rootObject



1
2
3
# File 'lib/capistrano/nomad/helpers/base.rb', line 1

def capistrano_nomad_root
  @capistrano_nomad_root ||= Pathname.new(fetch(:root) || "")
end

#capistrano_nomad_run_jobs(names, is_detached: true, **options) ⇒ Object



461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 461

def capistrano_nomad_run_jobs(names, is_detached: true, **options)
  capistrano_nomad_ensure_options!(options)

  names.each do |name|
    run_options = {
      namespace: options[:namespace],
      detach: is_detached,

      # Don't reset counts since they may have been scaled
      preserve_counts: true,
    }

    capistrano_nomad_fetch_job_var_files(name, **options).each do |var_file|
      run_options[:var_file] = capistrano_nomad_build_release_var_file_path(var_file, **options)
    end

    capistrano_nomad_execute_nomad_command(
      :run,
      run_options,
      capistrano_nomad_build_release_job_path(name, **options),
    )
  end
end

#capistrano_nomad_run_nomad_command(kind, *args) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 96

def capistrano_nomad_run_nomad_command(kind, *args)
  converted_args = args.each_with_object([]) do |arg, collection|
    # If hash then convert it as options
    if arg.is_a?(Hash)
      arg.each do |key, value|
        next unless value

        option = "-#{key.to_s.dasherize}"

        # Doesn't need a value if it's just meant to be a flag
        option << "=#{value}" unless value == true

        collection << option
      end
    else
      collection << arg
    end
  end

  # Ignore errors
  public_send(kind, :nomad, *converted_args, raise_on_non_zero_exit: false)
end

#capistrano_nomad_run_remotely(&block) ⇒ Object



9
10
11
# File 'lib/capistrano/nomad/helpers/base.rb', line 9

def capistrano_nomad_run_remotely(&block)
  on(roles(:manager), &block)
end

#capistrano_nomad_stop_jobs(names, **options) ⇒ Object



533
534
535
536
537
538
539
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 533

def capistrano_nomad_stop_jobs(names, **options)
  capistrano_nomad_ensure_options!(options)

  names.each do |name|
    capistrano_nomad_execute_nomad_command(:job, :stop, options, name)
  end
end

#capistrano_nomad_tail_job_logs(*args, **options) ⇒ Object



617
618
619
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 617

def capistrano_nomad_tail_job_logs(*args, **options)
  capistrano_nomad_display_job_logs(*args, **options.merge(tail: true, n: 50))
end

#capistrano_nomad_update_docker_image_types_manifest(image_type, properties = {}) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/capistrano/nomad/helpers/docker.rb', line 39

def capistrano_nomad_update_docker_image_types_manifest(image_type, properties = {})
  capistrano_nomad_run_remotely do
    # Read and update manifest
    manifest = capistrano_nomad_read_docker_image_types_manifest
    manifest[image_type] = (manifest[image_type] || {}).merge(properties.stringify_keys)

    io = StringIO.new(JSON.pretty_generate(manifest))

    # Write to manifest
    upload!(io, capistrano_nomad_docker_image_types_manifest_path)
  end
end

#capistrano_nomad_upload(local_path:, remote_path:, erb_vars: {}) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 205

def capistrano_nomad_upload(local_path:, remote_path:, erb_vars: {})
  # If directory upload everything within the directory
  if File.directory?(local_path)
    Dir.glob("#{local_path}/*").each do |path|
      capistrano_nomad_upload(local_path: path, remote_path: "#{remote_path}/#{File.basename(path)}")
    end

  # If file, attempt to always parse it as ERB
  else
    docker_image_types = fetch(:nomad_docker_image_types)
    docker_image_types_manifest = capistrano_nomad_read_docker_image_types_manifest

    # Merge manifest into image types
    docker_image_types_manifest.each do |manifest_image_type, manifest_attributes|
      docker_image_types[manifest_image_type]&.merge!(manifest_attributes) || {}
    end

    # Parse manifest files using ERB
    erb = ERB.new(File.open(local_path).read, trim_mode: "-")

    final_erb_vars = {
      git_commit_id: fetch(:current_revision) || capistrano_nomad_git_commit_id,
      docker_image_types: docker_image_types,
    }

    # Add global ERB vars
    final_erb_vars.merge!(fetch(:nomad_template_vars) || {})

    # Add job-specific ERB vars
    final_erb_vars.merge!(erb_vars)

    # We use a custom namespace class so that we can include helper methods into the namespace to make them available
    # for template to access
    namespace = CapistranoNomadErbNamespace.new(
      context: self,
      vars: final_erb_vars,
    )

    string_io = StringIO.new(erb.result(namespace.instance_eval { binding }))

    capistrano_nomad_run_remotely do
      # Ensure parent directory exists
      execute(:mkdir, "-p", File.dirname(remote_path))

      upload!(string_io, remote_path)
    end
  end
end

#capistrano_nomad_upload_jobs(names, **options) ⇒ Object



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 423

def capistrano_nomad_upload_jobs(names, **options)
  # Var files can be shared between jobs so don't upload duplicates
  uniq_var_files = names.map { |n| capistrano_nomad_fetch_job_var_files(n, **options) }.flatten.uniq

  uniq_var_files.each do |var_file|
    capistrano_nomad_upload(
      local_path: capistrano_nomad_build_local_var_file_path(var_file, **options),
      remote_path: capistrano_nomad_build_release_var_file_path(var_file, **options),
    )
  end

  run_locally do
    names.each do |name|
      nomad_job_options = capistrano_nomad_fetch_job_options(name, **options)

      # Can set job-specific ERB vars
      erb_vars = nomad_job_options[:erb_vars] || {}

      # Can set a custom template instead
      file_basename = nomad_job_options[:template] || name

      capistrano_nomad_upload(
        local_path: capistrano_nomad_build_local_job_path(file_basename, **options),
        remote_path: capistrano_nomad_build_release_job_path(name, **options),
        erb_vars: erb_vars,
      )
    end
  end
end

#capistrano_nomad_upload_plan_jobs(names, **options) ⇒ Object



497
498
499
500
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 497

def capistrano_nomad_upload_plan_jobs(names, **options)
  capistrano_nomad_upload_jobs(names, **options)
  capistrano_nomad_plan_jobs(names, **options)
end

#capistrano_nomad_upload_rerun_jobs(names, **options) ⇒ Object



509
510
511
512
513
514
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 509

def capistrano_nomad_upload_rerun_jobs(names, **options)
  general_options = options.slice!(:is_detached)

  capistrano_nomad_upload_jobs(names, **general_options)
  capistrano_nomad_rerun_jobs(names, **general_options.merge(options))
end

#capistrano_nomad_upload_run_jobs(names, **options) ⇒ Object



502
503
504
505
506
507
# File 'lib/capistrano/nomad/helpers/nomad.rb', line 502

def capistrano_nomad_upload_run_jobs(names, **options)
  general_options = options.slice!(:is_detached)

  capistrano_nomad_upload_jobs(names, **general_options)
  capistrano_nomad_run_jobs(names, **general_options.merge(options))
end

#nomad_docker_image_type(image_type, attributes = {}) ⇒ Object

Raises:

  • (ArgumentError)


3
4
5
6
7
8
9
10
11
12
13
14
15
16
# File 'lib/capistrano/nomad/helpers/dsl.rb', line 3

def nomad_docker_image_type(image_type, attributes = {})
  docker_image_types = fetch(:nomad_docker_image_types) || {}
  docker_image_types[image_type] = attributes.reverse_merge(
    # By default build and push Docker image locally
    strategy: :local_push,
  )

  raise ArgumentError, "passing in alias_digest is not allowed!" if attributes[:alias_digest]

  # If Docker image doesn't get pushed, this will still be populated
  docker_image_types[image_type][:alias_digest] = attributes[:alias]

  set(:nomad_docker_image_types, docker_image_types)
end

#nomad_job(name, attributes = {}) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/capistrano/nomad/helpers/dsl.rb', line 40

def nomad_job(name, attributes = {})
  # This is the namespace when there's no namespace defined in Nomad too
  @nomad_namespace ||= :default

  attributes[:tags] ||= []

  # Tags added to namespace should be added to all jobs within
  if (nomad_namespace_options = capistrano_nomad_fetch_namespace_options(namespace: @nomad_namespace))
    attributes[:tags] += nomad_namespace_options[:tags] || []
  end

  nomad_jobs = fetch(:nomad_jobs) || Hash.new { |h, n| h[n] = {} }
  nomad_jobs[@nomad_namespace][name] = attributes

  set(:nomad_jobs, nomad_jobs)

  define_tasks = lambda do |namespace: nil|
    description_name = ""
    description_name << "#{namespace}/" if namespace != :default
    description_name << name.to_s

    namespace(name) do
      desc("Build #{description_name} job Docker images")
      task(:build) do
        capistrano_nomad_build_jobs_docker_images([name], namespace: namespace)
      end

      desc("Push #{description_name} job Docker images")
      task(:push) do
        capistrano_nomad_push_jobs_docker_images([name], namespace: namespace)
      end

      desc("Build and push #{description_name} job Docker images")
      task(:assemble) do
        capistrano_nomad_build_jobs_docker_images([name], namespace: namespace)
        capistrano_nomad_push_jobs_docker_images([name], namespace: namespace)
      end

      desc("Upload #{description_name} job and related files")
      task(:upload) do
        capistrano_nomad_upload_jobs([name], namespace: namespace)
      end

      desc("Run #{description_name} job")
      task(:run) do
        capistrano_nomad_run_jobs([name], namespace: namespace, is_detached: false)
      end

      desc("Purge and run #{description_name} job again")
      task(:rerun) do
        capistrano_nomad_rerun_jobs([name], namespace: namespace, is_detached: false)
      end

      desc("Upload and plan #{description_name} job")
      task(:upload_plan) do
        capistrano_nomad_upload_plan_jobs([name], namespace: namespace)
      end

      desc("Upload and run #{description_name} job")
      task(:upload_run) do
        capistrano_nomad_upload_run_jobs([name], namespace: namespace, is_detached: false)
      end

      desc("Upload and re-run #{description_name} job")
      task(:upload_rerun) do
        capistrano_nomad_upload_rerun_jobs([name], namespace: namespace, is_detached: false)
      end

      desc("Deploy #{description_name} job")
      task(:deploy) do
        capistrano_nomad_deploy_jobs([name], namespace: namespace, is_detached: false)
      end

      desc("Stop #{description_name} job")
      task(:stop) do
        capistrano_nomad_stop_jobs([name], namespace: namespace)
      end

      desc("Restart #{description_name} job")
      task(:restart) do
        capistrano_nomad_restart_jobs([name], namespace: namespace)
      end

      desc("Revert #{description_name} job. Specify version with VERSION. Specify targeting tasks with docker image with DOCKER_IMAGE. If none specified, it will revert to previous version")
      task(:revert) do
        capistrano_nomad_revert_jobs([name],
          namespace: namespace,
          version: ENV["VERSION"],
          docker_image: ENV["DOCKER_IMAGE"],
        )
      end

      desc("Purge #{description_name} job")
      task(:purge) do
        capistrano_nomad_purge_jobs([name], namespace: namespace, is_detached: false)
      end

      desc("Display status of #{description_name} job")
      task(:status) do
        capistrano_nomad_display_job_status(name, namespace: namespace)
      end

      desc("Open console to #{description_name} job. Specify task with TASK, command with CMD")
      task(:console) do
        job_options = capistrano_nomad_fetch_job_options(name, namespace: namespace)
        command = ENV["COMMAND"].presence || ENV["CMD"].presence || job_options[:console_command] || "/bin/sh"

        capistrano_nomad_exec_within_job(name, command, namespace: namespace, task: ENV["TASK"])
      end

      desc("Display stdout and stderr of #{description_name} job. Specify task with TASK")
      task(:logs) do
        capistrano_nomad_tail_job_logs(name, namespace: namespace, stdout: true)
        capistrano_nomad_tail_job_logs(name, namespace: namespace, stderr: true)
      end

      desc("Display stdout of #{description_name} job. Specify task with TASK")
      task(:stdout) do
        capistrano_nomad_tail_job_logs(name, namespace: namespace, stdout: true)
      end

      desc("Display stderr of #{description_name} job. Specify task with TASK")
      task(:stderr) do
        capistrano_nomad_tail_job_logs(name, namespace: namespace, stderr: true)
      end

      desc("Follow logs of #{description_name} job. Specify task with TASK")
      task(:follow) do
        capistrano_nomad_display_job_logs(name, namespace: namespace, f: true)
      end

      desc("Open job in web UI")
      task(:ui) do
        capistrano_nomad_open_job_ui(name, namespace: namespace)
      end
    end
  end

  namespace(:nomad) do
    if @nomad_namespace
      # Also define tasks without namespace for default Nomad namespace
      define_tasks.call(namespace: @nomad_namespace) if @nomad_namespace == :default

      namespace(@nomad_namespace) do
        define_tasks.call(namespace: @nomad_namespace)
      end
    else
      define_tasks.call
    end
  end
end

#nomad_namespace(namespace, **options, &block) ⇒ Object

Raises:

  • (ArgumentError)


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/capistrano/nomad/helpers/dsl.rb', line 18

def nomad_namespace(namespace, **options, &block)
  raise ArgumentError, "cannot define default nomad namespace" if namespace == :default

  nomad_namespaces = fetch(:nomad_namespaces) || {}
  nomad_namespaces[namespace] = options
  set(:nomad_namespaces, nomad_namespaces)

  # Make namespace active for block
  @nomad_namespace = namespace

  instance_eval(&block)

  @nomad_namespace = nil

  # Define tasks for namespace jobs
  namespace(:nomad) do
    capistrano_nomad_define_group_tasks(namespace: namespace)
  end

  true
end

#nomad_template_helpers(&block) ⇒ Object



192
193
194
# File 'lib/capistrano/nomad/helpers/dsl.rb', line 192

def nomad_template_helpers(&block)
  CapistranoNomadErbNamespace.class_eval(&block)
end