gjallarhorn 0.1.0.alpha → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,362 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ require_relative "manager"
5
+
6
+ module Gjallarhorn
7
+ module Proxy
8
+ # Nginx proxy manager for zero-downtime deployments
9
+ #
10
+ # Manages nginx configuration updates and reloads to enable
11
+ # zero-downtime deployments by switching upstream servers.
12
+ #
13
+ # @since 0.1.0
14
+ class NginxManager < Manager
15
+ # Default nginx configuration paths
16
+ DEFAULT_NGINX_CONF_DIR = "/etc/nginx/conf.d"
17
+ DEFAULT_NGINX_BIN = "nginx"
18
+
19
+ # Initialize nginx proxy manager
20
+ #
21
+ # @param config [Hash] Nginx configuration
22
+ # @param logger [Logger] Logger instance
23
+ def initialize(config, logger = nil)
24
+ super
25
+ @nginx_conf_dir = config[:conf_dir] || DEFAULT_NGINX_CONF_DIR
26
+ @nginx_bin = config[:nginx_bin] || DEFAULT_NGINX_BIN
27
+ @domain = config[:domain] || config[:host]
28
+ @ssl_enabled = config[:ssl] || false
29
+ @app_port = config[:app_port] || 3000
30
+ end
31
+
32
+ # Switch traffic from old containers to new container
33
+ #
34
+ # @param service_name [String] Service name
35
+ # @param from_containers [Array<Hash>] Containers to switch traffic from (unused in nginx implementation)
36
+ # @param to_container [Hash] Container to switch traffic to
37
+ # @return [void]
38
+ def switch_traffic(service_name:, to_container:, from_containers: nil)
39
+ @logger.info "Switching nginx traffic for #{service_name} to #{to_container[:name]}"
40
+
41
+ # Generate new nginx configuration
42
+ new_config = generate_service_config(service_name, [to_container])
43
+
44
+ # Write configuration to file
45
+ write_nginx_config(service_name, new_config)
46
+
47
+ # Test nginx configuration
48
+ test_nginx_config
49
+
50
+ # Reload nginx gracefully
51
+ reload_nginx
52
+
53
+ # Verify traffic is flowing to new container
54
+ if verify_traffic_switch(service_name, to_container)
55
+ @logger.info "Successfully switched traffic to #{to_container[:name]}"
56
+ else
57
+ @logger.warn "Traffic switch completed but verification failed"
58
+ end
59
+ rescue StandardError => e
60
+ @logger.error "Failed to switch nginx traffic: #{e.message}"
61
+ raise ProxyError, "Nginx traffic switch failed: #{e.message}"
62
+ end
63
+
64
+ # Get nginx proxy status
65
+ #
66
+ # @return [Hash] Nginx status information
67
+ def status
68
+ {
69
+ type: "nginx",
70
+ status: nginx_running? ? "running" : "stopped",
71
+ config_dir: @nginx_conf_dir,
72
+ upstreams: configured_upstreams,
73
+ last_reload: last_reload_time
74
+ }
75
+ end
76
+
77
+ # Restart nginx service
78
+ #
79
+ # @return [Boolean] True if restart successful
80
+ def restart
81
+ @logger.info "Restarting nginx..."
82
+
83
+ begin
84
+ execute_nginx_command("restart")
85
+ @logger.info "Nginx restarted successfully"
86
+ true
87
+ rescue StandardError => e
88
+ @logger.error "Failed to restart nginx: #{e.message}"
89
+ false
90
+ end
91
+ end
92
+
93
+ # Check if nginx is healthy
94
+ #
95
+ # @return [Boolean] True if nginx is responding
96
+ def healthy?
97
+ nginx_running? && config_syntax_valid?
98
+ end
99
+
100
+ private
101
+
102
+ # Generate nginx configuration for a service
103
+ #
104
+ # @param service_name [String] Service name
105
+ # @param containers [Array<Hash>] Container information
106
+ # @return [String] Nginx configuration
107
+ def generate_service_config(service_name, containers)
108
+ upstream_config = generate_upstream_config(service_name, containers)
109
+ server_config = generate_server_config(service_name)
110
+
111
+ <<~NGINX
112
+ # Generated by Gjallarhorn for #{service_name}
113
+ # Generated at: #{Time.now.utc.iso8601}
114
+
115
+ #{upstream_config}
116
+
117
+ #{server_config}
118
+ NGINX
119
+ end
120
+
121
+ # Generate nginx server configuration block
122
+ #
123
+ # @param service_name [String] Service name
124
+ # @return [String] Server configuration block
125
+ def generate_server_config(service_name)
126
+ ssl_config = generate_ssl_config
127
+ security_headers = generate_security_headers
128
+ health_check_config = generate_health_check_config(service_name)
129
+ location_config = generate_location_config(service_name)
130
+
131
+ <<~SERVER
132
+ server {
133
+ listen 80;
134
+ #{ssl_config}
135
+ server_name #{@domain};
136
+
137
+ #{security_headers}
138
+
139
+ #{health_check_config}
140
+
141
+ #{location_config}
142
+ }
143
+ SERVER
144
+ end
145
+
146
+ # Generate health check configuration
147
+ #
148
+ # @param service_name [String] Service name
149
+ # @return [String] Health check location block
150
+ def generate_health_check_config(service_name)
151
+ health_path = @config[:health_check_path] || "/health"
152
+
153
+ <<~HEALTH
154
+ # Health check endpoint with container identification
155
+ location #{health_path} {
156
+ proxy_pass http://#{service_name}#{health_path};
157
+ proxy_set_header Host $host;
158
+ proxy_set_header X-Real-IP $remote_addr;
159
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
160
+ proxy_set_header X-Forwarded-Proto $scheme;
161
+ #{" "}
162
+ # Add headers for traffic verification
163
+ add_header X-Proxy-Backend $upstream_addr always;
164
+ add_header X-Proxy-Status $upstream_status always;
165
+ }
166
+ HEALTH
167
+ end
168
+
169
+ # Write nginx configuration to file
170
+ #
171
+ # @param service_name [String] Service name
172
+ # @param config_content [String] Configuration content
173
+ # @return [String] Path to written configuration file
174
+ def write_nginx_config(service_name, config_content)
175
+ config_file = File.join(@nginx_conf_dir, "gjallarhorn-#{service_name}.conf")
176
+
177
+ # Create backup of existing config if it exists
178
+ if File.exist?(config_file)
179
+ backup_file = "#{config_file}.backup-#{Time.now.strftime("%Y%m%d-%H%M%S")}"
180
+ FileUtils.cp(config_file, backup_file)
181
+ @logger.debug "Backed up existing config to #{backup_file}"
182
+ end
183
+
184
+ # Write new configuration
185
+ File.write(config_file, config_content)
186
+ @logger.debug "Wrote nginx config to #{config_file}"
187
+
188
+ config_file
189
+ end
190
+
191
+ # Test nginx configuration syntax
192
+ #
193
+ # @raise [ProxyError] If configuration is invalid
194
+ # @return [void]
195
+ def test_nginx_config
196
+ @logger.debug "Testing nginx configuration syntax..."
197
+
198
+ result = execute_nginx_command("configtest")
199
+ raise ProxyError, "Invalid nginx configuration: #{result[:error]}" unless result[:success]
200
+
201
+ @logger.debug "Nginx configuration syntax is valid"
202
+ end
203
+
204
+ # Reload nginx gracefully
205
+ #
206
+ # @raise [ProxyError] If reload fails
207
+ # @return [void]
208
+ def reload_nginx
209
+ @logger.info "Reloading nginx configuration..."
210
+
211
+ result = execute_nginx_command("reload")
212
+ raise ProxyError, "Failed to reload nginx: #{result[:error]}" unless result[:success]
213
+
214
+ @logger.info "Nginx reloaded successfully"
215
+ end
216
+
217
+ # Execute nginx command
218
+ #
219
+ # @param action [String] Action to perform (reload, restart, configtest)
220
+ # @return [Hash] Execution result
221
+ def execute_nginx_command(action)
222
+ case action
223
+ when "reload"
224
+ command = "#{@nginx_bin} -s reload"
225
+ when "restart"
226
+ command = "systemctl restart nginx"
227
+ when "configtest"
228
+ command = "#{@nginx_bin} -t"
229
+ else
230
+ raise ArgumentError, "Unknown nginx action: #{action}"
231
+ end
232
+
233
+ @logger.debug "Executing: #{command}"
234
+
235
+ result = system(command)
236
+ {
237
+ success: result,
238
+ error: result ? nil : "Command failed with exit code: #{$CHILD_STATUS.exitstatus}"
239
+ }
240
+ end
241
+
242
+ # Check if nginx is running
243
+ #
244
+ # @return [Boolean] True if nginx process is running
245
+ def nginx_running?
246
+ system("pgrep nginx > /dev/null 2>&1")
247
+ end
248
+
249
+ # Check if nginx configuration syntax is valid
250
+ #
251
+ # @return [Boolean] True if configuration is valid
252
+ def config_syntax_valid?
253
+ result = execute_nginx_command("configtest")
254
+ result[:success]
255
+ end
256
+
257
+ # Get configured upstream servers
258
+ #
259
+ # @return [Array<String>] List of configured upstreams
260
+ def configured_upstreams
261
+ config_files = Dir.glob(File.join(@nginx_conf_dir, "gjallarhorn-*.conf"))
262
+ upstreams = []
263
+
264
+ config_files.each do |file|
265
+ content = File.read(file)
266
+ upstreams.concat(content.scan(/upstream\s+(\w+)\s*{/).flatten)
267
+ end
268
+
269
+ upstreams
270
+ end
271
+
272
+ # Get last nginx reload time
273
+ #
274
+ # @return [Time, nil] Last reload time or nil if unknown
275
+ def last_reload_time
276
+ # Try to get nginx master process start time as proxy for last reload
277
+ if nginx_running?
278
+ pid = `pgrep -f "nginx: master process"`.strip
279
+ unless pid.empty?
280
+ stat_file = "/proc/#{pid}/stat"
281
+ if File.exist?(stat_file)
282
+ boot_time = File.read("/proc/stat").match(/btime (\d+)/)[1].to_i
283
+ start_time_ticks = File.read(stat_file).split[21].to_i
284
+ clock_ticks = 100 # Typical value for USER_HZ
285
+ Time.at(boot_time + start_time_ticks / clock_ticks)
286
+ end
287
+ end
288
+ end
289
+ rescue StandardError
290
+ nil
291
+ end
292
+
293
+ # Generate SSL configuration block
294
+ #
295
+ # @return [String] SSL configuration
296
+ def generate_ssl_config
297
+ return "" unless @ssl_enabled
298
+
299
+ <<~SSL
300
+ listen 443 ssl http2;
301
+ ssl_certificate /etc/letsencrypt/live/#{@domain}/fullchain.pem;
302
+ ssl_certificate_key /etc/letsencrypt/live/#{@domain}/privkey.pem;
303
+ ssl_session_timeout 1d;
304
+ ssl_session_cache shared:SSL:50m;
305
+ ssl_stapling on;
306
+ ssl_stapling_verify on;
307
+ SSL
308
+ end
309
+
310
+ # Generate security headers configuration
311
+ #
312
+ # @return [String] Security headers configuration
313
+ def generate_security_headers
314
+ <<~HEADERS
315
+ # Security headers
316
+ add_header X-Frame-Options DENY always;
317
+ add_header X-Content-Type-Options nosniff always;
318
+ add_header X-XSS-Protection "1; mode=block" always;
319
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
320
+
321
+ # Gjallarhorn identification headers
322
+ add_header X-Proxy-Type "nginx" always;
323
+ add_header X-Managed-By "gjallarhorn" always;
324
+ HEADERS
325
+ end
326
+
327
+ # Generate location configuration for main proxy
328
+ #
329
+ # @param service_name [String] Service name
330
+ # @return [String] Location configuration
331
+ def generate_location_config(service_name)
332
+ <<~LOCATION
333
+ location / {
334
+ proxy_pass http://#{service_name};
335
+ proxy_set_header Host $host;
336
+ proxy_set_header X-Real-IP $remote_addr;
337
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
338
+ proxy_set_header X-Forwarded-Proto $scheme;
339
+ proxy_set_header X-Forwarded-Host $host;
340
+ proxy_set_header X-Forwarded-Port $server_port;
341
+ #{" "}
342
+ # Timeouts
343
+ proxy_connect_timeout 5s;
344
+ proxy_send_timeout 60s;
345
+ proxy_read_timeout 60s;
346
+ #{" "}
347
+ # Buffer settings
348
+ proxy_buffering on;
349
+ proxy_buffer_size 4k;
350
+ proxy_buffers 8 4k;
351
+ #{" "}
352
+ # Health check support
353
+ proxy_next_upstream error timeout http_502 http_503 http_504;
354
+ }
355
+ LOCATION
356
+ end
357
+ end
358
+
359
+ # Raised when proxy operations fail
360
+ class ProxyError < Error; end
361
+ end
362
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "manager"
4
+
5
+ module Gjallarhorn
6
+ module Proxy
7
+ # Traefik proxy manager for zero-downtime deployments
8
+ #
9
+ # This is a placeholder implementation for Traefik support.
10
+ # Full implementation will be added in a future release.
11
+ #
12
+ # @since 0.1.0
13
+ class TraefikManager < Manager
14
+ # Switch traffic from old containers to new container
15
+ #
16
+ # @param service_name [String] Service name
17
+ # @param from_containers [Array<Hash>] Containers to switch traffic from
18
+ # @param to_container [Hash] Container to switch traffic to
19
+ # @return [void]
20
+ def switch_traffic(service_name:, from_containers:, to_container:)
21
+ raise NotImplementedError, "Traefik proxy support not yet implemented"
22
+ end
23
+
24
+ # Get traefik proxy status
25
+ #
26
+ # @return [Hash] Traefik status information
27
+ def status
28
+ {
29
+ type: "traefik",
30
+ status: "not_implemented",
31
+ upstreams: []
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gjallarhorn
4
- VERSION = "0.1.0.alpha"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/gjallarhorn.rb CHANGED
@@ -2,10 +2,26 @@
2
2
 
3
3
  require_relative "gjallarhorn/version"
4
4
 
5
+ # Gjallarhorn - Multi-cloud deployment guardian
6
+ #
7
+ # Gjallarhorn provides a unified interface for deploying containerized applications
8
+ # across different cloud providers using their native APIs. Named after Heimdall's horn
9
+ # in Norse mythology that sounds across all realms, Gjallarhorn enables secure,
10
+ # API-first deployments beyond traditional SSH-based tools.
11
+ #
12
+ # @example Basic usage
13
+ # deployer = Gjallarhorn::Deployer.new('deploy.yml')
14
+ # deployer.deploy('production', 'myapp:v1.0.0')
15
+ # deployer.status('production')
16
+ #
17
+ # @author Ken C. Demanawa
18
+ # @since 0.1.0
5
19
  module Gjallarhorn
20
+ # Base error class for all Gjallarhorn-specific errors
6
21
  class Error < StandardError; end
7
22
  end
8
23
 
9
24
  require_relative "gjallarhorn/configuration"
10
25
  require_relative "gjallarhorn/deployer"
11
26
  require_relative "gjallarhorn/cli"
27
+ require_relative "gjallarhorn/history"
metadata CHANGED
@@ -1,15 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gjallarhorn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-29 00:00:00.000000000 Z
11
+ date: 2025-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-ssm
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk-ec2
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
13
55
  - !ruby/object:Gem::Dependency
14
56
  name: irb
15
57
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +108,48 @@ dependencies:
66
108
  - - "~>"
67
109
  - !ruby/object:Gem::Version
68
110
  version: '1.21'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-minitest
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.38.1
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.38.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.7.1
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.7.1
139
+ - !ruby/object:Gem::Dependency
140
+ name: simplecov
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.22'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.22'
69
153
  description: A Ruby gem that sounds across all cloud realms with secure, API-first
70
154
  deployments beyond SSH.
71
155
  email:
@@ -75,6 +159,7 @@ executables:
75
159
  extensions: []
76
160
  extra_rdoc_files: []
77
161
  files:
162
+ - ".yardopts"
78
163
  - CODE_OF_CONDUCT.md
79
164
  - LICENSE.txt
80
165
  - README.md
@@ -82,13 +167,23 @@ files:
82
167
  - Rakefile
83
168
  - examples/deploy.yml
84
169
  - examples/dream-deploy.yml
170
+ - examples/zero-downtime-deployment.rb
85
171
  - exe/gjallarhorn
86
172
  - lib/gjallarhorn.rb
87
- - lib/gjallarhorn/adapters/aws.rb
88
- - lib/gjallarhorn/adapters/base.rb
173
+ - lib/gjallarhorn/adapter/aws.rb
174
+ - lib/gjallarhorn/adapter/base.rb
89
175
  - lib/gjallarhorn/cli.rb
90
176
  - lib/gjallarhorn/configuration.rb
91
177
  - lib/gjallarhorn/deployer.rb
178
+ - lib/gjallarhorn/deployment/basic.rb
179
+ - lib/gjallarhorn/deployment/legacy.rb
180
+ - lib/gjallarhorn/deployment/strategy.rb
181
+ - lib/gjallarhorn/deployment/zero_downtime.rb
182
+ - lib/gjallarhorn/history.rb
183
+ - lib/gjallarhorn/proxy/kamal_proxy_manager.rb
184
+ - lib/gjallarhorn/proxy/manager.rb
185
+ - lib/gjallarhorn/proxy/nginx_manager.rb
186
+ - lib/gjallarhorn/proxy/traefik_manager.rb
92
187
  - lib/gjallarhorn/version.rb
93
188
  - sig/bifrost.rbs
94
189
  homepage: https://github.com/kanutocd/gjallarhorn
@@ -109,9 +204,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
109
204
  version: 3.1.0
110
205
  required_rubygems_version: !ruby/object:Gem::Requirement
111
206
  requirements:
112
- - - ">"
207
+ - - ">="
113
208
  - !ruby/object:Gem::Version
114
- version: 1.3.1
209
+ version: '0'
115
210
  requirements: []
116
211
  rubygems_version: 3.3.27
117
212
  signing_key:
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # AWS SSM Adapter
4
- module Gjallarhorn
5
- module Adapters
6
- class AWSAdapter < Base
7
- def initialize(config)
8
- super
9
- require "aws-sdk-ssm"
10
- require "aws-sdk-ec2"
11
- @ssm = Aws::SSM::Client.new(region: config[:region])
12
- @ec2 = Aws::EC2::Client.new(region: config[:region])
13
- end
14
-
15
- def deploy(image:, environment:, services: [])
16
- instances = get_instances_by_tags(environment)
17
-
18
- commands = build_deployment_commands(image, services)
19
-
20
- logger.info "Deploying #{image} to #{instances.size} AWS instances"
21
-
22
- response = @ssm.send_command(
23
- instance_ids: instances,
24
- document_name: "AWS-RunShellScript",
25
- parameters: {
26
- "commands" => commands,
27
- "executionTimeout" => ["3600"]
28
- },
29
- comment: "Deploy #{image} via Gjallarhorn"
30
- )
31
-
32
- wait_for_command_completion(response.command.command_id, instances)
33
-
34
- # Verify health across all instances
35
- services.each do |service|
36
- wait_for_health(service)
37
- end
38
-
39
- logger.info "Deployment completed successfully"
40
- end
41
-
42
- def rollback(version:)
43
- # Similar implementation for rollback
44
- end
45
-
46
- def status
47
- instances = get_instances_by_tags(config[:environment])
48
- instances.map do |instance_id|
49
- {
50
- instance: instance_id,
51
- status: get_instance_status(instance_id)
52
- }
53
- end
54
- end
55
-
56
- def health_check(*)
57
- # Implement health check via SSM command
58
- true # Simplified
59
- end
60
-
61
- private
62
-
63
- def get_instances_by_tags(environment)
64
- resp = @ec2.describe_instances(
65
- filters: [
66
- { name: "tag:Environment", values: [environment] },
67
- { name: "tag:Role", values: %w[web app] },
68
- { name: "instance-state-name", values: ["running"] }
69
- ]
70
- )
71
-
72
- resp.reservations.flat_map(&:instances).map(&:instance_id)
73
- end
74
-
75
- def build_deployment_commands(image, services)
76
- [
77
- "docker pull #{image}",
78
- *services.map { |svc| "docker stop #{svc[:name]} || true" },
79
- *services.map do |svc|
80
- "docker run -d --name #{svc[:name]} " \
81
- "#{svc[:ports].map { |p| "-p #{p}" }.join(" ")} " \
82
- "#{svc[:env].map { |k, v| "-e #{k}=#{v}" }.join(" ")} " \
83
- "#{image}"
84
- end
85
- ]
86
- end
87
-
88
- def wait_for_command_completion(command_id, _instances)
89
- @ssm.wait_until(:command_executed, command_id: command_id) do |w|
90
- w.max_attempts = 60
91
- w.delay = 5
92
- end
93
- end
94
- end
95
- end
96
- end