kamal 2.11.0 → 2.12.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +48 -39
  3. data/lib/kamal/cli/app.rb +57 -48
  4. data/lib/kamal/cli/base.rb +99 -11
  5. data/lib/kamal/cli/build.rb +9 -6
  6. data/lib/kamal/cli/lock.rb +5 -16
  7. data/lib/kamal/cli/main.rb +59 -53
  8. data/lib/kamal/cli/proxy.rb +9 -9
  9. data/lib/kamal/cli/prune.rb +3 -3
  10. data/lib/kamal/cli/server.rb +24 -15
  11. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +1 -1
  12. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +1 -1
  13. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +1 -1
  14. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +1 -1
  15. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +1 -1
  16. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +1 -1
  17. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +1 -1
  18. data/lib/kamal/cli/templates/secrets +4 -0
  19. data/lib/kamal/commander.rb +54 -1
  20. data/lib/kamal/commands/accessory.rb +2 -2
  21. data/lib/kamal/commands/app/logging.rb +1 -1
  22. data/lib/kamal/commands/app.rb +1 -1
  23. data/lib/kamal/commands/builder/clone.rb +2 -1
  24. data/lib/kamal/configuration/accessory.rb +13 -5
  25. data/lib/kamal/configuration/docs/configuration.yml +18 -3
  26. data/lib/kamal/configuration/docs/env.yml +6 -4
  27. data/lib/kamal/configuration/docs/output.yml +25 -0
  28. data/lib/kamal/configuration/docs/role.yml +1 -0
  29. data/lib/kamal/configuration/docs/ssh.yml +8 -0
  30. data/lib/kamal/configuration/output.rb +34 -0
  31. data/lib/kamal/configuration/proxy/run.rb +9 -0
  32. data/lib/kamal/configuration/role.rb +18 -6
  33. data/lib/kamal/configuration/ssh.rb +5 -1
  34. data/lib/kamal/configuration/validator.rb +14 -2
  35. data/lib/kamal/configuration.rb +6 -1
  36. data/lib/kamal/git.rb +1 -1
  37. data/lib/kamal/otel_shipper.rb +176 -0
  38. data/lib/kamal/output/base_logger.rb +29 -0
  39. data/lib/kamal/output/file_logger.rb +51 -0
  40. data/lib/kamal/output/formatter.rb +36 -0
  41. data/lib/kamal/output/otel_logger.rb +70 -0
  42. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +10 -2
  43. data/lib/kamal/secrets/adapters/passbolt.rb +1 -1
  44. data/lib/kamal/secrets.rb +1 -1
  45. data/lib/kamal/sshkit_with_ext.rb +9 -4
  46. data/lib/kamal/version.rb +1 -1
  47. metadata +9 -2
@@ -0,0 +1,25 @@
1
+ # Output
2
+ #
3
+ # Configure where Kamal sends command output logs.
4
+
5
+ # Output options
6
+ #
7
+ # The options are specified under the output key in the configuration file.
8
+ output:
9
+
10
+ # OTel
11
+ #
12
+ # Ship deploy logs to an OpenTelemetry-compatible endpoint via OTLP HTTP.
13
+ #
14
+ # Logs are sent as OTLP log records with resource attributes derived from
15
+ # Kamal's deploy tags (service, version, performer, destination, etc.)
16
+ otel:
17
+ endpoint: http://otel-gateway:4318
18
+
19
+ # File
20
+ #
21
+ # Write deploy logs to a file on the local machine.
22
+ #
23
+ # One log file is created per deploy, named with the timestamp and command.
24
+ file:
25
+ path: /var/log/kamal/
@@ -39,6 +39,7 @@ servers:
39
39
  - 172.1.0.3
40
40
  - 172.1.0.4: experiment1
41
41
  cmd: "bin/jobs"
42
+ stop_timeout: 30
42
43
  options:
43
44
  memory: 2g
44
45
  cpus: 4
@@ -71,3 +71,11 @@ ssh:
71
71
  # /etc/ssh_config), to false ignore config files, or to a file path
72
72
  # (or array of paths) to load specific configuration. Defaults to true.
73
73
  config: [ "~/.ssh/myconfig" ]
74
+
75
+ # Forward agent
76
+ #
77
+ # Whether to forward the local SSH agent to the remote host. Defaults to
78
+ # true (sshkit's default). Set to false when connecting through a jump
79
+ # host or tunnel that does not support agent forwarding (for example,
80
+ # Cloudflare Access for Infrastructure with SSH).
81
+ forward_agent: false
@@ -0,0 +1,34 @@
1
+ class Kamal::Configuration::Output
2
+ include Kamal::Configuration::Validation
3
+
4
+ LOGGER_TYPES = {
5
+ "otel" => "Kamal::Output::OtelLogger",
6
+ "file" => "Kamal::Output::FileLogger"
7
+ }
8
+
9
+ attr_reader :output_config, :loggers
10
+
11
+ def initialize(config:)
12
+ @config = config
13
+ @output_config = config.raw_config.output || {}
14
+ validate! @output_config unless @output_config.empty?
15
+ @loggers = build_loggers
16
+ end
17
+
18
+ def enabled?
19
+ output_config.present?
20
+ end
21
+
22
+ def to_h
23
+ output_config
24
+ end
25
+
26
+ private
27
+ def build_loggers
28
+ output_config.filter_map do |key, settings|
29
+ if (klass_name = LOGGER_TYPES[key])
30
+ klass_name.constantize.build(settings: settings || {}, config: @config)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -131,6 +131,15 @@ class Kamal::Configuration::Proxy::Run
131
131
  File.join apps_container_directory, config.service_and_destination
132
132
  end
133
133
 
134
+ def ==(other)
135
+ other.is_a?(self.class) && run_config == other.run_config
136
+ end
137
+ alias_method :eql?, :==
138
+
139
+ def hash
140
+ run_config.hash
141
+ end
142
+
134
143
  private
135
144
  def format_bind_ip(ip)
136
145
  # Ensure IPv6 address inside square brackets - e.g. [::1]
@@ -44,11 +44,11 @@ class Kamal::Configuration::Role
44
44
  end
45
45
 
46
46
  def option_args
47
- if args = specializations["options"]
48
- optionize args
49
- else
50
- []
51
- end
47
+ optionize docker_options.reject { |key, _| key.to_s == "restart" }
48
+ end
49
+
50
+ def restart_policy
51
+ restart_policy_option || "unless-stopped"
52
52
  end
53
53
 
54
54
  def labels
@@ -81,11 +81,15 @@ class Kamal::Configuration::Role
81
81
 
82
82
  def stop_args
83
83
  # When deploying with the proxy, kamal-proxy will drain request before returning so we don't need to wait.
84
- timeout = running_proxy? ? nil : config.drain_timeout
84
+ timeout = stop_timeout || (running_proxy? ? nil : config.drain_timeout)
85
85
 
86
86
  [ *argumentize("-t", timeout) ]
87
87
  end
88
88
 
89
+ def stop_timeout
90
+ specializations["stop_timeout"] || config.stop_timeout
91
+ end
92
+
89
93
  def env(host)
90
94
  @envs ||= {}
91
95
  @envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
@@ -217,6 +221,14 @@ class Kamal::Configuration::Role
217
221
  @role_config ||= config.raw_config.servers.is_a?(Array) ? {} : config.raw_config.servers[name]
218
222
  end
219
223
 
224
+ def docker_options
225
+ specializations["options"] || {}
226
+ end
227
+
228
+ def restart_policy_option
229
+ docker_options.find { |key, _| key.to_s == "restart" }&.last
230
+ end
231
+
220
232
  def custom_labels
221
233
  Hash.new.tap do |labels|
222
234
  labels.merge!(config.labels) if config.labels.present?
@@ -53,8 +53,12 @@ class Kamal::Configuration::Ssh
53
53
  ssh_config["config"]
54
54
  end
55
55
 
56
+ def forward_agent
57
+ ssh_config["forward_agent"]
58
+ end
59
+
56
60
  def options
57
- { user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30, keys_only: keys_only, keys: keys, key_data: key_data, config: config }.compact
61
+ { user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30, keys_only: keys_only, keys: keys, key_data: key_data, config: config, forward_agent: forward_agent }.compact
58
62
  end
59
63
 
60
64
  def to_h
@@ -232,8 +232,20 @@ class Kamal::Configuration::Validator
232
232
  end
233
233
 
234
234
  def validate_docker_options!(options)
235
- if options
236
- error "Cannot set restart policy in docker options, unless-stopped is required" if options["restart"]
235
+ if restart_policy = options&.find { |key, _| key.to_s == "restart" }
236
+ validate_restart_policy!(restart_policy.last)
237
+ end
238
+ end
239
+
240
+ def validate_restart_policy!(restart_policy)
241
+ with_context("options/restart") do
242
+ unless restart_policy.is_a?(String)
243
+ error %(should be a string. Use "no" to disable restarts)
244
+ end
245
+
246
+ unless restart_policy.match?(/\A(?:no|always|unless-stopped|on-failure(?::\d+)?)\z/)
247
+ error "should be no, always, unless-stopped, on-failure, or on-failure:N"
248
+ end
237
249
  end
238
250
  end
239
251
  end
@@ -12,7 +12,7 @@ class Kamal::Configuration
12
12
  delegate :argumentize, :optionize, to: Kamal::Utils
13
13
 
14
14
  attr_reader :destination, :raw_config, :secrets
15
- attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :proxy_boot, :servers, :ssh, :sshkit, :registry
15
+ attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :output, :proxy, :proxy_boot, :servers, :ssh, :sshkit, :registry
16
16
 
17
17
  include Validation
18
18
 
@@ -71,6 +71,7 @@ class Kamal::Configuration
71
71
  @env = Env.new(config: @raw_config.env || {}, secrets: secrets)
72
72
 
73
73
  @logging = Logging.new(logging_config: @raw_config.logging)
74
+ @output = Output.new(config: self)
74
75
  @proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy, secrets: secrets)
75
76
  @proxy_boot = Proxy::Boot.new(config: self)
76
77
  @ssh = Ssh.new(config: self)
@@ -240,6 +241,10 @@ class Kamal::Configuration
240
241
  raw_config.drain_timeout || 30
241
242
  end
242
243
 
244
+ def stop_timeout
245
+ raw_config.stop_timeout
246
+ end
247
+
243
248
  def run_directory
244
249
  ".kamal"
245
250
  end
data/lib/kamal/git.rb CHANGED
@@ -6,7 +6,7 @@ module Kamal::Git
6
6
  end
7
7
 
8
8
  def user_name
9
- `git config user.name`.strip
9
+ `git config user.name`.force_encoding(Encoding::UTF_8).strip
10
10
  end
11
11
 
12
12
  def email
@@ -0,0 +1,176 @@
1
+ require "active_support/core_ext/numeric/time"
2
+ require "net/http"
3
+ require "json"
4
+ require "securerandom"
5
+ require "uri"
6
+
7
+ class Kamal::OtelShipper
8
+ BATCH_SIZE = 100
9
+ FLUSH_INTERVAL = 5.seconds
10
+
11
+ OTEL_ATTRIBUTE_KEYS = {
12
+ service: "service.namespace",
13
+ version: "kamal.deploy_version",
14
+ performer: "kamal.performer",
15
+ destination: "deployment.environment.name"
16
+ }
17
+
18
+ SEVERITIES = {
19
+ debug: { severityNumber: 5, severityText: "DEBUG" },
20
+ info: { severityNumber: 9, severityText: "INFO" },
21
+ warn: { severityNumber: 13, severityText: "WARN" },
22
+ error: { severityNumber: 17, severityText: "ERROR" },
23
+ fatal: { severityNumber: 21, severityText: "FATAL" }
24
+ }
25
+
26
+ LOGGER_SEVERITIES = {
27
+ Logger::DEBUG => :debug,
28
+ Logger::INFO => :info,
29
+ Logger::WARN => :warn,
30
+ Logger::ERROR => :error,
31
+ Logger::FATAL => :fatal
32
+ }
33
+
34
+ attr_reader :run_id
35
+
36
+ def initialize(endpoint:, tags:)
37
+ @endpoint = URI("#{endpoint.chomp('/')}/v1/logs")
38
+ @run_id = SecureRandom.uuid
39
+ @resource_attributes = [
40
+ { key: "service.name", value: { stringValue: "kamal" } },
41
+ { key: "service.version", value: { stringValue: Kamal::VERSION } },
42
+ { key: "kamal.run_id", value: { stringValue: @run_id } },
43
+ *tags.tags.map do |key, value|
44
+ otel_key = OTEL_ATTRIBUTE_KEYS.fetch(key, "kamal.#{key}")
45
+ { key: otel_key, value: { stringValue: value.to_s } }
46
+ end
47
+ ]
48
+ @buffer = Queue.new
49
+ @flush_mutex = Mutex.new
50
+ @running = true
51
+ @signal = Queue.new
52
+ @thread = start_flush_thread
53
+ end
54
+
55
+ def <<(str)
56
+ append(str)
57
+ end
58
+
59
+ def append(str, host: nil, iostream: nil, severity: nil)
60
+ otel_severity = LOGGER_SEVERITIES.fetch(severity, :info)
61
+ extra = build_context_attributes(host: host, iostream: iostream)
62
+ str.to_s.each_line do |line|
63
+ enqueue build_record(line.chomp, severity: otel_severity, attributes: extra)
64
+ end
65
+
66
+ self
67
+ end
68
+
69
+ def event(name, severity: :info, **attributes)
70
+ attrs = attributes.map { |k, v| { key: k.to_s, value: typed_value(v) } }
71
+ enqueue build_record(name, severity: severity, event_name: name, attributes: attrs)
72
+
73
+ self
74
+ end
75
+
76
+ def flush
77
+ @flush_mutex.synchronize do
78
+ lines = drain_buffer
79
+ ship(lines) if lines.any?
80
+ end
81
+ end
82
+
83
+ def shutdown
84
+ @running = false
85
+ @signal << true
86
+ @thread&.join(FLUSH_INTERVAL + 1.second)
87
+ flush
88
+ end
89
+
90
+ private
91
+ def enqueue(record)
92
+ @buffer << record
93
+ @signal << true if @buffer.size >= BATCH_SIZE
94
+ end
95
+
96
+ def start_flush_thread
97
+ Thread.new do
98
+ while @running
99
+ @signal.pop(timeout: FLUSH_INTERVAL)
100
+ flush
101
+ end
102
+ end
103
+ end
104
+
105
+ def drain_buffer
106
+ records = []
107
+ records << @buffer.pop(true) until @buffer.empty?
108
+ records
109
+ end
110
+
111
+ def ship(records)
112
+ with_connection do |http|
113
+ records.each_slice(BATCH_SIZE) do |batch|
114
+ ship_records(http, batch)
115
+ end
116
+ end
117
+ end
118
+
119
+ def build_record(body, severity: :info, event_name: nil, attributes: nil)
120
+ now = time_ns
121
+ { timeUnixNano: now, observedTimeUnixNano: now, **SEVERITIES.fetch(severity),
122
+ body: { stringValue: body }, eventName: event_name, attributes: attributes }.compact
123
+ end
124
+
125
+ def build_context_attributes(host:, iostream:)
126
+ attrs = []
127
+ attrs << { key: "server.address", value: { stringValue: host } } if host
128
+ attrs << { key: "log.iostream", value: { stringValue: iostream } } if iostream
129
+ attrs.presence
130
+ end
131
+
132
+ def typed_value(v)
133
+ case v
134
+ when Integer then { intValue: v }
135
+ when Float then { doubleValue: v }
136
+ when Array then { arrayValue: { values: v.map { |e| typed_value(e) } } }
137
+ else { stringValue: v.to_s }
138
+ end
139
+ end
140
+
141
+ def with_connection
142
+ http = Net::HTTP.new(@endpoint.host, @endpoint.port)
143
+ http.use_ssl = @endpoint.scheme == "https"
144
+ http.open_timeout = 2.seconds
145
+ http.read_timeout = 5.seconds
146
+ http.start { |conn| yield conn }
147
+ rescue StandardError => e
148
+ unless @ship_error_logged
149
+ @ship_error_logged = true
150
+ $stderr.puts "OTel log shipping failed: #{e.class}: #{e.message}"
151
+ $stderr.puts e.backtrace.join("\n") if ENV["VERBOSE"]
152
+ end
153
+ end
154
+
155
+ def ship_records(http, records)
156
+ payload = {
157
+ resourceLogs: [ {
158
+ resource: { attributes: @resource_attributes },
159
+ scopeLogs: [ { scope: { name: "kamal", version: Kamal::VERSION }, logRecords: records } ]
160
+ } ]
161
+ }
162
+
163
+ req = Net::HTTP::Post.new(@endpoint.request_uri, "Content-Type" => "application/json")
164
+ req.body = JSON.generate(payload)
165
+ response = http.request(req)
166
+
167
+ unless response.is_a?(Net::HTTPSuccess) || @ship_error_logged
168
+ @ship_error_logged = true
169
+ $stderr.puts "OTel log shipping failed: HTTP #{response.code} #{response.message}"
170
+ end
171
+ end
172
+
173
+ def time_ns
174
+ Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond).to_s
175
+ end
176
+ end
@@ -0,0 +1,29 @@
1
+ require "logger"
2
+
3
+ class Kamal::Output::BaseLogger < ::Logger
4
+ def initialize
5
+ super(nil)
6
+ @subscription = ActiveSupport::Notifications.subscribe("modify.kamal", self)
7
+ end
8
+
9
+ def start(name, id, payload)
10
+ @started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
11
+ on_start(payload)
12
+ end
13
+
14
+ def finish(name, id, payload)
15
+ runtime = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at).round(1)
16
+ on_finish(payload, runtime)
17
+ end
18
+
19
+ def add(severity, message = nil, progname = nil, &block)
20
+ if msg = message || (block ? block.call : progname)
21
+ self << msg.to_s
22
+ end
23
+ end
24
+
25
+ def close
26
+ ActiveSupport::Notifications.unsubscribe(@subscription)
27
+ on_close
28
+ end
29
+ end
@@ -0,0 +1,51 @@
1
+ class Kamal::Output::FileLogger < Kamal::Output::BaseLogger
2
+ attr_reader :path
3
+
4
+ def self.build(settings:, config:)
5
+ raise ArgumentError, "file path is required" unless settings["path"]
6
+ new(path: settings["path"])
7
+ end
8
+
9
+ def initialize(path:)
10
+ @path = Pathname.new(path)
11
+ super()
12
+ end
13
+
14
+ def <<(message)
15
+ @file&.print(message)
16
+ end
17
+
18
+ private
19
+ def on_start(payload)
20
+ path.mkpath
21
+ @file_path = path.join(filename_for(payload))
22
+ @file = File.open(@file_path, "a")
23
+ @file.sync = true
24
+ end
25
+
26
+ def on_finish(payload, runtime)
27
+ if @file
28
+ if payload[:exception]
29
+ error_class, error_message = payload[:exception]
30
+ @file.puts "# FAILED: #{error_class}: #{error_message} (#{runtime}s)"
31
+ else
32
+ @file.puts "# Completed in #{runtime}s"
33
+ end
34
+ @file.close
35
+ @file = nil
36
+ puts "Logs written to #{@file_path}"
37
+ end
38
+ end
39
+
40
+ def on_close
41
+ if @file
42
+ @file.close
43
+ @file = nil
44
+ end
45
+ end
46
+
47
+ def filename_for(payload)
48
+ command = [ payload[:command], payload[:subcommand] ].compact.join("_")
49
+ [ Time.now.strftime("%Y-%m-%dT%H-%M-%S"), payload[:destination], command ].compact.join("_") + ".log"
50
+ end
51
+ end
@@ -0,0 +1,36 @@
1
+ class Kamal::Output::Formatter < SSHKit::Formatter::Pretty
2
+ def initialize(output, logger)
3
+ @logger = logger
4
+ super(output)
5
+ end
6
+
7
+ def log_command_start(command)
8
+ with_command_context(command) { super }
9
+ end
10
+
11
+ def log_command_data(command, stream_type, stream_data)
12
+ with_command_context(command, iostream: stream_type.to_s) { super }
13
+ end
14
+
15
+ def log_command_exit(command)
16
+ with_command_context(command) { super }
17
+ end
18
+
19
+ private
20
+ def write_message(verbosity, message, uuid = nil)
21
+ super
22
+ Thread.current[:kamal_severity] = verbosity
23
+ @logger << "#{format_message(verbosity, message, uuid)}\n" rescue nil
24
+ ensure
25
+ Thread.current[:kamal_severity] = nil
26
+ end
27
+
28
+ def with_command_context(command, iostream: nil)
29
+ Thread.current[:kamal_host] = command.host.to_s
30
+ Thread.current[:kamal_iostream] = iostream
31
+ yield
32
+ ensure
33
+ Thread.current[:kamal_host] = nil
34
+ Thread.current[:kamal_iostream] = nil
35
+ end
36
+ end
@@ -0,0 +1,70 @@
1
+ class Kamal::Output::OtelLogger < Kamal::Output::BaseLogger
2
+ def self.build(settings:, config:)
3
+ raise ArgumentError, "OTel endpoint is required" unless settings["endpoint"]
4
+ new(
5
+ endpoint: settings["endpoint"],
6
+ tags: Kamal::Tags.from_config(config).except(:service_version, :recorded_at),
7
+ service: config.service
8
+ )
9
+ end
10
+
11
+ def initialize(endpoint:, tags:, service: nil)
12
+ @endpoint = endpoint
13
+ @shipper = Kamal::OtelShipper.new(endpoint: endpoint, tags: tags)
14
+ @service = service
15
+ super()
16
+ end
17
+
18
+ def <<(message)
19
+ host = Thread.current[:kamal_host]
20
+ iostream = Thread.current[:kamal_iostream]
21
+ severity = Thread.current[:kamal_severity]
22
+ @shipper.append(message, host: host, iostream: iostream, severity: severity)
23
+ end
24
+
25
+ DEPLOY_COMMANDS = %w[ deploy redeploy rollback setup ].freeze
26
+
27
+ private
28
+ def on_start(payload)
29
+ @shipper.event("kamal.start",
30
+ "kamal.command": full_command(payload),
31
+ **deployment_attrs(payload))
32
+ end
33
+
34
+ def on_finish(payload, runtime)
35
+ if payload[:exception]
36
+ error_class, error_message = payload[:exception]
37
+ @shipper.event("kamal.failed", severity: :error,
38
+ "kamal.command": full_command(payload), "kamal.runtime": runtime,
39
+ "exception.type": error_class, "exception.message": error_message,
40
+ **deployment_attrs(payload, status: "failed"))
41
+ else
42
+ @shipper.event("kamal.complete",
43
+ "kamal.command": full_command(payload), "kamal.runtime": runtime,
44
+ **deployment_attrs(payload, status: "succeeded"))
45
+ end
46
+ puts "Logs sent to #{@endpoint}"
47
+ end
48
+
49
+ def on_close
50
+ @shipper.shutdown
51
+ end
52
+
53
+ def full_command(payload)
54
+ [ payload[:command], payload[:subcommand] ].compact.join(" ")
55
+ end
56
+
57
+ def deploy?(payload)
58
+ DEPLOY_COMMANDS.include?(payload[:command])
59
+ end
60
+
61
+ def deployment_attrs(payload, status: nil)
62
+ if deploy?(payload)
63
+ attrs = { "deployment.id": @shipper.run_id, "deployment.name": "#{full_command(payload)} #{@service}" }
64
+ attrs[:"deployment.status"] = status if status
65
+ attrs
66
+ else
67
+ {}
68
+ end
69
+ end
70
+ end
@@ -14,8 +14,12 @@ class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Ba
14
14
  secret_name = secret["Name"]
15
15
  secret_string = JSON.parse(secret["SecretString"])
16
16
 
17
- secret_string.each do |key, value|
18
- results["#{secret_name}/#{key}"] = value
17
+ if secret_string.is_a?(Hash)
18
+ secret_string.each do |key, value|
19
+ results["#{secret_name}/#{key}"] = stringify_secret_value(value)
20
+ end
21
+ else
22
+ results["#{secret_name}"] = stringify_secret_value(secret_string)
19
23
  end
20
24
  rescue JSON::ParserError
21
25
  results["#{secret_name}"] = secret["SecretString"]
@@ -40,6 +44,10 @@ class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Ba
40
44
  end
41
45
  end
42
46
 
47
+ def stringify_secret_value(value)
48
+ value.is_a?(String) ? value : JSON.dump(value)
49
+ end
50
+
43
51
  def check_dependencies!
44
52
  raise RuntimeError, "AWS CLI is not installed" unless cli_installed?
45
53
  end
@@ -47,7 +47,7 @@ class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base
47
47
  end
48
48
 
49
49
  filter_condition = filter_conditions.any? ? "--filter '#{filter_conditions.join(" || ")}'" : ""
50
- items = `passbolt list resources #{filter_condition} #{folders.map { |item| "--folder #{item["id"].to_s.shellescape}" }.join(" ")} --json`
50
+ items = `passbolt list resources #{filter_condition} #{folders.map { |item| "--folder #{item["id"].to_s.shellescape}" }.join(" ")} --column name --column password --json`
51
51
  raise RuntimeError, "Could not read #{secrets} from Passbolt" unless $?.success?
52
52
  items = JSON.parse(items)
53
53
  found_names = items.map { |item| item["name"] }
data/lib/kamal/secrets.rb CHANGED
@@ -3,7 +3,7 @@ require "dotenv"
3
3
  class Kamal::Secrets
4
4
  Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!
5
5
 
6
- def initialize(destination: nil, secrets_path:)
6
+ def initialize(destination: nil, secrets_path: ".kamal/secrets")
7
7
  @destination = destination
8
8
  @secrets_path = secrets_path
9
9
  @mutex = Mutex.new
@@ -19,11 +19,16 @@ class SSHKit::Backend::Abstract
19
19
  JSON.pretty_generate(JSON.parse(capture(*args, **kwargs)))
20
20
  end
21
21
 
22
- def puts_by_host(host, output, type: "App", quiet: false)
23
- unless quiet
24
- puts "#{type} Host: #{host}"
22
+ def puts_by_host(host, output, type: "App", quiet: false, raw: false)
23
+ if raw
24
+ $stdout.binmode
25
+ $stdout.write(output)
26
+ else
27
+ unless quiet
28
+ puts "#{type} Host: #{host}"
29
+ end
30
+ puts "#{output}\n\n"
25
31
  end
26
- puts "#{output}\n\n"
27
32
  end
28
33
 
29
34
  # Our execution pattern is for the CLI execute args lists returned
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "2.11.0"
2
+ VERSION = "2.12.0"
3
3
  end