kamal-insecure 2.7.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 (130) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +13 -0
  4. data/bin/kamal +18 -0
  5. data/lib/kamal/cli/accessory.rb +313 -0
  6. data/lib/kamal/cli/alias/command.rb +10 -0
  7. data/lib/kamal/cli/app/assets.rb +24 -0
  8. data/lib/kamal/cli/app/boot.rb +126 -0
  9. data/lib/kamal/cli/app/error_pages.rb +33 -0
  10. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  11. data/lib/kamal/cli/app.rb +400 -0
  12. data/lib/kamal/cli/base.rb +223 -0
  13. data/lib/kamal/cli/build/clone.rb +61 -0
  14. data/lib/kamal/cli/build.rb +204 -0
  15. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  16. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  17. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  18. data/lib/kamal/cli/lock.rb +45 -0
  19. data/lib/kamal/cli/main.rb +277 -0
  20. data/lib/kamal/cli/proxy.rb +290 -0
  21. data/lib/kamal/cli/prune.rb +34 -0
  22. data/lib/kamal/cli/registry.rb +19 -0
  23. data/lib/kamal/cli/secrets.rb +49 -0
  24. data/lib/kamal/cli/server.rb +50 -0
  25. data/lib/kamal/cli/templates/deploy.yml +101 -0
  26. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  27. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  28. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  29. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  30. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  31. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  32. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  33. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +122 -0
  34. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  35. data/lib/kamal/cli/templates/secrets +17 -0
  36. data/lib/kamal/cli.rb +9 -0
  37. data/lib/kamal/commander/specifics.rb +62 -0
  38. data/lib/kamal/commander.rb +167 -0
  39. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  40. data/lib/kamal/commands/accessory.rb +113 -0
  41. data/lib/kamal/commands/app/assets.rb +51 -0
  42. data/lib/kamal/commands/app/containers.rb +31 -0
  43. data/lib/kamal/commands/app/error_pages.rb +9 -0
  44. data/lib/kamal/commands/app/execution.rb +32 -0
  45. data/lib/kamal/commands/app/images.rb +13 -0
  46. data/lib/kamal/commands/app/logging.rb +28 -0
  47. data/lib/kamal/commands/app/proxy.rb +32 -0
  48. data/lib/kamal/commands/app.rb +124 -0
  49. data/lib/kamal/commands/auditor.rb +39 -0
  50. data/lib/kamal/commands/base.rb +134 -0
  51. data/lib/kamal/commands/builder/base.rb +124 -0
  52. data/lib/kamal/commands/builder/clone.rb +31 -0
  53. data/lib/kamal/commands/builder/cloud.rb +22 -0
  54. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  55. data/lib/kamal/commands/builder/local.rb +14 -0
  56. data/lib/kamal/commands/builder/pack.rb +46 -0
  57. data/lib/kamal/commands/builder/remote.rb +63 -0
  58. data/lib/kamal/commands/builder.rb +48 -0
  59. data/lib/kamal/commands/docker.rb +34 -0
  60. data/lib/kamal/commands/hook.rb +20 -0
  61. data/lib/kamal/commands/lock.rb +70 -0
  62. data/lib/kamal/commands/proxy.rb +127 -0
  63. data/lib/kamal/commands/prune.rb +38 -0
  64. data/lib/kamal/commands/registry.rb +16 -0
  65. data/lib/kamal/commands/server.rb +15 -0
  66. data/lib/kamal/commands.rb +2 -0
  67. data/lib/kamal/configuration/accessory.rb +241 -0
  68. data/lib/kamal/configuration/alias.rb +15 -0
  69. data/lib/kamal/configuration/boot.rb +25 -0
  70. data/lib/kamal/configuration/builder.rb +211 -0
  71. data/lib/kamal/configuration/docs/accessory.yml +128 -0
  72. data/lib/kamal/configuration/docs/alias.yml +26 -0
  73. data/lib/kamal/configuration/docs/boot.yml +19 -0
  74. data/lib/kamal/configuration/docs/builder.yml +132 -0
  75. data/lib/kamal/configuration/docs/configuration.yml +184 -0
  76. data/lib/kamal/configuration/docs/env.yml +116 -0
  77. data/lib/kamal/configuration/docs/logging.yml +21 -0
  78. data/lib/kamal/configuration/docs/proxy.yml +164 -0
  79. data/lib/kamal/configuration/docs/registry.yml +56 -0
  80. data/lib/kamal/configuration/docs/role.yml +53 -0
  81. data/lib/kamal/configuration/docs/servers.yml +27 -0
  82. data/lib/kamal/configuration/docs/ssh.yml +70 -0
  83. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  84. data/lib/kamal/configuration/env/tag.rb +13 -0
  85. data/lib/kamal/configuration/env.rb +38 -0
  86. data/lib/kamal/configuration/logging.rb +33 -0
  87. data/lib/kamal/configuration/proxy/boot.rb +129 -0
  88. data/lib/kamal/configuration/proxy.rb +124 -0
  89. data/lib/kamal/configuration/registry.rb +32 -0
  90. data/lib/kamal/configuration/role.rb +222 -0
  91. data/lib/kamal/configuration/servers.rb +25 -0
  92. data/lib/kamal/configuration/ssh.rb +57 -0
  93. data/lib/kamal/configuration/sshkit.rb +22 -0
  94. data/lib/kamal/configuration/validation.rb +27 -0
  95. data/lib/kamal/configuration/validator/accessory.rb +13 -0
  96. data/lib/kamal/configuration/validator/alias.rb +15 -0
  97. data/lib/kamal/configuration/validator/builder.rb +15 -0
  98. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  99. data/lib/kamal/configuration/validator/env.rb +54 -0
  100. data/lib/kamal/configuration/validator/proxy.rb +25 -0
  101. data/lib/kamal/configuration/validator/registry.rb +25 -0
  102. data/lib/kamal/configuration/validator/role.rb +13 -0
  103. data/lib/kamal/configuration/validator/servers.rb +7 -0
  104. data/lib/kamal/configuration/validator.rb +191 -0
  105. data/lib/kamal/configuration/volume.rb +22 -0
  106. data/lib/kamal/configuration.rb +372 -0
  107. data/lib/kamal/docker.rb +30 -0
  108. data/lib/kamal/env_file.rb +44 -0
  109. data/lib/kamal/git.rb +37 -0
  110. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -0
  111. data/lib/kamal/secrets/adapters/base.rb +33 -0
  112. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  113. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  114. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  115. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  116. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  117. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  118. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  119. data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
  120. data/lib/kamal/secrets/adapters/test.rb +14 -0
  121. data/lib/kamal/secrets/adapters.rb +16 -0
  122. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +33 -0
  123. data/lib/kamal/secrets.rb +42 -0
  124. data/lib/kamal/sshkit_with_ext.rb +142 -0
  125. data/lib/kamal/tags.rb +40 -0
  126. data/lib/kamal/utils/sensitive.rb +20 -0
  127. data/lib/kamal/utils.rb +110 -0
  128. data/lib/kamal/version.rb +3 -0
  129. data/lib/kamal.rb +14 -0
  130. metadata +365 -0
@@ -0,0 +1,54 @@
1
+ class Kamal::Configuration::Validator::Env < Kamal::Configuration::Validator
2
+ SPECIAL_KEYS = [ "clear", "secret", "tags" ]
3
+
4
+ def validate!
5
+ if known_keys.any?
6
+ validate_complex_env!
7
+ else
8
+ validate_simple_env!
9
+ end
10
+ end
11
+
12
+ private
13
+ def validate_simple_env!
14
+ validate_hash_of!(config, String)
15
+ end
16
+
17
+ def validate_complex_env!
18
+ unknown_keys_error unknown_keys if unknown_keys.any?
19
+
20
+ with_context("clear") { validate_hash_of!(config["clear"], String) } if config.key?("clear")
21
+ with_context("secret") { validate_array_of!(config["secret"], String) } if config.key?("secret")
22
+ validate_tags! if config.key?("tags")
23
+ end
24
+
25
+ def known_keys
26
+ @known_keys ||= config.keys & SPECIAL_KEYS
27
+ end
28
+
29
+ def unknown_keys
30
+ @unknown_keys ||= config.keys - SPECIAL_KEYS
31
+ end
32
+
33
+ def validate_tags!
34
+ if context == "env"
35
+ with_context("tags") do
36
+ validate_type! config["tags"], Hash
37
+
38
+ config["tags"].each do |tag, value|
39
+ with_context(tag) do
40
+ validate_type! value, Hash
41
+
42
+ Kamal::Configuration::Validator::Env.new(
43
+ value,
44
+ example: example["tags"].values[1],
45
+ context: context
46
+ ).validate!
47
+ end
48
+ end
49
+ end
50
+ else
51
+ error "tags are only allowed in the root env"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
2
+ def validate!
3
+ unless config.nil?
4
+ super
5
+
6
+ if config["host"].blank? && config["hosts"].blank? && config["ssl"]
7
+ error "Must set a host to enable automatic SSL"
8
+ end
9
+
10
+ if (config.keys & [ "host", "hosts" ]).size > 1
11
+ error "Specify one of 'host' or 'hosts', not both"
12
+ end
13
+
14
+ if config["ssl"].is_a?(Hash)
15
+ if config["ssl"]["certificate_pem"].present? && config["ssl"]["private_key_pem"].blank?
16
+ error "Missing private_key_pem setting (required when certificate_pem is present)"
17
+ end
18
+
19
+ if config["ssl"]["private_key_pem"].present? && config["ssl"]["certificate_pem"].blank?
20
+ error "Missing certificate_pem setting (required when private_key_pem is present)"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ class Kamal::Configuration::Validator::Registry < Kamal::Configuration::Validator
2
+ STRING_OR_ONE_ITEM_ARRAY_KEYS = [ "username", "password" ]
3
+
4
+ def validate!
5
+ validate_against_example! \
6
+ config.except(*STRING_OR_ONE_ITEM_ARRAY_KEYS),
7
+ example.except(*STRING_OR_ONE_ITEM_ARRAY_KEYS)
8
+
9
+ validate_string_or_one_item_array! "username"
10
+ validate_string_or_one_item_array! "password"
11
+ end
12
+
13
+ private
14
+ def validate_string_or_one_item_array!(key)
15
+ with_context(key) do
16
+ value = config[key]
17
+
18
+ error "is required" unless value.present?
19
+
20
+ unless value.is_a?(String) || (value.is_a?(Array) && value.size == 1 && value.first.is_a?(String))
21
+ error "should be a string or an array with one string (for secret lookup)"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ class Kamal::Configuration::Validator::Role < Kamal::Configuration::Validator
2
+ def validate!
3
+ validate_type! config, Array, Hash
4
+
5
+ if config.is_a?(Array)
6
+ validate_servers!(config)
7
+ else
8
+ super
9
+ validate_labels!(config["labels"])
10
+ validate_docker_options!(config["options"])
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ class Kamal::Configuration::Validator::Servers < Kamal::Configuration::Validator
2
+ def validate!
3
+ validate_type! config, Array, Hash, NilClass
4
+
5
+ validate_servers! config if config.is_a?(Array)
6
+ end
7
+ end
@@ -0,0 +1,191 @@
1
+ class Kamal::Configuration::Validator
2
+ attr_reader :config, :example, :context
3
+
4
+ def initialize(config, example:, context:)
5
+ @config = config
6
+ @example = example
7
+ @context = context
8
+ end
9
+
10
+ def validate!
11
+ validate_against_example! config, example
12
+ end
13
+
14
+ private
15
+ def validate_against_example!(validation_config, example)
16
+ validate_type! validation_config, example.class
17
+
18
+ if example.class == Hash
19
+ check_unknown_keys! validation_config, example
20
+
21
+ validation_config.each do |key, value|
22
+ next if extension?(key)
23
+ with_context(key) do
24
+ example_value = example[key]
25
+
26
+ if example_value == "..."
27
+ unless key.to_s == "proxy" && boolean?(value.class)
28
+ validate_type! value, *(Array if key == :servers), Hash
29
+ end
30
+ elsif key.to_s == "ssl"
31
+ validate_type! value, TrueClass, FalseClass, Hash
32
+ elsif key == "hosts"
33
+ validate_servers! value
34
+ elsif example_value.is_a?(Array)
35
+ if key == "arch"
36
+ validate_array_of_or_type! value, example_value.first.class
37
+ else
38
+ validate_array_of! value, example_value.first.class
39
+ end
40
+ elsif example_value.is_a?(Hash)
41
+ case key.to_s
42
+ when "options", "args"
43
+ validate_type! value, Hash
44
+ when "labels"
45
+ validate_hash_of! value, example_value.first[1].class
46
+ else
47
+ validate_against_example! value, example_value
48
+ end
49
+ else
50
+ validate_type! value, example_value.class
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+
58
+ def valid_type?(value, type)
59
+ value.is_a?(type) ||
60
+ (type == String && stringish?(value)) ||
61
+ (boolean?(type) && boolean?(value.class))
62
+ end
63
+
64
+ def type_description(type)
65
+ if type == Integer || type == Array
66
+ "an #{type.name.downcase}"
67
+ elsif type == TrueClass || type == FalseClass
68
+ "a boolean"
69
+ else
70
+ "a #{type.name.downcase}"
71
+ end
72
+ end
73
+
74
+ def boolean?(type)
75
+ type == TrueClass || type == FalseClass
76
+ end
77
+
78
+ def stringish?(value)
79
+ value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
80
+ end
81
+
82
+ def validate_array_of_or_type!(value, type)
83
+ if value.is_a?(Array)
84
+ validate_array_of! value, type
85
+ else
86
+ validate_type! value, type
87
+ end
88
+ rescue Kamal::ConfigurationError
89
+ type_error(Array, type)
90
+ end
91
+
92
+ def validate_array_of!(array, type)
93
+ validate_type! array, Array
94
+
95
+ array.each_with_index do |value, index|
96
+ with_context(index) do
97
+ validate_type! value, type
98
+ end
99
+ end
100
+ end
101
+
102
+ def validate_hash_of!(hash, type)
103
+ validate_type! hash, Hash
104
+
105
+ hash.each do |key, value|
106
+ with_context(key) do
107
+ validate_type! value, type
108
+ end
109
+ end
110
+ end
111
+
112
+ def validate_servers!(servers)
113
+ validate_type! servers, Array
114
+
115
+ servers.each_with_index do |server, index|
116
+ with_context(index) do
117
+ validate_type! server, String, Hash
118
+
119
+ if server.is_a?(Hash)
120
+ error "multiple hosts found" unless server.size == 1
121
+ host, tags = server.first
122
+
123
+ with_context(host) do
124
+ validate_type! tags, String, Array
125
+ validate_array_of! tags, String if tags.is_a?(Array)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ def validate_type!(value, *types)
133
+ type_error(*types) unless types.any? { |type| valid_type?(value, type) }
134
+ end
135
+
136
+ def error(message)
137
+ raise Kamal::ConfigurationError, "#{error_context}#{message}"
138
+ end
139
+
140
+ def type_error(*expected_types)
141
+ error "should be #{expected_types.map { |type| type_description(type) }.join(" or ")}"
142
+ end
143
+
144
+ def unknown_keys_error(unknown_keys)
145
+ error "unknown #{"key".pluralize(unknown_keys.count)}: #{unknown_keys.join(", ")}"
146
+ end
147
+
148
+ def error_context
149
+ "#{context}: " if context.present?
150
+ end
151
+
152
+ def with_context(context)
153
+ old_context = @context
154
+ @context = [ @context, context ].select(&:present?).join("/")
155
+ yield
156
+ ensure
157
+ @context = old_context
158
+ end
159
+
160
+ def allow_extensions?
161
+ false
162
+ end
163
+
164
+ def extension?(key)
165
+ key.to_s.start_with?("x-")
166
+ end
167
+
168
+ def check_unknown_keys!(config, example)
169
+ unknown_keys = config.keys - example.keys
170
+ unknown_keys.reject! { |key| extension?(key) } if allow_extensions?
171
+ unknown_keys_error unknown_keys if unknown_keys.present?
172
+ end
173
+
174
+ def validate_labels!(labels)
175
+ return true if labels.blank?
176
+
177
+ with_context("labels") do
178
+ labels.each do |key, _|
179
+ with_context(key) do
180
+ error "invalid label. destination, role, and service are reserved labels" if %w[destination role service].include?(key)
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ def validate_docker_options!(options)
187
+ if options
188
+ error "Cannot set restart policy in docker options, unless-stopped is required" if options["restart"]
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,22 @@
1
+ class Kamal::Configuration::Volume
2
+ attr_reader :host_path, :container_path
3
+ delegate :argumentize, to: Kamal::Utils
4
+
5
+ def initialize(host_path:, container_path:)
6
+ @host_path = host_path
7
+ @container_path = container_path
8
+ end
9
+
10
+ def docker_args
11
+ argumentize "--volume", "#{host_path_for_docker_volume}:#{container_path}"
12
+ end
13
+
14
+ private
15
+ def host_path_for_docker_volume
16
+ if Pathname.new(host_path).absolute?
17
+ host_path
18
+ else
19
+ File.join "$(pwd)", host_path
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,372 @@
1
+ require "active_support/ordered_options"
2
+ require "active_support/core_ext/string/inquiry"
3
+ require "active_support/core_ext/module/delegation"
4
+ require "active_support/core_ext/hash/keys"
5
+ require "erb"
6
+ require "net/ssh/proxy/jump"
7
+
8
+ class Kamal::Configuration
9
+ delegate :service, :image, :labels, :hooks_path, to: :raw_config, allow_nil: true
10
+ delegate :argumentize, :optionize, to: Kamal::Utils
11
+
12
+ attr_reader :destination, :raw_config, :secrets
13
+ attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :proxy_boot, :servers, :ssh, :sshkit, :registry
14
+
15
+ include Validation
16
+
17
+ class << self
18
+ def create_from(config_file:, destination: nil, version: nil)
19
+ ENV["KAMAL_DESTINATION"] = destination
20
+
21
+ raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
22
+
23
+ new raw_config, destination: destination, version: version
24
+ end
25
+
26
+ private
27
+ def load_config_files(*files)
28
+ files.inject({}) { |config, file| config.deep_merge! load_config_file(file) }
29
+ end
30
+
31
+ def load_config_file(file)
32
+ if file.exist?
33
+ # Newer Psych doesn't load aliases by default
34
+ load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
35
+ YAML.send(load_method, ERB.new(File.read(file)).result).symbolize_keys
36
+ else
37
+ raise "Configuration file not found in #{file}"
38
+ end
39
+ end
40
+
41
+ def destination_config_file(base_config_file, destination)
42
+ base_config_file.sub_ext(".#{destination}.yml") if destination
43
+ end
44
+ end
45
+
46
+ def initialize(raw_config, destination: nil, version: nil, validate: true)
47
+ @raw_config = ActiveSupport::InheritableOptions.new(raw_config)
48
+ @destination = destination
49
+ @declared_version = version
50
+
51
+ validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration
52
+
53
+ @secrets = Kamal::Secrets.new(destination: destination)
54
+
55
+ # Eager load config to validate it, these are first as they have dependencies later on
56
+ @servers = Servers.new(config: self)
57
+ @registry = Registry.new(config: @raw_config, secrets: secrets)
58
+
59
+ @accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || []
60
+ @aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
61
+ @boot = Boot.new(config: self)
62
+ @builder = Builder.new(config: self)
63
+ @env = Env.new(config: @raw_config.env || {}, secrets: secrets)
64
+
65
+ @logging = Logging.new(logging_config: @raw_config.logging)
66
+ @proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy, secrets: secrets)
67
+ @proxy_boot = Proxy::Boot.new(config: self)
68
+ @ssh = Ssh.new(config: self)
69
+ @sshkit = Sshkit.new(config: self)
70
+
71
+ ensure_destination_if_required
72
+ ensure_required_keys_present
73
+ ensure_valid_kamal_version
74
+ ensure_retain_containers_valid
75
+ ensure_valid_service_name
76
+ ensure_no_traefik_reboot_hooks
77
+ ensure_one_host_for_ssl_roles
78
+ ensure_unique_hosts_for_ssl_roles
79
+ end
80
+
81
+ def version=(version)
82
+ @declared_version = version
83
+ end
84
+
85
+ def version
86
+ @declared_version.presence || ENV["VERSION"] || git_version
87
+ end
88
+
89
+ def abbreviated_version
90
+ if version
91
+ # Don't abbreviate <sha>_uncommitted_<etc>
92
+ if version.include?("_")
93
+ version
94
+ else
95
+ version[0...7]
96
+ end
97
+ end
98
+ end
99
+
100
+ def minimum_version
101
+ raw_config.minimum_version
102
+ end
103
+
104
+ def service_and_destination
105
+ [ service, destination ].compact.join("-")
106
+ end
107
+
108
+ def roles
109
+ servers.roles
110
+ end
111
+
112
+ def role(name)
113
+ roles.detect { |r| r.name == name.to_s }
114
+ end
115
+
116
+ def accessory(name)
117
+ accessories.detect { |a| a.name == name.to_s }
118
+ end
119
+
120
+ def all_hosts
121
+ (roles + accessories).flat_map(&:hosts).uniq
122
+ end
123
+
124
+ def app_hosts
125
+ roles.flat_map(&:hosts).uniq
126
+ end
127
+
128
+ def primary_host
129
+ primary_role&.primary_host
130
+ end
131
+
132
+ def primary_role_name
133
+ raw_config.primary_role || "web"
134
+ end
135
+
136
+ def primary_role
137
+ role(primary_role_name)
138
+ end
139
+
140
+ def allow_empty_roles?
141
+ raw_config.allow_empty_roles
142
+ end
143
+
144
+ def proxy_roles
145
+ roles.select(&:running_proxy?)
146
+ end
147
+
148
+ def proxy_role_names
149
+ proxy_roles.flat_map(&:name)
150
+ end
151
+
152
+ def proxy_accessories
153
+ accessories.select(&:running_proxy?)
154
+ end
155
+
156
+ def proxy_hosts
157
+ (proxy_roles.flat_map(&:hosts) + proxy_accessories.flat_map(&:hosts)).uniq
158
+ end
159
+
160
+ def repository
161
+ [ registry.server, image ].compact.join("/")
162
+ end
163
+
164
+ def absolute_image
165
+ "#{repository}:#{version}"
166
+ end
167
+
168
+ def latest_image
169
+ "#{repository}:#{latest_tag}"
170
+ end
171
+
172
+ def latest_tag
173
+ [ "latest", *destination ].join("-")
174
+ end
175
+
176
+ def service_with_version
177
+ "#{service}-#{version}"
178
+ end
179
+
180
+ def require_destination?
181
+ raw_config.require_destination
182
+ end
183
+
184
+ def retain_containers
185
+ raw_config.retain_containers || 5
186
+ end
187
+
188
+ def volume_args
189
+ if raw_config.volumes.present?
190
+ argumentize "--volume", raw_config.volumes
191
+ else
192
+ []
193
+ end
194
+ end
195
+
196
+ def logging_args
197
+ logging.args
198
+ end
199
+
200
+ def readiness_delay
201
+ raw_config.readiness_delay || 7
202
+ end
203
+
204
+ def deploy_timeout
205
+ raw_config.deploy_timeout || 30
206
+ end
207
+
208
+ def drain_timeout
209
+ raw_config.drain_timeout || 30
210
+ end
211
+
212
+ def run_directory
213
+ ".kamal"
214
+ end
215
+
216
+ def apps_directory
217
+ File.join run_directory, "apps"
218
+ end
219
+
220
+ def app_directory
221
+ File.join apps_directory, service_and_destination
222
+ end
223
+
224
+ def env_directory
225
+ File.join app_directory, "env"
226
+ end
227
+
228
+ def assets_directory
229
+ File.join app_directory, "assets"
230
+ end
231
+
232
+ def hooks_path
233
+ raw_config.hooks_path || ".kamal/hooks"
234
+ end
235
+
236
+ def asset_path
237
+ raw_config.asset_path
238
+ end
239
+
240
+ def error_pages_path
241
+ raw_config.error_pages_path
242
+ end
243
+
244
+ def env_tags
245
+ @env_tags ||= if (tags = raw_config.env["tags"])
246
+ tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
247
+ else
248
+ []
249
+ end
250
+ end
251
+
252
+ def env_tag(name)
253
+ env_tags.detect { |t| t.name == name.to_s }
254
+ end
255
+
256
+ def to_h
257
+ {
258
+ roles: role_names,
259
+ hosts: all_hosts,
260
+ primary_host: primary_host,
261
+ version: version,
262
+ repository: repository,
263
+ absolute_image: absolute_image,
264
+ service_with_version: service_with_version,
265
+ volume_args: volume_args,
266
+ ssh_options: ssh.to_h,
267
+ sshkit: sshkit.to_h,
268
+ builder: builder.to_h,
269
+ accessories: raw_config.accessories,
270
+ logging: logging_args
271
+ }.compact
272
+ end
273
+
274
+ private
275
+ # Will raise ArgumentError if any required config keys are missing
276
+ def ensure_destination_if_required
277
+ if require_destination? && destination.nil?
278
+ raise ArgumentError, "You must specify a destination"
279
+ end
280
+
281
+ true
282
+ end
283
+
284
+ def ensure_required_keys_present
285
+ %i[ service image registry ].each do |key|
286
+ raise Kamal::ConfigurationError, "Missing required configuration for #{key}" unless raw_config[key].present?
287
+ end
288
+
289
+ if raw_config.servers.nil?
290
+ raise Kamal::ConfigurationError, "No servers or accessories specified" unless raw_config.accessories.present?
291
+ else
292
+ unless role(primary_role_name).present?
293
+ raise Kamal::ConfigurationError, "The primary_role #{primary_role_name} isn't defined"
294
+ end
295
+
296
+ if primary_role.hosts.empty?
297
+ raise Kamal::ConfigurationError, "No servers specified for the #{primary_role.name} primary_role"
298
+ end
299
+
300
+ unless allow_empty_roles?
301
+ roles.each do |role|
302
+ if role.hosts.empty?
303
+ raise Kamal::ConfigurationError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true"
304
+ end
305
+ end
306
+ end
307
+ end
308
+
309
+ true
310
+ end
311
+
312
+ def ensure_valid_service_name
313
+ raise Kamal::ConfigurationError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/i
314
+
315
+ true
316
+ end
317
+
318
+ def ensure_valid_kamal_version
319
+ if minimum_version && Gem::Version.new(minimum_version) > Gem::Version.new(Kamal::VERSION)
320
+ raise Kamal::ConfigurationError, "Current version is #{Kamal::VERSION}, minimum required is #{minimum_version}"
321
+ end
322
+
323
+ true
324
+ end
325
+
326
+ def ensure_retain_containers_valid
327
+ raise Kamal::ConfigurationError, "Must retain at least 1 container" if retain_containers < 1
328
+
329
+ true
330
+ end
331
+
332
+ def ensure_no_traefik_reboot_hooks
333
+ hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) }
334
+
335
+ if hooks.any?
336
+ raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot"
337
+ end
338
+
339
+ true
340
+ end
341
+
342
+ def ensure_one_host_for_ssl_roles
343
+ roles.each(&:ensure_one_host_for_ssl)
344
+
345
+ true
346
+ end
347
+
348
+ def ensure_unique_hosts_for_ssl_roles
349
+ hosts = roles.select(&:ssl?).flat_map { |role| role.proxy.hosts }
350
+ duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
351
+
352
+ raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?
353
+
354
+ true
355
+ end
356
+
357
+ def role_names
358
+ raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
359
+ end
360
+
361
+ def git_version
362
+ @git_version ||=
363
+ if Kamal::Git.used?
364
+ if Kamal::Git.uncommitted_changes.present? && !builder.git_clone?
365
+ uncommitted_suffix = "_uncommitted_#{SecureRandom.hex(8)}"
366
+ end
367
+ [ Kamal::Git.revision, uncommitted_suffix ].compact.join
368
+ else
369
+ raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
370
+ end
371
+ end
372
+ end