kamal 2.4.0 → 2.5.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +1 -1
  3. data/lib/kamal/cli/alias/command.rb +1 -0
  4. data/lib/kamal/cli/app.rb +15 -3
  5. data/lib/kamal/cli/base.rb +16 -1
  6. data/lib/kamal/cli/build.rb +36 -14
  7. data/lib/kamal/cli/main.rb +4 -3
  8. data/lib/kamal/cli/proxy.rb +2 -4
  9. data/lib/kamal/cli/registry.rb +2 -0
  10. data/lib/kamal/cli/templates/deploy.yml +2 -2
  11. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  12. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  13. data/lib/kamal/cli.rb +1 -0
  14. data/lib/kamal/commander.rb +16 -25
  15. data/lib/kamal/commands/accessory.rb +1 -5
  16. data/lib/kamal/commands/app/assets.rb +4 -4
  17. data/lib/kamal/commands/base.rb +14 -0
  18. data/lib/kamal/commands/builder/base.rb +12 -5
  19. data/lib/kamal/commands/builder/cloud.rb +22 -0
  20. data/lib/kamal/commands/builder.rb +6 -20
  21. data/lib/kamal/commands/registry.rb +9 -7
  22. data/lib/kamal/configuration/accessory.rb +36 -19
  23. data/lib/kamal/configuration/builder.rb +4 -0
  24. data/lib/kamal/configuration/docs/accessory.yml +20 -1
  25. data/lib/kamal/configuration/docs/builder.yml +3 -0
  26. data/lib/kamal/configuration/registry.rb +6 -6
  27. data/lib/kamal/configuration/role.rb +6 -6
  28. data/lib/kamal/configuration/validator/role.rb +1 -1
  29. data/lib/kamal/configuration.rb +29 -12
  30. data/lib/kamal/docker.rb +30 -0
  31. data/lib/kamal/git.rb +10 -0
  32. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +12 -4
  33. data/lib/kamal/secrets/adapters/base.rb +5 -2
  34. data/lib/kamal/secrets/adapters/bitwarden.rb +2 -2
  35. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +72 -0
  36. data/lib/kamal/secrets/adapters/doppler.rb +15 -11
  37. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  38. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  39. data/lib/kamal/secrets/adapters/last_pass.rb +3 -2
  40. data/lib/kamal/secrets/adapters/one_password.rb +2 -2
  41. data/lib/kamal/secrets/adapters/test.rb +2 -2
  42. data/lib/kamal/secrets/adapters.rb +2 -0
  43. data/lib/kamal/version.rb +1 -1
  44. metadata +10 -4
  45. data/lib/kamal/secrets/adapters/test_optional_account.rb +0 -5
@@ -0,0 +1,112 @@
1
+ class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Base
2
+ private
3
+ def login(account)
4
+ # Since only the account option is passed from the cli, we'll use it for both account and service account
5
+ # impersonation.
6
+ #
7
+ # Syntax:
8
+ # ACCOUNT: USER | USER "|" DELEGATION_CHAIN
9
+ # USER: DEFAULT_USER | EMAIL
10
+ # DELEGATION_CHAIN: EMAIL | EMAIL "," DELEGATION_CHAIN
11
+ # EMAIL: <The email address of the user or service account, like "my-user@example.com" >
12
+ # DEFAULT_USER: "default"
13
+ #
14
+ # Some valid examples:
15
+ # - "my-user@example.com" sets the user
16
+ # - "my-user@example.com|my-service-user@example.com" will use my-user and enable service account impersonation as my-service-user
17
+ # - "default" will use the default user and no impersonation
18
+ # - "default|my-service-user@example.com" will use the default user, and enable service account impersonation as my-service-user
19
+ # - "default|my-service-user@example.com,another-service-user@example.com" same as above, but with an impersonation delegation chain
20
+
21
+ unless logged_in?
22
+ `gcloud auth login`
23
+ raise RuntimeError, "could not login to gcloud" unless logged_in?
24
+ end
25
+
26
+ nil
27
+ end
28
+
29
+ def fetch_secrets(secrets, from:, account:, session:)
30
+ user, service_account = parse_account(account)
31
+
32
+ {}.tap do |results|
33
+ secrets_with_metadata(prefixed_secrets(secrets, from: from)).each do |secret, (project, secret_name, secret_version)|
34
+ item_name = "#{project}/#{secret_name}"
35
+ results[item_name] = fetch_secret(project, secret_name, secret_version, user, service_account)
36
+ raise RuntimeError, "Could not read #{item_name} from Google Secret Manager" unless $?.success?
37
+ end
38
+ end
39
+ end
40
+
41
+ def fetch_secret(project, secret_name, secret_version, user, service_account)
42
+ secret = run_command(
43
+ "secrets versions access #{secret_version.shellescape} --secret=#{secret_name.shellescape}",
44
+ project: project,
45
+ user: user,
46
+ service_account: service_account
47
+ )
48
+ Base64.decode64(secret.dig("payload", "data"))
49
+ end
50
+
51
+ # The secret needs to at least contain a secret name, but project name, and secret version can also be specified.
52
+ #
53
+ # The string "default" can be used to refer to the default project configured for gcloud.
54
+ #
55
+ # The version can be either the string "latest", or a version number.
56
+ #
57
+ # The following formats are valid:
58
+ #
59
+ # - The following are all equivalent, and sets project: default, secret name: my-secret, version: latest
60
+ # - "my-secret"
61
+ # - "default/my-secret"
62
+ # - "default/my-secret/latest"
63
+ # - "my-secret/latest" in combination with --from=default
64
+ # - "my-secret/123" (only in combination with --from=some-project) -> project: some-project, secret name: my-secret, version: 123
65
+ # - "some-project/my-secret/123" -> project: some-project, secret name: my-secret, version: 123
66
+ def secrets_with_metadata(secrets)
67
+ {}.tap do |items|
68
+ secrets.each do |secret|
69
+ parts = secret.split("/")
70
+ parts.unshift("default") if parts.length == 1
71
+ project = parts.shift
72
+ secret_name = parts.shift
73
+ secret_version = parts.shift || "latest"
74
+
75
+ items[secret] = [ project, secret_name, secret_version ]
76
+ end
77
+ end
78
+ end
79
+
80
+ def run_command(command, project: "default", user: "default", service_account: nil)
81
+ full_command = [ "gcloud", command ]
82
+ full_command << "--project=#{project.shellescape}" unless project == "default"
83
+ full_command << "--account=#{user.shellescape}" unless user == "default"
84
+ full_command << "--impersonate-service-account=#{service_account.shellescape}" if service_account
85
+ full_command << "--format=json"
86
+ full_command = full_command.join(" ")
87
+
88
+ result = `#{full_command}`.strip
89
+ JSON.parse(result)
90
+ end
91
+
92
+ def check_dependencies!
93
+ raise RuntimeError, "gcloud CLI is not installed" unless cli_installed?
94
+ end
95
+
96
+ def cli_installed?
97
+ `gcloud --version 2> /dev/null`
98
+ $?.success?
99
+ end
100
+
101
+ def logged_in?
102
+ JSON.parse(`gcloud auth list --format=json`).any?
103
+ end
104
+
105
+ def parse_account(account)
106
+ account.split("|", 2)
107
+ end
108
+
109
+ def is_user?(candidate)
110
+ candidate.include?("@")
111
+ end
112
+ end
@@ -11,7 +11,8 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
11
11
  `lpass status --color never`.strip == "Logged in as #{account}."
12
12
  end
13
13
 
14
- def fetch_secrets(secrets, account:, session:)
14
+ def fetch_secrets(secrets, from:, account:, session:)
15
+ secrets = prefixed_secrets(secrets, from: from)
15
16
  items = `lpass show #{secrets.map(&:shellescape).join(" ")} --json`
16
17
  raise RuntimeError, "Could not read #{secrets} from LastPass" unless $?.success?
17
18
 
@@ -23,7 +24,7 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
23
24
  end
24
25
 
25
26
  if (missing_items = secrets - results.keys).any?
26
- raise RuntimeError, "Could not find #{missing_items.join(", ")} in LassPass"
27
+ raise RuntimeError, "Could not find #{missing_items.join(", ")} in LastPass"
27
28
  end
28
29
  end
29
30
  end
@@ -15,9 +15,9 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
15
15
  $?.success?
16
16
  end
17
17
 
18
- def fetch_secrets(secrets, account:, session:)
18
+ def fetch_secrets(secrets, from:, account:, session:)
19
19
  {}.tap do |results|
20
- vaults_items_fields(secrets).map do |vault, items|
20
+ vaults_items_fields(prefixed_secrets(secrets, from: from)).map do |vault, items|
21
21
  items.each do |item, fields|
22
22
  fields_json = JSON.parse(op_item_get(vault, item, fields, account: account, session: session))
23
23
  fields_json = [ fields_json ] if fields.one?
@@ -4,8 +4,8 @@ class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
4
4
  true
5
5
  end
6
6
 
7
- def fetch_secrets(secrets, account:, session:)
8
- secrets.to_h { |secret| [ secret, secret.reverse ] }
7
+ def fetch_secrets(secrets, from:, account:, session:)
8
+ prefixed_secrets(secrets, from: from).to_h { |secret| [ secret, secret.reverse ] }
9
9
  end
10
10
 
11
11
  def check_dependencies!
@@ -3,6 +3,8 @@ module Kamal::Secrets::Adapters
3
3
  def self.lookup(name)
4
4
  name = "one_password" if name.downcase == "1password"
5
5
  name = "last_pass" if name.downcase == "lastpass"
6
+ name = "gcp_secret_manager" if name.downcase == "gcp"
7
+ name = "bitwarden_secrets_manager" if name.downcase == "bitwarden-sm"
6
8
  adapter_class(name)
7
9
  end
8
10
 
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "2.4.0"
2
+ VERSION = "2.5.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.5.0
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-12-13 00:00:00.000000000 Z
11
+ date: 2025-02-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -236,8 +236,10 @@ files:
236
236
  - lib/kamal/cli/server.rb
237
237
  - lib/kamal/cli/templates/deploy.yml
238
238
  - lib/kamal/cli/templates/sample_hooks/docker-setup.sample
239
+ - lib/kamal/cli/templates/sample_hooks/post-app-boot.sample
239
240
  - lib/kamal/cli/templates/sample_hooks/post-deploy.sample
240
241
  - lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample
242
+ - lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample
241
243
  - lib/kamal/cli/templates/sample_hooks/pre-build.sample
242
244
  - lib/kamal/cli/templates/sample_hooks/pre-connect.sample
243
245
  - lib/kamal/cli/templates/sample_hooks/pre-deploy.sample
@@ -260,6 +262,7 @@ files:
260
262
  - lib/kamal/commands/builder.rb
261
263
  - lib/kamal/commands/builder/base.rb
262
264
  - lib/kamal/commands/builder/clone.rb
265
+ - lib/kamal/commands/builder/cloud.rb
263
266
  - lib/kamal/commands/builder/hybrid.rb
264
267
  - lib/kamal/commands/builder/local.rb
265
268
  - lib/kamal/commands/builder/remote.rb
@@ -309,6 +312,7 @@ files:
309
312
  - lib/kamal/configuration/validator/role.rb
310
313
  - lib/kamal/configuration/validator/servers.rb
311
314
  - lib/kamal/configuration/volume.rb
315
+ - lib/kamal/docker.rb
312
316
  - lib/kamal/env_file.rb
313
317
  - lib/kamal/git.rb
314
318
  - lib/kamal/secrets.rb
@@ -316,11 +320,13 @@ files:
316
320
  - lib/kamal/secrets/adapters/aws_secrets_manager.rb
317
321
  - lib/kamal/secrets/adapters/base.rb
318
322
  - lib/kamal/secrets/adapters/bitwarden.rb
323
+ - lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb
319
324
  - lib/kamal/secrets/adapters/doppler.rb
325
+ - lib/kamal/secrets/adapters/enpass.rb
326
+ - lib/kamal/secrets/adapters/gcp_secret_manager.rb
320
327
  - lib/kamal/secrets/adapters/last_pass.rb
321
328
  - lib/kamal/secrets/adapters/one_password.rb
322
329
  - lib/kamal/secrets/adapters/test.rb
323
- - lib/kamal/secrets/adapters/test_optional_account.rb
324
330
  - lib/kamal/secrets/dotenv/inline_command_substitution.rb
325
331
  - lib/kamal/sshkit_with_ext.rb
326
332
  - lib/kamal/tags.rb
@@ -346,7 +352,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
346
352
  - !ruby/object:Gem::Version
347
353
  version: '0'
348
354
  requirements: []
349
- rubygems_version: 3.3.22
355
+ rubygems_version: 3.5.22
350
356
  signing_key:
351
357
  specification_version: 4
352
358
  summary: Deploy web apps in containers to servers running Docker with zero downtime.
@@ -1,5 +0,0 @@
1
- class Kamal::Secrets::Adapters::TestOptionalAccount < Kamal::Secrets::Adapters::Test
2
- def requires_account?
3
- false
4
- end
5
- end