nocoffee-kamal 2.3.0.1 → 2.3.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb35407e368cbba3b4910024786b8ab7cdde387c95f7fc2c13f42b012e4a70aa
4
- data.tar.gz: ef27660c970f54c9ba62305fa42f308835c661551f117e7964984d3437eb650c
3
+ metadata.gz: 28c14758366da1ecad587823ea0ecaaee14a3584373a6b40ce6f2fe390490970
4
+ data.tar.gz: 7526c6d1c7188edeb94801529a804ec688a3bac00b6d60e22c6b3ee9fe3628c8
5
5
  SHA512:
6
- metadata.gz: d0b0ce4e4a9fe6943b2601f5ae9a50ff6cbe6a3a57d1e085a9f31d19e82aaa13434af30962c5392a794cdad3fb18598b0d9dad57d7cc53f84ad6ec82811f791f
7
- data.tar.gz: 5d6f6279a3c19bea79f62b523b27167f092488c8a75eb6f825f959ee3e6cc09ed2f8d249194d249f68abf513e0d417d5f00a2da9b70f12ecd8726c1003c1173a
6
+ metadata.gz: f9936c64cbfd480b28bfb64b5a93513a54f92efe4fb86aa71f3cbfefc1e11229cf2aec435a733d51990cd345255b981e243043c4f6bbf0d11c1d63a6bc9cbeff
7
+ data.tar.gz: c90910f3d1bfd2dfdf7ddebdd9c6e126a90c0104f8f109c1e41da13235c7c0cc06ea59a3217ceccdf64e8e6082db6a61d4cc5d7d9d5a01e9fa7fffe0b46455f5
@@ -18,6 +18,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
18
18
  execute *accessory.ensure_env_directory
19
19
  upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
20
20
  execute *accessory.run
21
+
22
+ if accessory.running_proxy?
23
+ target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
24
+ execute *accessory.deploy(target: target)
25
+ end
21
26
  end
22
27
  end
23
28
  end
@@ -75,6 +80,10 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
75
80
  on(hosts) do
76
81
  execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
77
82
  execute *accessory.start
83
+ if accessory.running_proxy?
84
+ target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
85
+ execute *accessory.deploy(target: target)
86
+ end
78
87
  end
79
88
  end
80
89
  end
@@ -87,6 +96,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
87
96
  on(hosts) do
88
97
  execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
89
98
  execute *accessory.stop, raise_on_non_zero_exit: false
99
+
100
+ if accessory.running_proxy?
101
+ target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
102
+ execute *accessory.remove if target
103
+ end
90
104
  end
91
105
  end
92
106
  end
@@ -112,14 +126,15 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
112
126
  end
113
127
  end
114
128
 
115
- desc "exec [NAME] [CMD]", "Execute a custom command on servers (use --help to show options)"
129
+ desc "exec [NAME] [CMD...]", "Execute a custom command on servers within the accessory container (use --help to show options)"
116
130
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
117
131
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
118
- def exec(name, cmd)
132
+ def exec(name, *cmd)
133
+ cmd = Kamal::Utils.join_commands(cmd)
119
134
  with_accessory(name) do |accessory, hosts|
120
135
  case
121
136
  when options[:interactive] && options[:reuse]
122
- say "Launching interactive command with via SSH from existing container...", :magenta
137
+ say "Launching interactive command via SSH from existing container...", :magenta
123
138
  run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
124
139
 
125
140
  when options[:interactive]
@@ -128,16 +143,16 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
128
143
 
129
144
  when options[:reuse]
130
145
  say "Launching command from existing container...", :magenta
131
- on(hosts) do
146
+ on(hosts) do |host|
132
147
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
133
- capture_with_info(*accessory.execute_in_existing_container(cmd))
148
+ puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd))
134
149
  end
135
150
 
136
151
  else
137
152
  say "Launching command from new container...", :magenta
138
- on(hosts) do
153
+ on(hosts) do |host|
139
154
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
140
- capture_with_info(*accessory.execute_in_new_container(cmd))
155
+ puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd))
141
156
  end
142
157
  end
143
158
  end
@@ -1,11 +1,17 @@
1
1
  class Kamal::Cli::Secrets < Kamal::Cli::Base
2
2
  desc "fetch [SECRETS...]", "Fetch secrets from a vault"
3
3
  option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
4
- option :account, type: :string, required: true, desc: "The account identifier or username"
4
+ option :account, type: :string, required: false, desc: "The account identifier or username"
5
5
  option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
6
6
  option :inline, type: :boolean, required: false, hidden: true
7
7
  def fetch(*secrets)
8
- results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
8
+ adapter = initialize_adapter(options[:adapter])
9
+
10
+ if adapter.requires_account? && options[:account].blank?
11
+ return puts "No value provided for required options '--account'"
12
+ end
13
+
14
+ results = adapter.fetch(secrets, **options.slice(:account, :from).symbolize_keys)
9
15
 
10
16
  return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
11
17
  end
@@ -29,7 +35,7 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
29
35
  end
30
36
 
31
37
  private
32
- def adapter(adapter)
38
+ def initialize_adapter(adapter)
33
39
  Kamal::Secrets::Adapters.lookup(adapter)
34
40
  end
35
41
 
@@ -16,8 +16,8 @@ servers:
16
16
  # Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
17
17
  # Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
18
18
  #
19
- # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
20
- proxy:
19
+ # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
20
+ proxy:
21
21
  ssl: true
22
22
  host: app.example.com
23
23
  # Proxy connects to your container on port 80 by default.
@@ -36,6 +36,9 @@ registry:
36
36
  # Configure builder setup.
37
37
  builder:
38
38
  arch: amd64
39
+ # Pass in additional build args needed for your Dockerfile.
40
+ # args:
41
+ # RUBY_VERSION: <%= File.read('.ruby-version').strip %>
39
42
 
40
43
  # Inject ENV variables into containers (secrets come from .kamal/secrets).
41
44
  #
@@ -0,0 +1,16 @@
1
+ module Kamal::Commands::Accessory::Proxy
2
+ delegate :proxy_container_name, to: :config
3
+
4
+ def deploy(target:)
5
+ proxy_exec :deploy, service_name, *proxy.deploy_command_args(target: target)
6
+ end
7
+
8
+ def remove
9
+ proxy_exec :remove, service_name
10
+ end
11
+
12
+ private
13
+ def proxy_exec(*command)
14
+ docker :exec, proxy_container_name, "kamal-proxy", *command
15
+ end
16
+ end
@@ -1,9 +1,13 @@
1
1
  class Kamal::Commands::Accessory < Kamal::Commands::Base
2
+ include Proxy
3
+
2
4
  attr_reader :accessory_config
3
5
  delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
4
6
  :network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
5
- :secrets_io, :secrets_path, :env_directory,
7
+ :secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?,
6
8
  to: :accessory_config
9
+ delegate :proxy_container_name, to: :config
10
+
7
11
 
8
12
  def initialize(config, name:)
9
13
  super(config)
@@ -2,7 +2,7 @@ module Kamal::Commands::App::Containers
2
2
  DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
3
3
 
4
4
  def list_containers
5
- docker :container, :ls, "--all", *filter_args
5
+ docker :container, :ls, "--all", *container_filter_args
6
6
  end
7
7
 
8
8
  def list_container_names
@@ -20,7 +20,7 @@ module Kamal::Commands::App::Containers
20
20
  end
21
21
 
22
22
  def remove_containers
23
- docker :container, :prune, "--force", *filter_args
23
+ docker :container, :prune, "--force", *container_filter_args
24
24
  end
25
25
 
26
26
  def container_health_log(version:)
@@ -4,7 +4,7 @@ module Kamal::Commands::App::Images
4
4
  end
5
5
 
6
6
  def remove_images
7
- docker :image, :prune, "--all", "--force", *filter_args
7
+ docker :image, :prune, "--all", "--force", *image_filter_args
8
8
  end
9
9
 
10
10
  def tag_latest_image
@@ -47,7 +47,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
47
47
  end
48
48
 
49
49
  def info
50
- docker :ps, *filter_args
50
+ docker :ps, *container_filter_args
51
51
  end
52
52
 
53
53
 
@@ -67,7 +67,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
67
67
 
68
68
  def list_versions(*docker_args, statuses: nil)
69
69
  pipe \
70
- docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
70
+ docker(:ps, *container_filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
71
71
  extract_version_from_name
72
72
  end
73
73
 
@@ -91,11 +91,15 @@ class Kamal::Commands::App < Kamal::Commands::Base
91
91
  end
92
92
 
93
93
  def latest_container(format:, filters: nil)
94
- docker :ps, "--latest", *format, *filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
94
+ docker :ps, "--latest", *format, *container_filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
95
95
  end
96
96
 
97
- def filter_args(statuses: nil)
98
- argumentize "--filter", filters(statuses: statuses)
97
+ def container_filter_args(statuses: nil)
98
+ argumentize "--filter", container_filters(statuses: statuses)
99
+ end
100
+
101
+ def image_filter_args
102
+ argumentize "--filter", image_filters
99
103
  end
100
104
 
101
105
  def extract_version_from_name
@@ -103,13 +107,17 @@ class Kamal::Commands::App < Kamal::Commands::Base
103
107
  %(while read line; do echo ${line##{role.container_prefix}-}; done)
104
108
  end
105
109
 
106
- def filters(statuses: nil)
110
+ def container_filters(statuses: nil)
107
111
  [ "label=service=#{config.service}" ].tap do |filters|
108
- filters << "label=destination=#{config.destination}" if config.destination
112
+ filters << "label=destination=#{config.destination}"
109
113
  filters << "label=role=#{role}" if role
110
114
  statuses&.each do |status|
111
115
  filters << "status=#{status}"
112
116
  end
113
117
  end
114
118
  end
119
+
120
+ def image_filters
121
+ [ "label=service=#{config.service}" ]
122
+ end
115
123
  end
@@ -6,7 +6,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
6
6
  delegate :argumentize, to: Kamal::Utils
7
7
  delegate \
8
8
  :args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
9
- :cache_from, :cache_to, :ssh, :provenance, :driver, :docker_driver?,
9
+ :cache_from, :cache_to, :ssh, :provenance, :sbom, :driver, :docker_driver?,
10
10
  to: :builder_config
11
11
 
12
12
  def clean
@@ -37,7 +37,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
37
37
  end
38
38
 
39
39
  def build_options
40
- [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance ]
40
+ [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance, *builder_sbom ]
41
41
  end
42
42
 
43
43
  def build_context
@@ -101,6 +101,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
101
101
  argumentize "--provenance", provenance unless provenance.nil?
102
102
  end
103
103
 
104
+ def builder_sbom
105
+ argumentize "--sbom", sbom unless sbom.nil?
106
+ end
107
+
104
108
  def builder_config
105
109
  config.builder
106
110
  end
@@ -5,7 +5,7 @@ class Kamal::Configuration::Accessory
5
5
 
6
6
  delegate :argumentize, :optionize, to: Kamal::Utils
7
7
 
8
- attr_reader :name, :accessory_config, :env
8
+ attr_reader :name, :accessory_config, :env, :proxy
9
9
 
10
10
  def initialize(name, config:)
11
11
  @name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]
@@ -20,6 +20,8 @@ class Kamal::Configuration::Accessory
20
20
  config: accessory_config.fetch("env", {}),
21
21
  secrets: config.secrets,
22
22
  context: "accessories/#{name}/env"
23
+
24
+ initialize_proxy if running_proxy?
23
25
  end
24
26
 
25
27
  def service_name
@@ -106,6 +108,17 @@ class Kamal::Configuration::Accessory
106
108
  accessory_config["cmd"]
107
109
  end
108
110
 
111
+ def running_proxy?
112
+ @accessory_config["proxy"].present?
113
+ end
114
+
115
+ def initialize_proxy
116
+ @proxy = Kamal::Configuration::Proxy.new \
117
+ config: config,
118
+ proxy_config: accessory_config["proxy"],
119
+ context: "accessories/#{name}/proxy"
120
+ end
121
+
109
122
  private
110
123
  attr_accessor :config
111
124
 
@@ -176,7 +189,9 @@ class Kamal::Configuration::Accessory
176
189
 
177
190
  def hosts_from_roles
178
191
  if accessory_config.key?("roles")
179
- accessory_config["roles"].flat_map { |role| config.role(role).hosts }
192
+ accessory_config["roles"].flat_map do |role|
193
+ config.role(role)&.hosts || raise(Kamal::ConfigurationError, "Unknown role in accessories config: '#{role}'")
194
+ end
180
195
  end
181
196
  end
182
197
 
@@ -115,6 +115,10 @@ class Kamal::Configuration::Builder
115
115
  builder_config["provenance"]
116
116
  end
117
117
 
118
+ def sbom
119
+ builder_config["sbom"]
120
+ end
121
+
118
122
  def git_clone?
119
123
  Kamal::Git.used? && builder_config["context"].nil?
120
124
  end
@@ -98,3 +98,7 @@ accessories:
98
98
  # Defaults to kamal:
99
99
  network: custom
100
100
 
101
+ # Proxy
102
+ #
103
+ proxy:
104
+ ...
@@ -108,3 +108,9 @@ builder:
108
108
  # It is used to configure provenance attestations for the build result.
109
109
  # The value can also be a boolean to enable or disable provenance attestations.
110
110
  provenance: mode=max
111
+
112
+ # SBOM (Software Bill of Materials)
113
+ #
114
+ # It is used to configure SBOM generation for the build result.
115
+ # The value can also be a boolean to enable or disable SBOM generation.
116
+ sbom: true
@@ -14,7 +14,7 @@ class Kamal::Configuration
14
14
 
15
15
  include Validation
16
16
 
17
- PROXY_MINIMUM_VERSION = "v0.8.2.6"
17
+ PROXY_MINIMUM_VERSION = "v0.8.2.7"
18
18
  PROXY_HTTP_PORT = 80
19
19
  PROXY_HTTPS_PORT = 443
20
20
  PROXY_LOG_MAX_SIZE = "10m"
@@ -0,0 +1,34 @@
1
+ class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Base
2
+ private
3
+ def login(_account)
4
+ nil
5
+ end
6
+
7
+ def fetch_secrets(secrets, account:, session:)
8
+ {}.tap do |results|
9
+ JSON.parse(get_from_secrets_manager(secrets, account: account))["SecretValues"].each do |secret|
10
+ secret_name = secret["Name"]
11
+ secret_string = JSON.parse(secret["SecretString"])
12
+
13
+ secret_string.each do |key, value|
14
+ results["#{secret_name}/#{key}"] = value
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ def get_from_secrets_manager(secrets, account:)
21
+ `aws secretsmanager batch-get-secret-value --secret-id-list #{secrets.map(&:shellescape).join(" ")} --profile #{account.shellescape}`.tap do
22
+ raise RuntimeError, "Could not read #{secret} from AWS Secrets Manager" unless $?.success?
23
+ end
24
+ end
25
+
26
+ def check_dependencies!
27
+ raise RuntimeError, "AWS CLI is not installed" unless cli_installed?
28
+ end
29
+
30
+ def cli_installed?
31
+ `aws --version 2> /dev/null`
32
+ $?.success?
33
+ end
34
+ end
@@ -1,13 +1,20 @@
1
1
  class Kamal::Secrets::Adapters::Base
2
2
  delegate :optionize, to: Kamal::Utils
3
3
 
4
- def fetch(secrets, account:, from: nil)
4
+ def fetch(secrets, account: nil, from: nil)
5
+ raise RuntimeError, "Missing required option '--account'" if requires_account? && account.blank?
6
+
5
7
  check_dependencies!
8
+
6
9
  session = login(account)
7
10
  full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
8
11
  fetch_secrets(full_secrets, account: account, session: session)
9
12
  end
10
13
 
14
+ def requires_account?
15
+ true
16
+ end
17
+
11
18
  private
12
19
  def login(...)
13
20
  raise NotImplementedError
@@ -0,0 +1,53 @@
1
+ class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base
2
+ def requires_account?
3
+ false
4
+ end
5
+
6
+ private
7
+ def login(*)
8
+ unless loggedin?
9
+ `doppler login -y`
10
+ raise RuntimeError, "Failed to login to Doppler" unless $?.success?
11
+ end
12
+ end
13
+
14
+ def loggedin?
15
+ `doppler me --json 2> /dev/null`
16
+ $?.success?
17
+ end
18
+
19
+ def fetch_secrets(secrets, **)
20
+ project_and_config_flags = ""
21
+ unless service_token_set?
22
+ project, config, _ = secrets.first.split("/")
23
+
24
+ unless project && config
25
+ raise RuntimeError, "Missing project or config from '--from=project/config' option"
26
+ end
27
+
28
+ project_and_config_flags = "-p #{project.shellescape} -c #{config.shellescape}"
29
+ end
30
+
31
+ secret_names = secrets.collect { |s| s.split("/").last }
32
+
33
+ items = `doppler secrets get #{secret_names.map(&:shellescape).join(" ")} --json #{project_and_config_flags}`
34
+ raise RuntimeError, "Could not read #{secrets} from Doppler" unless $?.success?
35
+
36
+ items = JSON.parse(items)
37
+
38
+ items.transform_values { |value| value["computed"] }
39
+ end
40
+
41
+ def service_token_set?
42
+ ENV["DOPPLER_TOKEN"] && ENV["DOPPLER_TOKEN"][0, 5] == "dp.st"
43
+ end
44
+
45
+ def check_dependencies!
46
+ raise RuntimeError, "Doppler CLI is not installed" unless cli_installed?
47
+ end
48
+
49
+ def cli_installed?
50
+ `doppler --version 2> /dev/null`
51
+ $?.success?
52
+ end
53
+ end
@@ -0,0 +1,5 @@
1
+ class Kamal::Secrets::Adapters::TestOptionalAccount < Kamal::Secrets::Adapters::Test
2
+ def requires_account?
3
+ false
4
+ end
5
+ end
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "2.3.0.1"
2
+ VERSION = "2.3.0.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nocoffee-kamal
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0.1
4
+ version: 2.3.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-04 00:00:00.000000000 Z
11
+ date: 2024-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -204,7 +204,7 @@ dependencies:
204
204
  - - ">="
205
205
  - !ruby/object:Gem::Version
206
206
  version: '0'
207
- description: Kamal with the TLS on demand feature
207
+ description:
208
208
  email: dhh@hey.com
209
209
  executables:
210
210
  - kamal
@@ -247,6 +247,7 @@ files:
247
247
  - lib/kamal/commander/specifics.rb
248
248
  - lib/kamal/commands.rb
249
249
  - lib/kamal/commands/accessory.rb
250
+ - lib/kamal/commands/accessory/proxy.rb
250
251
  - lib/kamal/commands/app.rb
251
252
  - lib/kamal/commands/app/assets.rb
252
253
  - lib/kamal/commands/app/containers.rb
@@ -312,11 +313,14 @@ files:
312
313
  - lib/kamal/git.rb
313
314
  - lib/kamal/secrets.rb
314
315
  - lib/kamal/secrets/adapters.rb
316
+ - lib/kamal/secrets/adapters/aws_secrets_manager.rb
315
317
  - lib/kamal/secrets/adapters/base.rb
316
318
  - lib/kamal/secrets/adapters/bitwarden.rb
319
+ - lib/kamal/secrets/adapters/doppler.rb
317
320
  - lib/kamal/secrets/adapters/last_pass.rb
318
321
  - lib/kamal/secrets/adapters/one_password.rb
319
322
  - lib/kamal/secrets/adapters/test.rb
323
+ - lib/kamal/secrets/adapters/test_optional_account.rb
320
324
  - lib/kamal/secrets/dotenv/inline_command_substitution.rb
321
325
  - lib/kamal/sshkit_with_ext.rb
322
326
  - lib/kamal/tags.rb
@@ -342,8 +346,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
342
346
  - !ruby/object:Gem::Version
343
347
  version: '0'
344
348
  requirements: []
345
- rubygems_version: 3.5.19
349
+ rubygems_version: 3.5.23
346
350
  signing_key:
347
351
  specification_version: 4
348
- summary: Deploy web apps in containers to servers running Docker with zero downtime
352
+ summary: Kamal with the TLS on demand feature.
349
353
  test_files: []