Class: Kdeploy::Executor

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

Overview

SSH/SCP executor for remote command execution and file operations

Instance Method Summary collapse

Constructor Details

#initialize(host_config) ⇒ Executor

Returns a new instance of Executor.



15
16
17
18
19
20
21
22
23
24
25
# File 'lib/kdeploy/executor/executor.rb', line 15

def initialize(host_config)
  @host = host_config[:name]
  @user = host_config[:user]
  @ip = host_config[:ip]
  @password = host_config[:password]
  @key = host_config[:key]
  @port = host_config[:port] # Added custom port support
  @use_sudo = host_config[:use_sudo] || false
  @sudo_password = host_config[:sudo_password]
  @base_dir = host_config[:base_dir] # Base directory for resolving relative paths
end

Instance Method Details

#build_command_result(stdout, stderr, command, exit_status) ⇒ Object



70
71
72
73
74
75
76
77
# File 'lib/kdeploy/executor/executor.rb', line 70

def build_command_result(stdout, stderr, command, exit_status)
  {
    stdout: stdout.strip,
    stderr: stderr.strip,
    command: command,
    exit_status: exit_status
  }
end

#execute(command, use_sudo: nil) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/kdeploy/executor/executor.rb', line 27

def execute(command, use_sudo: nil)
  use_sudo = @use_sudo if use_sudo.nil?
  command = wrap_with_sudo(command) if use_sudo

  Net::SSH.start(@ip, @user, ssh_options) do |ssh|
    execute_command_on_ssh(ssh, command)
  end
rescue Net::SSH::AuthenticationFailed => e
  raise SSHError.new("SSH authentication failed: #{e.message}", e)
rescue StandardError => e
  raise SSHError.new("SSH execution failed: #{e.message}", e)
end

#execute_command_on_ssh(ssh, command) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/kdeploy/executor/executor.rb', line 40

def execute_command_on_ssh(ssh, command)
  stdout = String.new
  stderr = String.new
  exit_status = nil

  ssh.open_channel do |channel|
    channel.exec(command) do |_ch, success|
      raise SSHError, "Could not execute command: #{command}" unless success

      setup_channel_handlers(channel, stdout, stderr)
      channel.on_request('exit-status') do |_ch, data|
        exit_status = data.read_long
      end
    end
  end
  ssh.loop
  raise_nonzero_exit!(command, exit_status, stdout, stderr)
  build_command_result(stdout, stderr, command, exit_status)
end

#raise_nonzero_exit!(command, exit_status, stdout, stderr) ⇒ Object

Raises:



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

def raise_nonzero_exit!(command, exit_status, stdout, stderr)
  return if exit_status.nil?
  return if exit_status.zero?

  raise SSHError.new(
    "Command exited with status #{exit_status}",
    nil,
    command: command,
    exit_status: exit_status,
    stdout: stdout.strip,
    stderr: stderr.strip
  )
end

#setup_channel_handlers(channel, stdout, stderr) ⇒ Object



60
61
62
63
64
65
66
67
68
# File 'lib/kdeploy/executor/executor.rb', line 60

def setup_channel_handlers(channel, stdout, stderr)
  channel.on_data do |_ch, data|
    stdout << data
  end

  channel.on_extended_data do |_ch, _type, data|
    stderr << data
  end
end

#sync_directory(source, destination, ignore: [], exclude: [], delete: false, fast: nil, parallel: nil, use_sudo: nil) ⇒ Object



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
# File 'lib/kdeploy/executor/executor.rb', line 119

def sync_directory(source, destination, ignore: [], exclude: [], delete: false, fast: nil, parallel: nil,
                   use_sudo: nil)
  use_sudo = @use_sudo if use_sudo.nil?

  # Resolve relative paths relative to base_dir
  resolved_source = resolve_path(source)

  # Validate source directory
  raise FileNotFoundError, "Source directory not found: #{resolved_source}" unless File.directory?(resolved_source)

  # Create file filter
  all_patterns = ignore + exclude
  filter = FileFilter.new(ignore_patterns: all_patterns)

  if fast
    rsync_result = sync_with_rsync(
      resolved_source,
      destination,
      ignore: ignore,
      exclude: exclude,
      delete: delete,
      use_sudo: use_sudo
    )
    return rsync_result if rsync_result
  end

  # Collect files to sync
  files_to_sync = collect_files_to_sync(resolved_source, filter)

  # Upload files
  uploaded_count = upload_files(
    files_to_sync,
    resolved_source,
    destination,
    parallel: parallel,
    use_sudo: use_sudo
  )

  # Delete extra files if requested
  deleted_count = 0
  deleted_count = delete_extra_files(resolved_source, destination, filter, use_sudo: use_sudo) if delete

  {
    uploaded: uploaded_count,
    deleted: deleted_count,
    total: files_to_sync.size
  }
rescue StandardError => e
  raise SCPError.new("Directory sync failed: #{e.message}", e)
end

#upload(source, destination, use_sudo: nil) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/kdeploy/executor/executor.rb', line 93

def upload(source, destination, use_sudo: nil)
  use_sudo = @use_sudo if use_sudo.nil?

  # Resolve relative paths relative to base_dir
  resolved_source = resolve_path(source)

  # If destination requires sudo, upload to temp location first, then move with sudo
  if use_sudo || requires_sudo?(destination)
    upload_with_sudo(resolved_source, destination)
  else
    Net::SCP.start(@ip, @user, ssh_options) do |scp|
      scp.upload!(resolved_source, destination)
    end
  end
rescue StandardError => e
  raise SCPError.new("SCP upload failed: #{e.message}", e)
end

#upload_template(source, destination, variables = {}) ⇒ Object



111
112
113
114
115
116
117
# File 'lib/kdeploy/executor/executor.rb', line 111

def upload_template(source, destination, variables = {})
  # Resolve relative paths relative to base_dir
  resolved_source = resolve_path(source)
  Template.render_and_upload(self, resolved_source, destination, variables)
rescue StandardError => e
  raise TemplateError.new("Template upload failed: #{e.message}", e)
end