kamal 1.6.0 → 1.7.0

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