kamal 1.6.0 → 1.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +5 -3
  3. data/lib/kamal/cli/app.rb +6 -3
  4. data/lib/kamal/cli/build.rb +13 -10
  5. data/lib/kamal/cli/healthcheck/poller.rb +2 -2
  6. data/lib/kamal/cli/main.rb +14 -2
  7. data/lib/kamal/cli/registry.rb +9 -10
  8. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +1 -1
  9. data/lib/kamal/cli/traefik.rb +5 -3
  10. data/lib/kamal/cli.rb +1 -1
  11. data/lib/kamal/commands/accessory.rb +4 -4
  12. data/lib/kamal/commands/app/logging.rb +4 -4
  13. data/lib/kamal/commands/builder/base.rb +13 -0
  14. data/lib/kamal/commands/builder/multiarch/remote.rb +10 -0
  15. data/lib/kamal/commands/builder/multiarch.rb +4 -0
  16. data/lib/kamal/commands/builder/native/cached.rb +10 -1
  17. data/lib/kamal/commands/builder/native/remote.rb +8 -0
  18. data/lib/kamal/commands/builder.rb +17 -11
  19. data/lib/kamal/commands/registry.rb +4 -13
  20. data/lib/kamal/commands/traefik.rb +8 -47
  21. data/lib/kamal/configuration/accessory.rb +30 -41
  22. data/lib/kamal/configuration/boot.rb +9 -4
  23. data/lib/kamal/configuration/builder.rb +33 -33
  24. data/lib/kamal/configuration/docs/accessory.yml +90 -0
  25. data/lib/kamal/configuration/docs/boot.yml +19 -0
  26. data/lib/kamal/configuration/docs/builder.yml +107 -0
  27. data/lib/kamal/configuration/docs/configuration.yml +157 -0
  28. data/lib/kamal/configuration/docs/env.yml +72 -0
  29. data/lib/kamal/configuration/docs/healthcheck.yml +59 -0
  30. data/lib/kamal/configuration/docs/logging.yml +21 -0
  31. data/lib/kamal/configuration/docs/registry.yml +49 -0
  32. data/lib/kamal/configuration/docs/role.yml +52 -0
  33. data/lib/kamal/configuration/docs/servers.yml +27 -0
  34. data/lib/kamal/configuration/docs/ssh.yml +46 -0
  35. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  36. data/lib/kamal/configuration/docs/traefik.yml +62 -0
  37. data/lib/kamal/configuration/env/tag.rb +1 -1
  38. data/lib/kamal/configuration/env.rb +10 -14
  39. data/lib/kamal/configuration/healthcheck.rb +63 -0
  40. data/lib/kamal/configuration/logging.rb +33 -0
  41. data/lib/kamal/configuration/registry.rb +31 -0
  42. data/lib/kamal/configuration/role.rb +53 -65
  43. data/lib/kamal/configuration/servers.rb +18 -0
  44. data/lib/kamal/configuration/ssh.rb +11 -8
  45. data/lib/kamal/configuration/sshkit.rb +9 -7
  46. data/lib/kamal/configuration/traefik.rb +60 -0
  47. data/lib/kamal/configuration/validation.rb +27 -0
  48. data/lib/kamal/configuration/validator/accessory.rb +9 -0
  49. data/lib/kamal/configuration/validator/builder.rb +9 -0
  50. data/lib/kamal/configuration/validator/env.rb +54 -0
  51. data/lib/kamal/configuration/validator/registry.rb +25 -0
  52. data/lib/kamal/configuration/validator/role.rb +11 -0
  53. data/lib/kamal/configuration/validator/servers.rb +7 -0
  54. data/lib/kamal/configuration/validator.rb +140 -0
  55. data/lib/kamal/configuration.rb +41 -66
  56. data/lib/kamal/version.rb +1 -1
  57. data/lib/kamal.rb +2 -0
  58. metadata +49 -3
@@ -1,30 +1,39 @@
1
1
  class Kamal::Configuration::Accessory
2
+ include Kamal::Configuration::Validation
3
+
2
4
  delegate :argumentize, :optionize, to: Kamal::Utils
3
5
 
4
- attr_accessor :name, :specifics
6
+ attr_reader :name, :accessory_config, :env
5
7
 
6
8
  def initialize(name, config:)
7
- @name, @config, @specifics = name.inquiry, config, config.raw_config["accessories"][name]
9
+ @name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]
10
+
11
+ validate! \
12
+ accessory_config,
13
+ example: validation_yml["accessories"]["mysql"],
14
+ context: "accessories/#{name}",
15
+ with: Kamal::Configuration::Validator::Accessory
16
+
17
+ @env = Kamal::Configuration::Env.new \
18
+ config: accessory_config.fetch("env", {}),
19
+ secrets_file: File.join(config.host_env_directory, "accessories", "#{service_name}.env"),
20
+ context: "accessories/#{name}/env"
8
21
  end
9
22
 
10
23
  def service_name
11
- specifics["service"] || "#{config.service}-#{name}"
24
+ accessory_config["service"] || "#{config.service}-#{name}"
12
25
  end
13
26
 
14
27
  def image
15
- specifics["image"]
28
+ accessory_config["image"]
16
29
  end
17
30
 
18
31
  def hosts
19
- if (specifics.keys & [ "host", "hosts", "roles" ]).size != 1
20
- raise ArgumentError, "Specify one of `host`, `hosts` or `roles` for accessory `#{name}`"
21
- end
22
-
23
32
  hosts_from_host || hosts_from_hosts || hosts_from_roles
24
33
  end
25
34
 
26
35
  def port
27
- if port = specifics["port"]&.to_s
36
+ if port = accessory_config["port"]&.to_s
28
37
  port.include?(":") ? port : "#{port}:#{port}"
29
38
  end
30
39
  end
@@ -34,32 +43,26 @@ class Kamal::Configuration::Accessory
34
43
  end
35
44
 
36
45
  def labels
37
- default_labels.merge(specifics["labels"] || {})
46
+ default_labels.merge(accessory_config["labels"] || {})
38
47
  end
39
48
 
40
49
  def label_args
41
50
  argumentize "--label", labels
42
51
  end
43
52
 
44
- def env
45
- Kamal::Configuration::Env.from_config \
46
- config: specifics.fetch("env", {}),
47
- secrets_file: File.join(config.host_env_directory, "accessories", "#{service_name}.env")
48
- end
49
-
50
53
  def env_args
51
54
  env.args
52
55
  end
53
56
 
54
57
  def files
55
- specifics["files"]&.to_h do |local_to_remote_mapping|
58
+ accessory_config["files"]&.to_h do |local_to_remote_mapping|
56
59
  local_file, remote_file = local_to_remote_mapping.split(":")
57
60
  [ expand_local_file(local_file), expand_remote_file(remote_file) ]
58
61
  end || {}
59
62
  end
60
63
 
61
64
  def directories
62
- specifics["directories"]&.to_h do |host_to_container_mapping|
65
+ accessory_config["directories"]&.to_h do |host_to_container_mapping|
63
66
  host_path, container_path = host_to_container_mapping.split(":")
64
67
  [ expand_host_path(host_path), container_path ]
65
68
  end || {}
@@ -74,7 +77,7 @@ class Kamal::Configuration::Accessory
74
77
  end
75
78
 
76
79
  def option_args
77
- if args = specifics["options"]
80
+ if args = accessory_config["options"]
78
81
  optionize args
79
82
  else
80
83
  []
@@ -82,7 +85,7 @@ class Kamal::Configuration::Accessory
82
85
  end
83
86
 
84
87
  def cmd
85
- specifics["cmd"]
88
+ accessory_config["cmd"]
86
89
  end
87
90
 
88
91
  private
@@ -116,18 +119,18 @@ class Kamal::Configuration::Accessory
116
119
  end
117
120
 
118
121
  def specific_volumes
119
- specifics["volumes"] || []
122
+ accessory_config["volumes"] || []
120
123
  end
121
124
 
122
125
  def remote_files_as_volumes
123
- specifics["files"]&.collect do |local_to_remote_mapping|
126
+ accessory_config["files"]&.collect do |local_to_remote_mapping|
124
127
  _, remote_file = local_to_remote_mapping.split(":")
125
128
  "#{service_data_directory + remote_file}:#{remote_file}"
126
129
  end || []
127
130
  end
128
131
 
129
132
  def remote_directories_as_volumes
130
- specifics["directories"]&.collect do |host_to_container_mapping|
133
+ accessory_config["directories"]&.collect do |host_to_container_mapping|
131
134
  host_path, container_path = host_to_container_mapping.split(":")
132
135
  [ expand_host_path(host_path), container_path ].join(":")
133
136
  end || []
@@ -146,30 +149,16 @@ class Kamal::Configuration::Accessory
146
149
  end
147
150
 
148
151
  def hosts_from_host
149
- if specifics.key?("host")
150
- host = specifics["host"]
151
- if host
152
- [ host ]
153
- else
154
- raise ArgumentError, "Missing host for accessory `#{name}`"
155
- end
156
- end
152
+ [ accessory_config["host"] ] if accessory_config.key?("host")
157
153
  end
158
154
 
159
155
  def hosts_from_hosts
160
- if specifics.key?("hosts")
161
- hosts = specifics["hosts"]
162
- if hosts.is_a?(Array)
163
- hosts
164
- else
165
- raise ArgumentError, "Hosts should be an Array for accessory `#{name}`"
166
- end
167
- end
156
+ accessory_config["hosts"] if accessory_config.key?("hosts")
168
157
  end
169
158
 
170
159
  def hosts_from_roles
171
- if specifics.key?("roles")
172
- specifics["roles"].flat_map { |role| config.role(role).hosts }
160
+ if accessory_config.key?("roles")
161
+ accessory_config["roles"].flat_map { |role| config.role(role).hosts }
173
162
  end
174
163
  end
175
164
  end
@@ -1,20 +1,25 @@
1
1
  class Kamal::Configuration::Boot
2
+ include Kamal::Configuration::Validation
3
+
4
+ attr_reader :boot_config, :host_count
5
+
2
6
  def initialize(config:)
3
- @options = config.raw_config.boot || {}
7
+ @boot_config = config.raw_config.boot || {}
4
8
  @host_count = config.all_hosts.count
9
+ validate! boot_config
5
10
  end
6
11
 
7
12
  def limit
8
- limit = @options["limit"]
13
+ limit = boot_config["limit"]
9
14
 
10
15
  if limit.to_s.end_with?("%")
11
- [ @host_count * limit.to_i / 100, 1 ].max
16
+ [ host_count * limit.to_i / 100, 1 ].max
12
17
  else
13
18
  limit
14
19
  end
15
20
  end
16
21
 
17
22
  def wait
18
- @options["wait"]
23
+ boot_config["wait"]
19
24
  end
20
25
  end
@@ -1,73 +1,79 @@
1
1
  class Kamal::Configuration::Builder
2
+ include Kamal::Configuration::Validation
3
+
4
+ attr_reader :config, :builder_config
5
+ delegate :image, :service, to: :config
6
+ delegate :server, to: :"config.registry"
7
+
2
8
  def initialize(config:)
3
- @options = config.raw_config.builder || {}
9
+ @config = config
10
+ @builder_config = config.raw_config.builder || {}
4
11
  @image = config.image
5
- @server = config.registry["server"]
12
+ @server = config.registry.server
6
13
  @service = config.service
7
- @destination = config.destination
8
14
 
9
- valid?
15
+ validate! builder_config, with: Kamal::Configuration::Validator::Builder
10
16
  end
11
17
 
12
18
  def to_h
13
- @options
19
+ builder_config
14
20
  end
15
21
 
16
22
  def multiarch?
17
- @options["multiarch"] != false
23
+ builder_config["multiarch"] != false
18
24
  end
19
25
 
20
26
  def local?
21
- !!@options["local"]
27
+ !!builder_config["local"]
22
28
  end
23
29
 
24
30
  def remote?
25
- !!@options["remote"]
31
+ !!builder_config["remote"]
26
32
  end
27
33
 
28
34
  def cached?
29
- !!@options["cache"]
35
+ !!builder_config["cache"]
30
36
  end
31
37
 
32
38
  def args
33
- @options["args"] || {}
39
+ builder_config["args"] || {}
34
40
  end
35
41
 
36
42
  def secrets
37
- @options["secrets"] || []
43
+ builder_config["secrets"] || []
38
44
  end
39
45
 
40
46
  def dockerfile
41
- @options["dockerfile"] || "Dockerfile"
47
+ builder_config["dockerfile"] || "Dockerfile"
42
48
  end
43
49
 
44
50
  def target
45
- @options["target"]
51
+ builder_config["target"]
46
52
  end
47
53
 
48
54
  def context
49
- @options["context"] || "."
55
+ builder_config["context"] || "."
50
56
  end
51
57
 
52
58
  def local_arch
53
- @options["local"]["arch"] if local?
59
+ builder_config["local"]["arch"] if local?
54
60
  end
55
61
 
56
62
  def local_host
57
- @options["local"]["host"] if local?
63
+ builder_config["local"]["host"] if local?
58
64
  end
59
65
 
60
66
  def remote_arch
61
- @options["remote"]["arch"] if remote?
67
+ builder_config["remote"]["arch"] if remote?
62
68
  end
63
69
 
64
70
  def remote_host
65
- @options["remote"]["host"] if remote?
71
+ builder_config["remote"]["host"] if remote?
66
72
  end
67
73
 
68
74
  def cache_from
69
75
  if cached?
70
- case @options["cache"]["type"]
76
+ case builder_config["cache"]["type"]
71
77
  when "gha"
72
78
  cache_from_config_for_gha
73
79
  when "registry"
@@ -78,7 +84,7 @@ class Kamal::Configuration::Builder
78
84
 
79
85
  def cache_to
80
86
  if cached?
81
- case @options["cache"]["type"]
87
+ case builder_config["cache"]["type"]
82
88
  when "gha"
83
89
  cache_to_config_for_gha
84
90
  when "registry"
@@ -88,15 +94,15 @@ class Kamal::Configuration::Builder
88
94
  end
89
95
 
90
96
  def ssh
91
- @options["ssh"]
97
+ builder_config["ssh"]
92
98
  end
93
99
 
94
100
  def git_clone?
95
- Kamal::Git.used? && @options["context"].nil?
101
+ Kamal::Git.used? && builder_config["context"].nil?
96
102
  end
97
103
 
98
104
  def clone_directory
99
- @clone_directory ||= File.join Dir.tmpdir, "kamal-clones", [ @service, pwd_sha ].compact.join("-")
105
+ @clone_directory ||= File.join Dir.tmpdir, "kamal-clones", [ service, pwd_sha ].compact.join("-")
100
106
  end
101
107
 
102
108
  def build_directory
@@ -109,18 +115,12 @@ class Kamal::Configuration::Builder
109
115
  end
110
116
 
111
117
  private
112
- def valid?
113
- if @options["cache"] && @options["cache"]["type"]
114
- raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless [ "gha", "registry" ].include?(@options["cache"]["type"])
115
- end
116
- end
117
-
118
118
  def cache_image
119
- @options["cache"]&.fetch("image", nil) || "#{@image}-build-cache"
119
+ builder_config["cache"]&.fetch("image", nil) || "#{image}-build-cache"
120
120
  end
121
121
 
122
122
  def cache_image_ref
123
- [ @server, cache_image ].compact.join("/")
123
+ [ server, cache_image ].compact.join("/")
124
124
  end
125
125
 
126
126
  def cache_from_config_for_gha
@@ -132,11 +132,11 @@ class Kamal::Configuration::Builder
132
132
  end
133
133
 
134
134
  def cache_to_config_for_gha
135
- [ "type=gha", @options["cache"]&.fetch("options", nil) ].compact.join(",")
135
+ [ "type=gha", builder_config["cache"]&.fetch("options", nil) ].compact.join(",")
136
136
  end
137
137
 
138
138
  def cache_to_config_for_registry
139
- [ "type=registry", @options["cache"]&.fetch("options", nil), "ref=#{cache_image_ref}" ].compact.join(",")
139
+ [ "type=registry", builder_config["cache"]&.fetch("options", nil), "ref=#{cache_image_ref}" ].compact.join(",")
140
140
  end
141
141
 
142
142
  def repo_basename
@@ -0,0 +1,90 @@
1
+ # Accessories
2
+ #
3
+ # Accessories can be booted on a single host, a list of hosts, or on specific roles.
4
+ # The hosts do not need to be defined in the Kamal servers configuration.
5
+ #
6
+ # Accessories are managed separately from the main service - they are not updated
7
+ # when you deploy and they do not have zero-downtime deployments.
8
+ #
9
+ # Run `kamal accessory boot <accessory>` to boot an accessory.
10
+ # See `kamal accessory --help` for more information.
11
+
12
+ # Configuring accessories
13
+ #
14
+ # First define the accessory in the `accessories`
15
+ accessories:
16
+ mysql:
17
+
18
+ # Service name
19
+ #
20
+ # This is used in the service label and defaults to `<service>-<accessory>`
21
+ # where `<service>` is the main service name from the root configuration
22
+ service: mysql
23
+
24
+ # Image
25
+ #
26
+ # The Docker image to use, prefix with a registry if not using Docker hub
27
+ image: mysql:8.0
28
+
29
+ # Accessory hosts
30
+ #
31
+ # Specify one of `host`, `hosts` or `roles`
32
+ host: mysql-db1
33
+ hosts:
34
+ - mysql-db1
35
+ - mysql-db2
36
+ roles:
37
+ - mysql
38
+
39
+ # Custom command
40
+ #
41
+ # You can set a custom command to run in the container, if you do not want to use the default
42
+ cmd: "bin/mysqld"
43
+
44
+ # Port mappings
45
+ #
46
+ # See https://docs.docker.com/network/, especially note the warning about the security
47
+ # implications of exposing ports publicly.
48
+ port: "127.0.0.1:3306:3306"
49
+
50
+ # Labels
51
+ labels:
52
+ app: myapp
53
+
54
+ # Options
55
+ # These are passed to the Docker run command in the form `--<name> <value>`
56
+ options:
57
+ restart: always
58
+ cpus: 2
59
+
60
+ # Environment variables
61
+ # See kamal docs env for more information
62
+ env:
63
+ ...
64
+
65
+ # Copying files
66
+ #
67
+ # You can specify files to mount into the container.
68
+ # The format is `local:remote` where `local` is the path to the file on the local machine
69
+ # and `remote` is the path to the file in the container.
70
+ #
71
+ # They will be uploaded from the local repo to the host and then mounted.
72
+ #
73
+ # ERB files will be evaluated before being copied.
74
+ files:
75
+ - config/my.cnf.erb:/etc/mysql/my.cnf
76
+ - config/myoptions.cnf:/etc/mysql/myoptions.cnf
77
+
78
+ # Directories
79
+ #
80
+ # You can specify directories to mount into the container. They will be created on the host
81
+ # before being mounted
82
+ directories:
83
+ - mysql-logs:/var/log/mysql
84
+
85
+ # Volumes
86
+ #
87
+ # Any other volumes to mount, in addition to the files and directories.
88
+ # They are not created or copied before mounting
89
+ volumes:
90
+ - /path/to/mysql-logs:/var/log/mysql
@@ -0,0 +1,19 @@
1
+ # Booting
2
+ #
3
+ # When deploying to large numbers of hosts, you might prefer not to restart your services on every host at the same time.
4
+ #
5
+ # Kamal’s default is to boot new containers on all hosts in parallel. But you can control this with the boot configuration.
6
+
7
+ # Fixed group sizes
8
+ #
9
+ # Here we boot 2 hosts at a time with a 10 second gap between each group.
10
+ boot:
11
+ limit: 2
12
+ wait: 10
13
+
14
+ # Percentage of hosts
15
+ #
16
+ # Here we boot 25% of the hosts at a time with a 2 second gap between each group.
17
+ boot:
18
+ limit: 25%
19
+ wait: 2
@@ -0,0 +1,107 @@
1
+ # Builder
2
+ #
3
+ # The builder configuration controls how the application is built with `docker build` or `docker buildx build`
4
+ #
5
+ # If no configuration is specified, Kamal will:
6
+ # 1. Create a buildx context called `kamal-<service>-multiarch`
7
+ # 2. Use `docker buildx build` to build a multiarch image for linux/amd64,linux/arm64 with that context
8
+ #
9
+ # See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information
10
+
11
+ # Builder options
12
+ #
13
+ # Options go under the builder key in the root configuration.
14
+ builder:
15
+
16
+ # Multiarch
17
+ #
18
+ # Enables multiarch builds, defaults to `true`
19
+ multiarch: false
20
+
21
+ # Local configuration
22
+ #
23
+ # The build configuration for local builds, only used if multiarch is enabled (the default)
24
+ #
25
+ # If there is no remote configuration, by default we build for amd64 and arm64.
26
+ # If you only want to build for one architecture, you can specify it here.
27
+ # The docker socket is optional and uses the default docker host socket when not specified
28
+ local:
29
+ arch: amd64
30
+ host: /var/run/docker.sock
31
+
32
+ # Remote configuration
33
+ #
34
+ # The build configuration for remote builds, also only used if multiarch is enabled.
35
+ # The arch is required and can be either amd64 or arm64.
36
+ remote:
37
+ arch: arm64
38
+ host: ssh://docker@docker-builder
39
+
40
+ # Builder cache
41
+ #
42
+ # The type must be either 'gha' or 'registry'
43
+ #
44
+ # The image is only used for registry cache
45
+ cache:
46
+ type: registry
47
+ options: mode=max
48
+ image: kamal-app-build-cache
49
+
50
+ # Build context
51
+ #
52
+ # If this is not set, then a local git clone of the repo is used.
53
+ # This ensures a clean build with no uncommitted changes.
54
+ #
55
+ # To use the local checkout instead you can set the context to `.`, or a path to another directory.
56
+ context: .
57
+
58
+ # Dockerfile
59
+ #
60
+ # The Dockerfile to use for building, defaults to `Dockerfile`
61
+ dockerfile: Dockerfile.production
62
+
63
+ # Build target
64
+ #
65
+ # If not set, then the default target is used
66
+ target: production
67
+
68
+ # Build Arguments
69
+ #
70
+ # Any additional build arguments, passed to `docker build` with `--build-arg <key>=<value>`
71
+ args:
72
+ ENVIRONMENT: production
73
+
74
+ # Referencing build arguments
75
+ #
76
+ # ```shell
77
+ # ARG RUBY_VERSION
78
+ # FROM ruby:$RUBY_VERSION-slim as base
79
+ # ```
80
+
81
+ # Build secrets
82
+ #
83
+ # Values are read from the environment.
84
+ #
85
+ secrets:
86
+ - SECRET1
87
+ - SECRET2
88
+
89
+ # Referencing Build Secrets
90
+ #
91
+ # ```shell
92
+ # # Copy Gemfiles
93
+ # COPY Gemfile Gemfile.lock ./
94
+ #
95
+ # # Install dependencies, including private repositories via access token
96
+ # # Then remove bundle cache with exposed GITHUB_TOKEN)
97
+ # RUN --mount=type=secret,id=GITHUB_TOKEN \
98
+ # BUNDLE_GITHUB__COM=x-access-token:$(cat /run/secrets/GITHUB_TOKEN) \
99
+ # bundle install && \
100
+ # rm -rf /usr/local/bundle/cache
101
+ # ```
102
+
103
+
104
+ # SSH
105
+ #
106
+ # SSH agent socket or keys to expose to the build
107
+ ssh: default=$SSH_AUTH_SOCK