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.
- checksums.yaml +4 -4
- data/lib/kamal/cli/accessory.rb +48 -39
- data/lib/kamal/cli/app.rb +57 -48
- data/lib/kamal/cli/base.rb +99 -11
- data/lib/kamal/cli/build.rb +9 -6
- data/lib/kamal/cli/lock.rb +5 -16
- data/lib/kamal/cli/main.rb +59 -53
- data/lib/kamal/cli/proxy.rb +9 -9
- data/lib/kamal/cli/prune.rb +3 -3
- data/lib/kamal/cli/server.rb +24 -15
- data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +1 -1
- data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +1 -1
- data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +1 -1
- data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +1 -1
- data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +1 -1
- data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +1 -1
- data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +1 -1
- data/lib/kamal/cli/templates/secrets +4 -0
- data/lib/kamal/commander.rb +54 -1
- data/lib/kamal/commands/accessory.rb +2 -2
- data/lib/kamal/commands/app/logging.rb +1 -1
- data/lib/kamal/commands/app.rb +1 -1
- data/lib/kamal/commands/builder/clone.rb +2 -1
- data/lib/kamal/configuration/accessory.rb +13 -5
- data/lib/kamal/configuration/docs/configuration.yml +18 -3
- data/lib/kamal/configuration/docs/env.yml +6 -4
- data/lib/kamal/configuration/docs/output.yml +25 -0
- data/lib/kamal/configuration/docs/role.yml +1 -0
- data/lib/kamal/configuration/docs/ssh.yml +8 -0
- data/lib/kamal/configuration/output.rb +34 -0
- data/lib/kamal/configuration/proxy/run.rb +9 -0
- data/lib/kamal/configuration/role.rb +18 -6
- data/lib/kamal/configuration/ssh.rb +5 -1
- data/lib/kamal/configuration/validator.rb +14 -2
- data/lib/kamal/configuration.rb +6 -1
- data/lib/kamal/git.rb +1 -1
- data/lib/kamal/otel_shipper.rb +176 -0
- data/lib/kamal/output/base_logger.rb +29 -0
- data/lib/kamal/output/file_logger.rb +51 -0
- data/lib/kamal/output/formatter.rb +36 -0
- data/lib/kamal/output/otel_logger.rb +70 -0
- data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +10 -2
- data/lib/kamal/secrets/adapters/passbolt.rb +1 -1
- data/lib/kamal/secrets.rb +1 -1
- data/lib/kamal/sshkit_with_ext.rb +9 -4
- data/lib/kamal/version.rb +1 -1
- 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/
|
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/kamal/configuration.rb
CHANGED
|
@@ -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
|
@@ -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.
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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