kamal 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
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