Class: Odysseus::Deployer::Executor

Inherits:
Object
  • Object
show all
Defined in:
lib/odysseus/deployer/executor.rb

Constant Summary collapse

WEB_ROLE =
:web

Instance Method Summary collapse

Constructor Details

#initialize(config_path, verbose: false) ⇒ Executor

Returns a new instance of Executor.

Parameters:

  • config_path (String)

    path to deploy.yml

  • verbose (Boolean) (defaults to: false)

    show commands being executed



10
11
12
13
14
15
16
17
# File 'lib/odysseus/deployer/executor.rb', line 10

def initialize(config_path, verbose: false)
  @config_path = config_path
  @config_dir = File.dirname(config_path)
  parser = Odysseus::Config::Parser.new(config_path)
  @config = parser.parse
  @verbose = verbose
  @secrets_loader = Odysseus::Secrets::Loader.new(@config, config_dir: @config_dir)
end

Instance Method Details

#boot_dependenciesObject

Boot all dependencies to their configured hosts



284
285
286
# File 'lib/odysseus/deployer/executor.rb', line 284

def boot_dependencies
  dependency_manager.boot_all
end

#build(image_tag: nil, push: false, context_path: nil) ⇒ Hash

Build Docker image

Parameters:

  • image_tag (String, nil) (defaults to: nil)

    docker image tag (e.g., "v1.0.0"), or nil to resolve from git

  • push (Boolean) (defaults to: false)

    push to registry after build

  • context_path (String) (defaults to: nil)

    path to build context (defaults to config directory)

Returns:

  • (Hash)

    build result



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/odysseus/deployer/executor.rb', line 36

def build(image_tag: nil, push: false, context_path: nil)
  resolved = deploy_version(image_tag)
  context = context_path || resolve_build_context
  full_image = "#{@config[:image]}:#{resolved.version}"

  builder = build_builder

  if push
    builder.build_and_push(
      context_path: context,
      image: full_image,
      registry: @config[:registry]
    )
  else
    builder.build(context_path: context, image: full_image)
  end
end

#build_and_deploy(image_tag: nil, context_path: nil, dry_run: false) ⇒ Hash

Build and deploy in one step

Parameters:

  • image_tag (String, nil) (defaults to: nil)

    docker image tag, or nil to resolve from git

  • context_path (String) (defaults to: nil)

    path to build context

  • dry_run (Boolean) (defaults to: false)

    if true, don't actually deploy

Returns:

  • (Hash)

    results



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/odysseus/deployer/executor.rb', line 59

def build_and_deploy(image_tag: nil, context_path: nil, dry_run: false)
  # First, build the image
  build_result = build(image_tag: image_tag, push: true, context_path: context_path)

  return { build: build_result, deploy: nil, success: false } unless build_result[:success]

  # Then deploy
  deploy_results = deploy_all(image_tag: image_tag, dry_run: dry_run)

  {
    build: build_result,
    deploy: deploy_results,
    success: deploy_results.values.all? { |r| r[:success] }
  }
end

#build_and_distribute(image_tag: nil, context_path: nil) ⇒ Hash

Build and distribute image based on config Uses registry if configured, otherwise pussh to hosts

Parameters:

  • image_tag (String, nil) (defaults to: nil)

    docker image tag, or nil to resolve from git

  • context_path (String) (defaults to: nil)

    path to build context

Returns:

  • (Hash)

    build and distribution results



118
119
120
121
122
123
124
# File 'lib/odysseus/deployer/executor.rb', line 118

def build_and_distribute(image_tag: nil, context_path: nil)
  if uses_registry?
    build_and_push_to_registry(image_tag: image_tag, context_path: context_path)
  else
    build_and_pussh(image_tag: image_tag, context_path: context_path)
  end
end

#build_and_push_to_registry(image_tag: nil, context_path: nil) ⇒ Hash

Build and push to registry

Parameters:

  • image_tag (String, nil) (defaults to: nil)

    docker image tag, or nil to resolve from git

  • context_path (String) (defaults to: nil)

    path to build context

Returns:

  • (Hash)

    build and push results



136
137
138
139
140
141
142
143
144
# File 'lib/odysseus/deployer/executor.rb', line 136

def build_and_push_to_registry(image_tag: nil, context_path: nil)
  build_result = build(image_tag: image_tag, push: true, context_path: context_path)

  {
    build: build_result,
    push: build_result[:pushed] ? { success: true } : { success: false },
    success: build_result[:success] && build_result[:pushed]
  }
end

#build_and_pussh(image_tag: nil, context_path: nil) ⇒ Hash

Build and pussh to all hosts (no registry needed)

Parameters:

  • image_tag (String, nil) (defaults to: nil)

    docker image tag, or nil to resolve from git

  • context_path (String) (defaults to: nil)

    path to build context

Returns:

  • (Hash)

    build and pussh results



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/odysseus/deployer/executor.rb', line 97

def build_and_pussh(image_tag: nil, context_path: nil)
  # First, build the image locally
  build_result = build(image_tag: image_tag, push: false, context_path: context_path)

  return { build: build_result, pussh: nil, success: false } unless build_result[:success]

  # Then pussh to all hosts
  pussh_result = pussh(image_tag: image_tag)

  {
    build: build_result,
    pussh: pussh_result,
    success: pussh_result[:success]
  }
end

#dependency_statusObject

List dependency status on all configured hosts



279
280
281
# File 'lib/odysseus/deployer/executor.rb', line 279

def dependency_status
  dependency_manager.status
end

#deploy_all(image_tag: nil, dry_run: false) ⇒ Object

Deploy all roles to their configured hosts

Parameters:

  • image_tag (String, nil) (defaults to: nil)

    docker image tag, or nil to resolve from git

  • dry_run (Boolean) (defaults to: false)

    if true, don't actually deploy



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/odysseus/deployer/executor.rb', line 149

def deploy_all(image_tag: nil, dry_run: false)
  results = {}

  @config[:servers].each do |role, role_config|
    hosts = resolve_hosts(role_config)
    hosts.each do |host|
      puts "\n=== Deploying #{role} to #{host} ==="
      results["#{role}@#{host}"] = deploy_role(host: host, image_tag: image_tag, dry_run: dry_run, role: role)
    end
  end

  prune_old_images unless dry_run

  results
end

#deploy_dependency(name:) ⇒ Object

Deploy an dependency to all its configured hosts

Parameters:

  • name (Symbol)

    dependency name



256
257
258
# File 'lib/odysseus/deployer/executor.rb', line 256

def deploy_dependency(name:)
  dependency_manager.deploy(name: name)
end

#deploy_role(host:, role:, image_tag: nil, dry_run: false) ⇒ Object

Deploy a single role to a specific host

Parameters:

  • host (String)

    target host (from config)

  • image_tag (String, nil) (defaults to: nil)

    docker image tag (e.g., "v1.2.3"), or nil to resolve from git

  • dry_run (Boolean) (defaults to: false)

    if true, don't actually deploy

  • role (Symbol)

    server role



240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/odysseus/deployer/executor.rb', line 240

def deploy_role(host:, role:, image_tag: nil, dry_run: false)
  resolved = deploy_version(image_tag)

  if dry_run
    puts "Dry run - would deploy #{@config[:image]}:#{resolved.version} to #{host}"
    puts "Service: #{@config[:service]}"
    puts "Role: #{role}"
    puts "Proxy hosts: #{@config[:proxy][:hosts].join(', ')}" if role == WEB_ROLE
    return { success: true, dry_run: true }
  end

  run_deploy(host: host, role: role, resolved: resolved)
end

#deploy_version(image_tag = nil) ⇒ Odysseus::DeployVersion

The identity of the deploy: version, ref and deployer.

Resolved once per tag so a multi-role, multi-host deploy cannot end up with two versions, and so the git commands run once rather than per host.

Parameters:

  • image_tag (String, nil) (defaults to: nil)

    explicit tag, or nil to resolve from git

Returns:



26
27
28
29
# File 'lib/odysseus/deployer/executor.rb', line 26

def deploy_version(image_tag = nil)
  @deploy_versions ||= {}
  @deploy_versions[image_tag] ||= version_resolver.resolve(image_tag: image_tag)
end

#host_rolesHash{String => Array<Symbol>}

Every configured host mapped to the roles it serves, each in config order. A host serving two roles (e.g. web and cron on the same box) gets both, in the order its roles are declared in deploy.yml — the order #version_survey depends on to know which role's containers to look for first.

Public API, not just an internal helper: the CLI asks this exact question too — which hosts does this config target, and which roles on each — to visit every host once regardless of how many roles it serves. odysseus doctor depends on it.

Returns:

  • (Hash{String => Array<Symbol>})


300
301
302
303
304
305
306
307
308
# File 'lib/odysseus/deployer/executor.rb', line 300

def host_roles
  roles_by_host = {}

  @config[:servers].each do |role, role_config|
    resolve_hosts(role_config).each { |host| (roles_by_host[host] ||= []) << role }
  end

  roles_by_host
end

#prune_old_imagesHash{String => Array<String>}

Delete a service's images that no host needs any more.

Returns:

  • (Hash{String => Array<String>})

    versions removed, keyed by host



231
232
233
# File 'lib/odysseus/deployer/executor.rb', line 231

def prune_old_images
  retention_sweeper.sweep(host_roles)
end

#pussh(image_tag: nil) ⇒ Hash

Push image to all configured hosts via SSH (using docker pussh)

Parameters:

  • image_tag (String, nil) (defaults to: nil)

    docker image tag, or nil to resolve from git

Returns:

  • (Hash)

    pussh results



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/odysseus/deployer/executor.rb', line 78

def pussh(image_tag: nil)
  resolved = deploy_version(image_tag)
  full_image = "#{@config[:image]}:#{resolved.version}"
  hosts = collect_all_hosts

  return { success: false, error: 'No hosts configured' } if hosts.empty?

  builder = build_builder
  builder.pussh_to_hosts(
    image: full_image,
    hosts: hosts,
    user: @config[:ssh][:user]
  )
end

#remove_dependency(name:) ⇒ Object

Remove an dependency from all its configured hosts

Parameters:

  • name (Symbol)

    dependency name



262
263
264
# File 'lib/odysseus/deployer/executor.rb', line 262

def remove_dependency(name:)
  dependency_manager.remove(name: name)
end

#restart_dependency(name:) ⇒ Object

Restart an dependency on all its configured hosts

Parameters:

  • name (Symbol)

    dependency name



268
269
270
# File 'lib/odysseus/deployer/executor.rb', line 268

def restart_dependency(name:)
  dependency_manager.restart(name: name)
end

#rollback_all(plan) ⇒ Hash

Roll every role on every host back to the planned version.

Reuses the deploy path unchanged, so health gating, proxy handling and zero-downtime behaviour are shared with deploy rather than reimplemented. Sequential, and inheriting deploy's partial-failure semantics: the plan's pre-flight rules out the common cause of a half-rolled-back fleet — a missing image — but does not make the roll atomic.

Parameters:

Returns:

  • (Hash)

    results keyed "role@host"



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/odysseus/deployer/executor.rb', line 209

def rollback_all(plan)
  resolved = Odysseus::DeployVersion.new(
    version: plan.version, ref: plan.ref, deployer: version_resolver.deployer
  )
  results = {}

  @config[:servers].each do |role, role_config|
    resolve_hosts(role_config).each do |host|
      puts "\n=== Rolling back #{role} on #{host} to #{plan.version} ==="
      results["#{role}@#{host}"] = run_deploy(
        host: host, role: role, resolved: resolved,
        kind: 'rolled-back', from: plan.from_for(host)
      )
    end
  end

  results
end

#rollback_plan(version: nil) ⇒ Odysseus::RollbackPlan

Decide what a rollback would do, without doing it.

Surveys the fleet and returns the plan, or raises RollbackError with a message naming the hosts at fault. Separate from rollback_all so the caller can show the target — and any approximate-ordering warning — before anything is touched, and so the survey runs once.

Parameters:

  • version (String, nil) (defaults to: nil)

    explicit target, or nil for the previous one

Returns:



194
195
196
# File 'lib/odysseus/deployer/executor.rb', line 194

def rollback_plan(version: nil)
  Odysseus::RollbackPlanner.new(version_survey).plan(version: version)
end

#upgrade_dependency(name:) ⇒ Object

Upgrade an dependency to a new image version on all its configured hosts

Parameters:

  • name (Symbol)

    dependency name



274
275
276
# File 'lib/odysseus/deployer/executor.rb', line 274

def upgrade_dependency(name:)
  dependency_manager.upgrade(name: name)
end

#uses_registry?Boolean

Check if config uses a registry for image distribution

Returns:

  • (Boolean)


128
129
130
# File 'lib/odysseus/deployer/executor.rb', line 128

def uses_registry?
  @config[:registry] && @config[:registry][:server]
end

#version_surveyArray<Odysseus::HostVersions>

What every host reports about this service's versions.

One entry per unique host across all roles, so a host serving two roles is surveyed once. Connections are opened and closed per host.

Returns:



171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/odysseus/deployer/executor.rb', line 171

def version_survey
  host_roles.map do |host, roles|
    ssh = connect_to_server(host)

    begin
      Odysseus::HostVersions.read(
        host: host, ssh: ssh, service: @config[:service], image: @config[:image], roles: roles
      )
    ensure
      ssh.close
    end
  end
end