Compare commits

..

116 Commits

Author SHA1 Message Date
Donal McBreen
685312c9f8 Bump version for 2.3.0 2024-10-31 09:14:29 +00:00
Donal McBreen
ca5e53404b Merge pull request #1175 from basecamp/proxy-0.8.2
Bump proxy minimum version to 0.8.2
2024-10-31 08:13:18 +00:00
Donal McBreen
2c14f48300 Bump proxy minimum version to 0.8.2
Detect event-stream content type properly

See: https://github.com/basecamp/kamal-proxy/releases/tag/v0.8.2
2024-10-30 08:06:52 +00:00
dependabot[bot]
cd4e183213 Bump rexml from 3.3.6 to 3.3.9 in the bundler group across 1 directory (#1173)
Bumps the bundler group with 1 update in the / directory: [rexml](https://github.com/ruby/rexml).


Updates `rexml` from 3.3.6 to 3.3.9
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.6...v3.3.9)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 01:39:47 -07:00
Donal McBreen
2465681408 Merge pull request #1151 from basecamp/net-ssh-7.3.0
Ensure using at least net-ssh 7.3.0
2024-10-25 16:16:18 +01:00
Donal McBreen
b917d7cd40 Merge pull request #1152 from basecamp/skip-log-max-size
Allow log max size to not be set
2024-10-25 08:55:18 +01:00
Donal McBreen
1980a79e73 Update lib/kamal/cli/proxy.rb
Co-authored-by: Sijawusz Pur Rahnama <sija@sija.pl>
2024-10-25 08:10:25 +01:00
Donal McBreen
347eb69350 Merge pull request #994 from honzasterba/bw_fetch_all_fields
[bitwarden] ability to fetch all fields from an item
2024-10-23 16:32:39 +01:00
Donal McBreen
9a8a45015b Allow log max size to not be set
The max-size log opt is not valid for all logging drivers, such as
syslog. Allow the option to be removed from the boot config with:

```
kamal proxy boot_config set --log-max-size=
or
kamal proxy boot_config set --log-max-size=""
```
2024-10-23 15:21:06 +01:00
Donal McBreen
8d0f4903ae Ensure using at least net-ssh 7.3.0
This has support for aes(128|256)gcm ciphers and some fixes for
Ruby 3.3.
2024-10-23 14:58:36 +01:00
Donal McBreen
57d582e3bc Merge pull request #972 from kohkimakimoto/dev-provenance-flag
Add provenance option
2024-10-23 14:07:19 +01:00
Donal McBreen
bf8779cef4 Merge pull request #950 from admtnnr/fix-registry-cache-options
builder/cache/options: fix order of build args when using registry
2024-10-23 13:59:59 +01:00
Jan Sterba
7142534e77 [bitwarden] ability to fetch all fields from an item
Sometimes a projects has a lot of secrets (more than 10). And its
cumbersome to write $(kama secrets fetch ...) with a lot of field
names.

I want to be able to just fetch all the fields from a given item
 and then just use these with $(kamal extract NAME)
2024-10-23 13:28:37 +01:00
Donal McBreen
0f97e0b056 Merge pull request #1114 from alanoliveira/main
prevent escape '#' when generating env_file string
2024-10-23 12:35:07 +01:00
Donal McBreen
bd8c35b194 Merge pull request #1020 from igor-alexandrov/network-args
Allow to override network
2024-10-23 12:22:36 +01:00
Donal McBreen
35075e2e4d Merge pull request #1136 from aidanharan/secrets-files-not-found-message
Updated secrets error message if secrets files do not exist
2024-10-23 11:14:41 +01:00
Donal McBreen
53dad5f54f Merge pull request #1121 from kylerippey/adapter-cli-installation-checks
Raise meaningful error messages when secret adapter CLIs are not installed
2024-10-23 11:12:50 +01:00
Donal McBreen
66f6e8b576 Merge pull request #1045 from junket/allow-false-env-var-value
Allow false env var value
2024-10-23 11:11:43 +01:00
Alan Oliveira
a3f5830728 improve test legibility 2024-10-23 08:06:27 +09:00
Donal McBreen
a3e5505bb2 Merge pull request #1117 from pardeyke/add_proxy_reboot_info
Added `kamal proxy reboot` to raised error message
2024-10-22 17:26:36 +01:00
Donal McBreen
fdf8ef1343 Merge pull request #1144 from basecamp/zeitwerk-2.7
Require zeitwerk 2.6.18
2024-10-22 15:03:45 +01:00
Donal McBreen
3ee45d7b30 Require zeitwerk 2.6.12
We were requiring Zeitwerk 2.5+, but calling eager_load_namespace
which was added in 2.6.2.

Fixes: https://github.com/basecamp/kamal/issues/1109
2024-10-22 13:01:36 +01:00
Aidan Haran
c320343bb2 Updated secrets error message if secrets files do not exist 2024-10-19 20:01:00 +01:00
dependabot[bot]
74a06b0ccd Bump actionpack in the bundler group across 1 directory (#1127)
Bumps the bundler group with 1 update in the / directory: [actionpack](https://github.com/rails/rails).


Updates `actionpack` from 7.1.3.4 to 7.1.4.1
- [Release notes](https://github.com/rails/rails/releases)
- [Changelog](https://github.com/rails/rails/blob/v7.2.1.1/actionpack/CHANGELOG.md)
- [Commits](https://github.com/rails/rails/compare/v7.1.3.4...v7.1.4.1)

---
updated-dependencies:
- dependency-name: actionpack
  dependency-type: indirect
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 11:30:14 -07:00
dependabot[bot]
c0ca5e6dbb Bump rexml from 3.3.4 to 3.3.6 in the bundler group across 1 directory (#1126)
Bumps the bundler group with 1 update in the / directory: [rexml](https://github.com/ruby/rexml).


Updates `rexml` from 3.3.4 to 3.3.6
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.4...v3.3.6)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 11:29:59 -07:00
dependabot[bot]
6f08750c3e Bump webrick from 1.8.1 to 1.8.2 in the bundler group across 1 directory (#1125)
Bumps the bundler group with 1 update in the / directory: [webrick](https://github.com/ruby/webrick).


Updates `webrick` from 1.8.1 to 1.8.2
- [Release notes](https://github.com/ruby/webrick/releases)
- [Commits](https://github.com/ruby/webrick/compare/v1.8.1...v1.8.2)

---
updated-dependencies:
- dependency-name: webrick
  dependency-type: indirect
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 11:29:41 -07:00
Jonas Pardeyke
e362b0106a changed text 2024-10-16 09:08:30 +02:00
Kyle Rippey
8cec17dd05 Made secret adapters raise a meaningful error if the required CLI is not installed 2024-10-15 23:20:18 -07:00
Jonas Pardeyke
0f3786781b added kamal proxy reboot to raised error 2024-10-15 22:47:08 +02:00
Alan Oliveira
844e3acf50 prevent escape '#' when generating env_file string 2024-10-15 14:57:53 +09:00
David Heinemeier Hansson
607368121e Merge pull request #1079 from jjatinggoyal/valkey
Switch Redis to Valkey in deploy template and tests
2024-10-13 19:31:12 +02:00
Puru
0f16ba1995 Upgrade Ruby base image from 3.2.0 to 3.3.x (#1107)
* Upgrade ruby base image to fix HIGH and CRITICAL CVEs
* Float on latest 3.3.x

---------

Co-authored-by: Jeremy Daer <jeremydaer@gmail.com>
2024-10-13 10:07:09 -07:00
Jatin Goyal
f3b8a59133 Use valkey for redis image in deploy template 2024-10-13 22:04:03 +05:30
David Heinemeier Hansson
bf79c7192f Clearer still 2024-10-11 10:40:37 -07:00
David Heinemeier Hansson
cb82767d0f Clarify proxy settings 2024-10-11 10:39:58 -07:00
Donal McBreen
5cb9fb787b Bump version for 2.2.2 2024-10-10 13:29:38 -04:00
Donal McBreen
493c5690f1 Merge pull request #1066 from davidstosik/v2.1.1/path-space-fix
Support spaces in git repository path
2024-10-10 10:17:00 -04:00
Donal McBreen
5de55a22ff Merge pull request #1088 from emmceemoore/patch-1
Typo fix.
2024-10-10 10:16:01 -04:00
Nick Pezza
a1e40f9fec Update to be able to run on 3.4 with frozen strings (#1080)
* Update to be able to run on 3.4 with frozen strings

---------

Co-authored-by: Jeremy Daer <jeremydaer@gmail.com>
Co-authored-by: Sijawusz Pur Rahnama <sija@sija.pl>
2024-10-09 21:11:06 -04:00
Mike Moore
7ddf3bcb02 Typo fix. 2024-10-09 17:34:42 -06:00
Donal McBreen
3654a7e1be Bump version for 2.2.1 2024-10-09 14:46:44 -04:00
Donal McBreen
6a7783c979 Merge pull request #1086 from basecamp/proxy-1.8.1
Bump proxy to version 0.8.1
2024-10-09 14:46:09 -04:00
Donal McBreen
7dc2609b77 Merge pull request #1082 from graysonchen/patch-1
ERROR (ArgumentError): Unknown boot_config subcommand clear
2024-10-09 14:03:07 -04:00
Donal McBreen
74960499c0 Bump proxy to version 0.8.1
Fixes issue where incorrect status code may be returned when buffering
responses.

https://github.com/basecamp/kamal-proxy/releases/tag/v0.8.1
2024-10-09 14:00:38 -04:00
Igor Alexandrov
69b13ebc6a Renamed NETWORK to DEFAULT_NETWORK 2024-10-09 10:00:57 +04:00
Igor Alexandrov
da2a543cbc Reverted network arguments everywhere except to accessory config 2024-10-09 10:00:49 +04:00
Igor Alexandrov
08dacd2745 Added command tests 2024-10-09 09:53:25 +04:00
Igor Alexandrov
b6a10df56a Added tests for network configuration option 2024-10-09 09:53:25 +04:00
Igor Alexandrov
c917dd82cf Added network_args to proxy configuration 2024-10-09 09:53:25 +04:00
Igor Alexandrov
f04cae529a Added network configuration option to application, proxy and accessory sections 2024-10-09 09:53:17 +04:00
Grayson Chen
50c96e36c0 typo clear change to reset
kamal proxy boot_config clear
 
 ERROR (ArgumentError): Unknown boot_config subcommand clear
2024-10-09 00:11:54 +08:00
Donal McBreen
7b48648bf2 Bump version for 2.2.0 2024-10-08 08:59:23 -04:00
Donal McBreen
91df935d05 Merge pull request #1076 from basecamp/active-support-require-for-to-sentence
Add Active Support require for to_sentence
2024-10-08 07:46:53 -04:00
Donal McBreen
bbfcbfa94b Merge pull request #1064 from basecamp/kamal-proxy-0.8.0
Update to kamal-proxy 0.8.0
2024-10-08 07:42:48 -04:00
Donal McBreen
440044b900 Merge pull request #1072 from basecamp/proxy-default-10m-logs
Default to keeping 10m of proxy logs
2024-10-08 07:35:38 -04:00
Donal McBreen
06419f8749 Add Active Support require for to_sentence
Fixes: https://github.com/basecamp/kamal/issues/1061
2024-10-08 07:33:03 -04:00
David Stosik
8d6d7ffed0 s/refute_match/assert_no_match/ 2024-10-08 07:10:08 +09:00
Donal McBreen
67ce1912f7 Default to keeping 10m of proxy logs
Match the defaults for the application containers of 10m of logs.

Allow them to be altered with the proxy boot_config set command.
2024-10-07 16:20:40 -04:00
David Stosik
f45c754e53 Remove unnecessary method 2024-10-07 15:46:04 +09:00
David Stosik
d40057286d Escape more paths and write a test 2024-10-07 15:46:04 +09:00
David Stosik
0840fdf0dd Support spaces in git repository path
See https://github.com/basecamp/kamal/issues/1036
2024-10-07 15:46:04 +09:00
Donal McBreen
a434b10bfd Update to kamal-proxy 0.8.0
Proxy changes:
- Add option to use custom TLS certificates (#17)
- Don't buffer SSE responses (#36)
- Allow routing to wildcard subdomains (#45)

Custom TLS certificates not supported in Kamal itself yet. Buffering
SSE responses and wildcard subdomains will work without any Kamal
changes.
2024-10-06 13:48:00 -04:00
Donal McBreen
e34031f70c Bump version for 2.1.2 2024-10-06 13:40:53 -04:00
Donal McBreen
23898a5197 Merge pull request #1062 from basecamp/skip-proxy-flag-ssl-false
Skip setting the proxy flag when ssl is false
2024-10-06 18:32:45 +01:00
Donal McBreen
1e9c9e9103 Skip setting the proxy flag when ssl is false
Fixes: https://github.com/basecamp/kamal/issues/1037
2024-10-06 13:22:43 -04:00
David Heinemeier Hansson
4b2c9cdc72 Merge pull request #1026 from ehutzelman/patch-1
Update init description for kamal secrets
2024-10-05 01:54:08 +02:00
David Heinemeier Hansson
80191588c2 Merge pull request #1050 from tiramizoo/template-docker-setup
Update sample template for docker setup hook.
2024-10-05 01:46:09 +02:00
David Heinemeier Hansson
5ca806f4d3 Merge pull request #1054 from tuladhar/cloudflare-ssl
Clarify SSL comment when using Cloudflare
2024-10-04 21:03:23 +02:00
Puru
1d04a6644f Clarify SSL comment when using Cloudflare 2024-10-05 00:45:04 +05:45
Wojciech Wnętrzak
950624d667 Update sample template for docker setup hook.
"kamal" network is already created (in v2.0) so the sample code is
no longer accurate.
2024-10-04 09:27:17 +02:00
David Heinemeier Hansson
81f3508507 Bump version for 2.1.1 2024-10-03 11:39:56 -07:00
David Heinemeier Hansson
9a16873f21 Merge pull request #1035 from basecamp/fix-kamal-setup-on-accessory-hosts
Restore kamal setup to accessory hosts
2024-10-03 19:25:33 +02:00
junket
6d1d7a4c82 Updates argumentize test for false values 2024-10-03 10:05:54 -04:00
junket
ccf32c2c1f Pass false values in env vars to docker 2024-10-03 09:31:30 -04:00
David Heinemeier Hansson
e5ca53db6e Use new deploy config so as not to update all other tests 2024-10-02 17:34:13 -07:00
David Heinemeier Hansson
82a436fa02 Rubocop 2024-10-02 17:07:51 -07:00
David Heinemeier Hansson
7be2e7e0ba Test accessory_hosts with roles and without filtering 2024-10-02 17:03:30 -07:00
David Heinemeier Hansson
4f7ebd73a3 Specifics#accessory_hosts was being filtered out by role host check 2024-10-02 16:30:32 -07:00
Donal McBreen
279bda2770 Bump version for 2.1.0 2024-10-02 11:35:45 +01:00
Donal McBreen
aa15fa532a Merge pull request #1024 from jeromedalbert/follow-primary-role
Follow logs on primary role by default
2024-10-02 09:29:16 +01:00
Donal McBreen
276b469c2b Merge pull request #1019 from nickhammond/ENV/destination
Set KAMAL_DESTINATION when loading config
2024-10-02 09:28:01 +01:00
Donal McBreen
c10b3fb07a Merge pull request #908 from basecamp/ignorable-ssh-config
SSH: allow setting `config: false` to ignore local user `~/.ssh/config`
2024-10-02 08:28:57 +01:00
Eric Hutzelman
0ff1450a74 Update init description for kamal secrets
No longer uses .env stub, replace with secrets stub in .kamal directory.
2024-10-01 18:49:08 -05:00
Jerome Dalbert
f47fd13e5b Follow logs on primary role by default
Do not default `kamal app logs -f` to use the hardcoded “web” role;
instead use the primary role, which can be different from “web”.
2024-10-01 13:51:09 -07:00
Adam Tanner
256933f6f3 builder/cache/options: fix order of build args when using registry 2024-10-01 12:27:45 -04:00
Nick Hammond
1d8c40f5d2 Run RC 2024-10-01 08:20:21 -07:00
Nick Hammond
73c78079bc Set KAMAL_DESTINATION when loading config 2024-10-01 08:16:52 -07:00
Donal McBreen
cd12f95a97 Merge pull request #1018 from basecamp/kamal-proxy-deploy-equals-in-args
Use `=` in kamal-proxy deploy command args
2024-10-01 15:51:43 +01:00
Donal McBreen
641e9056b3 Use = in kamal-proxy deploy command args
`=` is required for boolean values and works for all values.
2024-10-01 15:42:12 +01:00
Donal McBreen
b4bcf35f78 Merge pull request #1000 from kpumuk/hosts
Allow specifying multiple hosts for kamal-proxy via an array
2024-09-30 10:26:15 -04:00
Donal McBreen
7f6095c9eb Merge pull request #1009 from basecamp/secrets-print
Add `kamal secrets print` for secret debugging
2024-09-30 09:55:53 -04:00
Donal McBreen
ef1271df47 Merge pull request #1010 from basecamp/kamal-proxy-0-7-0
Added support for ACME `http-01` challenges
2024-09-30 09:55:34 -04:00
Donal McBreen
df1232d90f Added support for ACME http-01 challenges
Update to kamal-proxy 0.7.0 for ACME `http-01` challenge support.
2024-09-30 14:44:28 +01:00
Dmytro Shteflyuk
e75365c8c6 Simplified deploy options for kamal-proxy as it supports multiple --host arguments 2024-09-30 08:09:05 -04:00
Donal McBreen
e441399255 Add kamal secrets print for secret debugging
Dotenv's variable substitution doesn't work the same way as commands run
in the shell. It needs values to be escaped.

```sh
$ cat /tmp/env
SECRETS=$(cat /tmp/json)
SECRETS2=$(echo $SECRETS | jq)
$ cat /tmp/json
\{\ \"foo\"\ :\ \"bar\" \}
$ SECRETS=$(cat /tmp/json)
$ SECRETS2=$(echo $SECRETS | jq)
jq: parse error: Invalid numeric literal at line 1, column 2
$ ruby -e 'require "dotenv"; puts Dotenv.parse("/tmp/env")["SECRETS2"]'
{
  "foo": "bar"
}
```

Since you then can't use the shell to debug, `kamal secrets print` will
allow you to see what the secrets will be set to.
2024-09-30 12:28:29 +01:00
Donal McBreen
af992ce755 Merge pull request #996 from iximiuz/patch-1
Fix git --add safe.directory command in Dockerfile
2024-09-30 04:36:00 -04:00
Donal McBreen
32caf4b148 Merge pull request #995 from honzasterba/bw_nicer_error_message_on_non_field_fetch
[bitwarden] default fetch raises NoMethodError
2024-09-30 04:35:48 -04:00
Donal McBreen
28a02262df Merge pull request #988 from igor-alexandrov/kamal-proxy-remove-no-target
Fixed kamal-proxy remove command
2024-09-30 04:18:12 -04:00
Donal McBreen
b11fb93a6c Merge pull request #971 from igor-alexandrov/add-app-port-example
Added app_port example to the proxy section
2024-09-30 04:16:34 -04:00
Dmytro Shteflyuk
67ad7662ab Simplified proxy hosts validation and documentation, similar to accessory config 2024-09-29 20:56:23 -04:00
Dmytro Shteflyuk
c63ec39f07 Added a test for colliding hosts passed via hosts array 2024-09-29 20:56:23 -04:00
Dmytro Shteflyuk
8df7d7d92d Do not allow both host and hosts for proxy configuration 2024-09-29 20:43:44 -04:00
Dmytro Shteflyuk
1d48a0fb0a Allow specifying multiple hosts for kamal proxy via an array 2024-09-29 20:43:44 -04:00
Ivan Velichko
0f815e17e4 Relax the safe.directory requirement
Co-authored-by: Jeremy Daer <jeremydaer@gmail.com>
2024-09-28 18:00:10 +02:00
Ivan Velichko
a310aa8fef Fix git --add safe.directory command in Dockerfile
Upgrading kamal from `v1.8.3` to `v1.9.0` broke my [kamal playground](https://labs.iximiuz.com/playgrounds/kamal):

```
laborant@dev-machine:~/svc-a$ kamal setup
  INFO [34d0def6] Running /usr/bin/env mkdir -p .kamal on 172.16.0.3
  INFO [c34cf833] Running /usr/bin/env mkdir -p .kamal on 172.16.0.4
  INFO [34d0def6] Finished in 0.147 seconds with exit status 0 (successful).
  INFO [c34cf833] Finished in 0.204 seconds with exit status 0 (successful).
Acquiring the deploy lock...
Ensure Docker is installed...
  INFO [413ee426] Running docker -v on 172.16.0.4
  INFO [f1acacba] Running docker -v on 172.16.0.3
  INFO [413ee426] Finished in 0.036 seconds with exit status 0 (successful).
  INFO [f1acacba] Finished in 0.076 seconds with exit status 0 (successful).
Log into image registry...
  INFO [94cff492] Running docker login registry.iximiuz.com -u [REDACTED] -p [REDACTED] on localhost
  INFO [94cff492] Finished in 0.077 seconds with exit status 0 (successful).
  INFO [605c535f] Running docker login registry.iximiuz.com -u [REDACTED] -p [REDACTED] on 172.16.0.4
  INFO [6002b598] Running docker login registry.iximiuz.com -u [REDACTED] -p [REDACTED] on 172.16.0.3
  INFO [605c535f] Finished in 0.083 seconds with exit status 0 (successful).
  INFO [6002b598] Finished in 0.083 seconds with exit status 0 (successful).
Build and push app image...
  INFO [9d172b1e] Running docker --version && docker buildx version on localhost
  INFO [9d172b1e] Finished in 0.059 seconds with exit status 0 (successful).
  INFO Cloning repo into build directory `/tmp/kamal-clones/svc-a-2f65914456263/workdir/`...
  INFO [26fb1bd3] Running /usr/bin/env git -C /tmp/kamal-clones/svc-a-2f65914456263 clone /workdir --recurse-submodules on localhost
 ERROR Error preparing clone: Failed to clone repo: git exit status: 32768
git stdout: Nothing written
git stderr: Cloning into 'workdir'...
fatal: detected dubious ownership in repository at '/workdir/.git'
To add an exception for this directory, call:

        git config --global --add safe.directory /workdir/.git
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
, deleting and retrying...
  INFO Cloning repo into build directory `/tmp/kamal-clones/svc-a-2f65914456263/workdir/`...
  INFO [fd4aac0c] Running /usr/bin/env git -C /tmp/kamal-clones/svc-a-2f65914456263 clone /workdir --recurse-submodules on localhost
  Finished all in 0.3 seconds
Releasing the deploy lock...
  Finished all in 0.6 seconds
  ERROR (SSHKit::Command::Failed): git exit status: 32768
git stdout: Nothing written
git stderr: Cloning into 'workdir'...
fatal: detected dubious ownership in repository at '/workdir/.git'
To add an exception for this directory, call:

        git config --global --add safe.directory /workdir/.git
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

laborant@dev-machine:~/svc-a$ kamal version
2.0.0
```

I checked the [v1.8.3...v1.9.0](https://github.com/basecamp/kamal/compare/v1.8.3...v1.9.0) diff, and couldn't find anything even remotely related to the above error.

Then I checked the `git` versions in kamal `v1.8.3` and `v1.9.0` images:

```
docker run -it --rm --entrypoint sh ghcr.io/basecamp/kamal:v1.8.3 
/workdir # git --version
git version 2.38.5
```

vs.

```
docker run -it --rm --entrypoint sh ghcr.io/basecamp/kamal:v2.0.0 
/workdir # git --version
git version 2.39.5
```

Apparently, something changed in between `2.38.5` and `2.39.5` git releases (likely yet another CVE fix), and the `git config --global --add safe.directory /workdir` stopped working.

Here is the mitigation I currently use, but it's a bit awkward to do it:

```
docker build -t ghcr.io/basecamp/kamal:v2.0.0 - <<EOF
FROM ghcr.io/basecamp/kamal:v2.0.0

RUN git config --global --add safe.directory /workdir/.git
EOF
```

Hence, this PR.

To repro, you can start a [kamal playground](https://labs.iximiuz.com/playgrounds/kamal), then `docker pull ghcr.io/basecamp/kamal:v2.0.0` to override my patched image, and `cd svc-a && kamal setup`.
2024-09-28 14:23:30 +02:00
Jan Sterba
29b02f5c30 [bitwarden] default fetch raises NoMethodError
When fetched item is not a login, Bitwarden adapter raises NoMethodError
because the returned JSON does not have the login.password value.

Add a nicer error message for that case.
2024-09-28 13:24:14 +02:00
Igor Alexandrov
6d63c4e9c6 Fixed example with the proxy status after the application has been removed 2024-09-27 21:11:51 +04:00
Igor Alexandrov
472d163cc7 Assert 404 after app is stopped 2024-09-27 19:15:42 +04:00
Igor Alexandrov
dadac999d7 Fixed kamal-proxy remove call 2024-09-27 17:45:35 +04:00
Kohki Makimoto
92d82dd1a7 test: If the provenance is false, output "--provenance false". 2024-09-26 05:50:51 +09:00
Kohki Makimoto
c17bdba61c add tests 2024-09-25 23:50:05 +09:00
Kohki Makimoto
13328687d1 support the "provenance" option in the "builder" config 2024-09-25 23:24:52 +09:00
Igor Aleksandrov
2b0810d063 Update lib/kamal/cli/templates/deploy.yml
Co-authored-by: Nick Hammond <nick@nickhammond.com>
2024-09-25 17:19:20 +04:00
Igor Alexandrov
098f1855e2 Added back accidentially removed new line 2024-09-25 12:11:45 +04:00
Igor Alexandrov
88351312bf Added app_port example to the proxy section 2024-09-25 11:25:42 +04:00
Jeremy Daer
190f4fba28 SSH: allow setting config: false to ignore local user ~/.ssh/config 2024-08-13 12:46:50 -07:00
61 changed files with 678 additions and 176 deletions

View File

@@ -27,6 +27,7 @@ jobs:
- "3.1"
- "3.2"
- "3.3"
- "3.4.0-preview2"
gemfile:
- Gemfile
- gemfiles/rails_edge.gemfile
@@ -41,6 +42,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Remove gemfile.lock
run: rm Gemfile.lock
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
@@ -49,3 +53,5 @@ jobs:
- name: Run tests
run: bin/test
env:
RUBYOPT: ${{ startsWith(matrix.ruby-version, '3.4.') && '--enable=frozen-string-literal' || '' }}

View File

@@ -1,5 +1,4 @@
# Use the official Ruby 3.2.0 Alpine image as the base image
FROM ruby:3.2.0-alpine
FROM ruby:3.3-alpine
# Install docker/buildx-bin
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx
@@ -33,7 +32,7 @@ WORKDIR /workdir
# Tell git it's safe to access /workdir/.git even if
# the directory is owned by a different user
RUN git config --global --add safe.directory /workdir
RUN git config --global --add safe.directory '*'
# Set the entrypoint to run the installed binary in /workdir
# Example: docker run -it -v "$PWD:/workdir" kamal init

View File

@@ -1,24 +1,24 @@
PATH
remote: .
specs:
kamal (2.0.0)
kamal (2.3.0)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)
concurrent-ruby (~> 1.2)
dotenv (~> 3.1)
ed25519 (~> 1.2)
net-ssh (~> 7.0)
net-ssh (~> 7.3)
sshkit (>= 1.23.0, < 2.0)
thor (~> 1.3)
zeitwerk (~> 2.5)
zeitwerk (>= 2.6.18, < 3.0)
GEM
remote: https://rubygems.org/
specs:
actionpack (7.1.3.4)
actionview (= 7.1.3.4)
activesupport (= 7.1.3.4)
actionpack (7.1.4.1)
actionview (= 7.1.4.1)
activesupport (= 7.1.4.1)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
@@ -26,13 +26,13 @@ GEM
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actionview (7.1.3.4)
activesupport (= 7.1.3.4)
actionview (7.1.4.1)
activesupport (= 7.1.4.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activesupport (7.1.3.4)
activesupport (7.1.4.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -49,7 +49,7 @@ GEM
bcrypt_pbkdf (1.1.1-x86_64-darwin)
bigdecimal (3.1.8)
builder (3.3.0)
concurrent-ruby (1.3.3)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
crass (1.0.6)
debug (1.9.2)
@@ -59,7 +59,7 @@ GEM
drb (2.2.1)
ed25519 (1.3.0)
erubi (1.13.0)
i18n (1.14.5)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
io-console (0.7.2)
irb (1.14.0)
@@ -70,7 +70,7 @@ GEM
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
minitest (5.24.1)
minitest (5.25.1)
mocha (2.4.5)
ruby2_keywords (>= 0.0.5)
mutex_m (0.2.0)
@@ -78,7 +78,7 @@ GEM
net-ssh (>= 2.6.5, < 8.0.0)
net-sftp (4.0.0)
net-ssh (>= 5.0.0, < 8.0.0)
net-ssh (7.2.3)
net-ssh (7.3.0)
nokogiri (1.16.7-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.7-x86_64-darwin)
@@ -92,7 +92,7 @@ GEM
psych (5.1.2)
stringio
racc (1.8.1)
rack (3.1.7)
rack (3.1.8)
rack-session (2.0.0)
rack (>= 3.0.0)
rack-test (2.1.0)
@@ -107,9 +107,9 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.1.3.4)
actionpack (= 7.1.3.4)
activesupport (= 7.1.3.4)
railties (7.1.4.1)
actionpack (= 7.1.4.1)
activesupport (= 7.1.4.1)
irb
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -122,8 +122,7 @@ GEM
regexp_parser (2.9.2)
reline (0.5.9)
io-console (~> 0.5)
rexml (3.3.4)
strscan
rexml (3.3.9)
rubocop (1.65.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
@@ -161,13 +160,12 @@ GEM
net-sftp (>= 2.1.2)
net-ssh (>= 2.8.0)
stringio (3.1.1)
strscan (3.1.0)
thor (1.3.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
webrick (1.8.1)
zeitwerk (2.6.17)
webrick (1.8.2)
zeitwerk (2.7.1)
PLATFORMS
arm64-darwin

View File

@@ -13,10 +13,10 @@ Gem::Specification.new do |spec|
spec.add_dependency "activesupport", ">= 7.0"
spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0"
spec.add_dependency "net-ssh", "~> 7.0"
spec.add_dependency "net-ssh", "~> 7.3"
spec.add_dependency "thor", "~> 1.3"
spec.add_dependency "dotenv", "~> 3.1"
spec.add_dependency "zeitwerk", "~> 2.5"
spec.add_dependency "zeitwerk", ">= 2.6.18", "< 3.0"
spec.add_dependency "ed25519", "~> 1.2"
spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
spec.add_dependency "concurrent-ruby", "~> 1.2"

View File

@@ -1,3 +1,5 @@
require "active_support/core_ext/array/conversions"
class Kamal::Cli::Accessory < Kamal::Cli::Base
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
def boot(name, prepare: true)

View File

@@ -68,7 +68,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
if endpoint.present?
execute *app.remove(target: endpoint), raise_on_non_zero_exit: false
execute *app.remove, raise_on_non_zero_exit: false
end
end
@@ -203,7 +203,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
run_locally do
info "Following logs on #{KAMAL.primary_host}..."
KAMAL.specific_roles ||= [ "web" ]
KAMAL.specific_roles ||= [ KAMAL.primary_role.name ]
role = KAMAL.roles_on(KAMAL.primary_host).first
app = KAMAL.app(role: role, host: host)

View File

@@ -135,7 +135,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
puts "No documentation found for #{section}"
end
desc "init", "Create config stub in config/deploy.yml and env stub in .env"
desc "init", "Create config stub in config/deploy.yml and secrets stub in .kamal"
option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub"
def init
require "fileutils"

View File

@@ -14,23 +14,25 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
version = capture_with_info(*KAMAL.proxy.version).strip.presence
if version && Kamal::Utils.older_version?(version, Kamal::Configuration::PROXY_MINIMUM_VERSION)
raise "kamal-proxy version #{version} is too old, please reboot to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
end
execute *KAMAL.proxy.start_or_run
end
end
end
desc "boot_config <set|get|clear>", "Mange kamal-proxy boot configuration"
desc "boot_config <set|get|reset>", "Manage kamal-proxy boot configuration"
option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
option :http_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTP_PORT, desc: "HTTP port to publish on the host"
option :https_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTPS_PORT, desc: "HTTPS port to publish on the host"
option :log_max_size, type: :string, default: Kamal::Configuration::PROXY_LOG_MAX_SIZE, desc: "Max size of proxy logs"
option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
def boot_config(subcommand)
case subcommand
when "set"
boot_options = [
*(KAMAL.config.proxy_publish_args(options[:http_port], options[:https_port]) if options[:publish]),
*(KAMAL.config.proxy_logging_args(options[:log_max_size])),
*options[:docker_options].map { |option| "--#{option}" }
]

View File

@@ -7,7 +7,7 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
def fetch(*secrets)
results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
return_or_puts JSON.dump(results), inline: options[:inline]
return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
end
desc "extract", "Extract a single secret from the results of a fetch call"
@@ -21,6 +21,13 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
return_or_puts value, inline: options[:inline]
end
desc "print", "Print the secrets (for debugging)"
def print
KAMAL.config.secrets.to_h.each do |key, value|
puts "#{key}=#{value}"
end
end
private
def adapter(adapter)
Kamal::Secrets::Adapters.lookup(adapter)

View File

@@ -13,11 +13,15 @@ servers:
# - 192.168.0.1
# cmd: bin/jobs
# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server).
# Set ssl: false if using something like Cloudflare to terminate SSL (but keep host!).
proxy:
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
#
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
proxy:
ssl: true
host: app.example.com
# Proxy connects to your container on port 80 by default.
# app_port: 3000
# Credentials for your image host.
registry:
@@ -87,7 +91,7 @@ builder:
# directories:
# - data:/var/lib/mysql
# redis:
# image: redis:7.0
# image: valkey/valkey:8
# host: 192.168.0.2
# port: 6379
# directories:

View File

@@ -1,13 +1,3 @@
#!/usr/bin/env ruby
#!/bin/sh
# A sample docker-setup hook
#
# Sets up a Docker network on defined hosts which can then be used by the applications containers
hosts = ENV["KAMAL_HOSTS"].split(",")
hosts.each do |ip|
destination = "root@#{ip}"
puts "Creating a Docker network \"kamal\" on #{destination}"
`ssh #{destination} docker network create kamal`
end
echo "Docker set up on $KAMAL_HOSTS..."

View File

@@ -43,7 +43,12 @@ class Kamal::Commander::Specifics
end
def specified_hosts
(specific_hosts || config.all_hosts) \
.select { |host| (specific_roles || config.roles).flat_map(&:hosts).include?(host) }
specified_hosts = specific_hosts || config.all_hosts
if (specific_role_hosts = specific_roles&.flat_map(&:hosts)).present?
specified_hosts.select { |host| specific_role_hosts.include?(host) }
else
specified_hosts
end
end
end

View File

@@ -1,7 +1,7 @@
class Kamal::Commands::Accessory < Kamal::Commands::Base
attr_reader :accessory_config
delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
:publish_args, :env_args, :volume_args, :label_args, :option_args,
:network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
:secrets_io, :secrets_path, :env_directory,
to: :accessory_config
@@ -15,7 +15,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
"--name", service_name,
"--detach",
"--restart", "unless-stopped",
"--network", "kamal",
*network_args,
*config.logging_args,
*publish_args,
*env_args,
@@ -64,7 +64,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
docker :run,
("-it" if interactive),
"--rm",
"--network", "kamal",
*network_args,
*env_args,
*volume_args,
image,

View File

@@ -5,8 +5,8 @@ module Kamal::Commands::App::Proxy
proxy_exec :deploy, role.container_prefix, *role.proxy.deploy_command_args(target: target)
end
def remove(target:)
proxy_exec :remove, role.container_prefix, *role.proxy.remove_command_args(target: target)
def remove
proxy_exec :remove, role.container_prefix
end
private

View File

@@ -11,14 +11,7 @@ module Kamal::Commands
end
def run_over_ssh(*command, host:)
"ssh".tap do |cmd|
if config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Jump)
cmd << " -J #{config.ssh.proxy.jump_proxies}"
elsif config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Command)
cmd << " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
end
cmd << " -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
end
"ssh#{ssh_proxy_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
end
def container_id_for(container_name:, only_running: false)
@@ -92,5 +85,14 @@ module Kamal::Commands
def tags(**details)
Kamal::Tags.from_config(config, **details)
end
def ssh_proxy_args
case config.ssh.proxy
when Net::SSH::Proxy::Jump
" -J #{config.ssh.proxy.jump_proxies}"
when Net::SSH::Proxy::Command
" -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
end
end
end
end

View File

@@ -6,7 +6,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
delegate :argumentize, to: Kamal::Utils
delegate \
:args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
:cache_from, :cache_to, :ssh, :driver, :docker_driver?,
:cache_from, :cache_to, :ssh, :provenance, :driver, :docker_driver?,
to: :builder_config
def clean
@@ -37,7 +37,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
end
def build_options
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ]
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance ]
end
def build_context
@@ -97,6 +97,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
argumentize "--ssh", ssh if ssh.present?
end
def builder_provenance
argumentize "--provenance", provenance unless provenance.nil?
end
def builder_config
config.builder
end

View File

@@ -1,29 +1,31 @@
module Kamal::Commands::Builder::Clone
extend ActiveSupport::Concern
included do
delegate :clone_directory, :build_directory, to: :"config.builder"
end
def clone
git :clone, Kamal::Git.root, "--recurse-submodules", path: clone_directory
git :clone, escaped_root, "--recurse-submodules", path: config.builder.clone_directory.shellescape
end
def clone_reset_steps
[
git(:remote, "set-url", :origin, Kamal::Git.root, path: build_directory),
git(:fetch, :origin, path: build_directory),
git(:reset, "--hard", Kamal::Git.revision, path: build_directory),
git(:clean, "-fdx", path: build_directory),
git(:submodule, :update, "--init", path: build_directory)
git(:remote, "set-url", :origin, escaped_root, path: escaped_build_directory),
git(:fetch, :origin, path: escaped_build_directory),
git(:reset, "--hard", Kamal::Git.revision, path: escaped_build_directory),
git(:clean, "-fdx", path: escaped_build_directory),
git(:submodule, :update, "--init", path: escaped_build_directory)
]
end
def clone_status
git :status, "--porcelain", path: build_directory
git :status, "--porcelain", path: escaped_build_directory
end
def clone_revision
git :"rev-parse", :HEAD, path: build_directory
git :"rev-parse", :HEAD, path: escaped_build_directory
end
def escaped_root
Kamal::Git.root.shellescape
end
def escaped_build_directory
config.builder.build_directory.shellescape
end
end

View File

@@ -14,12 +14,15 @@ class Kamal::Configuration
include Validation
PROXY_MINIMUM_VERSION = "v0.6.0"
PROXY_MINIMUM_VERSION = "v0.8.2"
PROXY_HTTP_PORT = 80
PROXY_HTTPS_PORT = 443
PROXY_LOG_MAX_SIZE = "10m"
class << self
def create_from(config_file:, destination: nil, version: nil)
ENV["KAMAL_DESTINATION"] = destination
raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
new raw_config, destination: destination, version: version
@@ -250,8 +253,12 @@ class Kamal::Configuration
argumentize "--publish", [ "#{http_port}:#{PROXY_HTTP_PORT}", "#{https_port}:#{PROXY_HTTPS_PORT}" ]
end
def proxy_logging_args(max_size)
argumentize "--log-opt", "max-size=#{max_size}" if max_size.present?
end
def proxy_options_default
proxy_publish_args PROXY_HTTP_PORT, PROXY_HTTPS_PORT
[ *proxy_publish_args(PROXY_HTTP_PORT, PROXY_HTTPS_PORT), *proxy_logging_args(PROXY_LOG_MAX_SIZE) ]
end
def proxy_image
@@ -360,7 +367,7 @@ class Kamal::Configuration
end
def ensure_unique_hosts_for_ssl_roles
hosts = roles.select(&:ssl?).map { |role| role.proxy.host }
hosts = roles.select(&:ssl?).flat_map { |role| role.proxy.hosts }
duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?

View File

@@ -1,6 +1,8 @@
class Kamal::Configuration::Accessory
include Kamal::Configuration::Validation
DEFAULT_NETWORK = "kamal"
delegate :argumentize, :optionize, to: Kamal::Utils
attr_reader :name, :accessory_config, :env
@@ -38,6 +40,10 @@ class Kamal::Configuration::Accessory
end
end
def network_args
argumentize "--network", network
end
def publish_args
argumentize "--publish", port if port
end
@@ -173,4 +179,8 @@ class Kamal::Configuration::Accessory
accessory_config["roles"].flat_map { |role| config.role(role).hosts }
end
end
def network
accessory_config["network"] || DEFAULT_NETWORK
end
end

View File

@@ -111,6 +111,10 @@ class Kamal::Configuration::Builder
builder_config["ssh"]
end
def provenance
builder_config["provenance"]
end
def git_clone?
Kamal::Git.used? && builder_config["context"].nil?
end
@@ -166,7 +170,7 @@ class Kamal::Configuration::Builder
end
def cache_to_config_for_registry
[ "type=registry", builder_config["cache"]&.fetch("options", nil), "ref=#{cache_image_ref}" ].compact.join(",")
[ "type=registry", "ref=#{cache_image_ref}", builder_config["cache"]&.fetch("options", nil) ].compact.join(",")
end
def repo_basename

View File

@@ -90,3 +90,11 @@ accessories:
# They are not created or copied before mounting:
volumes:
- /path/to/mysql-logs:/var/log/mysql
# Network
#
# The network the accessory will be attached to.
#
# Defaults to kamal:
network: custom

View File

@@ -102,3 +102,9 @@ builder:
#
# The build driver to use, defaults to `docker-container`:
driver: docker
# Provenance
#
# It is used to configure provenance attestations for the build result.
# The value can also be a boolean to enable or disable provenance attestations.
provenance: mode=max

View File

@@ -17,16 +17,19 @@
# `proxy: true` or providing a proxy configuration.
proxy:
# Host
# Hosts
#
# The hosts that will be used to serve the app. The proxy will only route requests
# to this host to your app.
#
# If no hosts are set, then all requests will be forwarded, except for matching
# requests for other apps deployed on that server that do have a host set.
#
# Specify one of `host` or `hosts`.
host: foo.example.com
# If multiple hosts are needed, these can be specified by comma-separating the hosts.
host: foo.example.com,bar.example.com
hosts:
- foo.example.com
- bar.example.com
# App port
#

View File

@@ -61,3 +61,10 @@ ssh:
# An array of strings, with each element of the array being
# a raw private key in PEM format.
key_data: [ "-----BEGIN OPENSSH PRIVATE KEY-----" ]
# Config
#
# Set to true to load the default OpenSSH config files (~/.ssh/config,
# /etc/ssh_config), to false ignore config files, or to a file path
# (or array of paths) to load specific configuration. Defaults to true.
config: true

View File

@@ -22,14 +22,14 @@ class Kamal::Configuration::Proxy
proxy_config.fetch("ssl", false)
end
def host
proxy_config["host"]
def hosts
proxy_config["hosts"] || proxy_config["host"]&.split(",") || []
end
def deploy_options
{
host: proxy_config["host"],
tls: proxy_config["ssl"] ? true : nil,
host: hosts,
tls: proxy_config["ssl"].presence,
"deploy-timeout": seconds_duration(config.deploy_timeout),
"drain-timeout": seconds_duration(config.drain_timeout),
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
@@ -48,11 +48,7 @@ class Kamal::Configuration::Proxy
end
def deploy_command_args(target:)
optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options)
end
def remove_command_args(target:)
optionize({ target: "#{target}:#{app_port}" })
optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options), with: "="
end
def merge(other)

View File

@@ -3,9 +3,13 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
unless config.nil?
super
if config["host"].blank? && config["ssl"]
if config["host"].blank? && config["hosts"].blank? && config["ssl"]
error "Must set a host to enable automatic SSL"
end
if (config.keys & [ "host", "hosts" ]).size > 1
error "Specify one of 'host' or 'hosts', not both"
end
end
end
end

View File

@@ -37,6 +37,8 @@ class Kamal::EnvFile
def escape_docker_env_file_ascii_value(value)
# Doublequotes are treated literally in docker env files
# so remove leading and trailing ones and unescape any others
value.to_s.dump[1..-2].gsub(/\\"/, "\"")
value.to_s.dump[1..-2]
.gsub(/\\"/, "\"")
.gsub(/\\#/, "#")
end
end

View File

@@ -1,13 +1,10 @@
require "dotenv"
class Kamal::Secrets
attr_reader :secrets_files
Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!
def initialize(destination: nil)
@secrets_files = \
[ ".kamal/secrets-common", ".kamal/secrets#{(".#{destination}" if destination)}" ].select { |f| File.exist?(f) }
@destination = destination
@mutex = Mutex.new
end
@@ -17,10 +14,10 @@ class Kamal::Secrets
secrets.fetch(key)
end
rescue KeyError
if secrets_files
if secrets_files.present?
raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
else
raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files provided"
raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files (#{secrets_filenames.join(", ")}) provided"
end
end
@@ -28,10 +25,18 @@ class Kamal::Secrets
secrets
end
def secrets_files
@secrets_files ||= secrets_filenames.select { |f| File.exist?(f) }
end
private
def secrets
@secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
secrets.merge!(::Dotenv.parse(secrets_file))
end
end
def secrets_filenames
[ ".kamal/secrets-common", ".kamal/secrets#{(".#{@destination}" if @destination)}" ]
end
end

View File

@@ -2,6 +2,7 @@ class Kamal::Secrets::Adapters::Base
delegate :optionize, to: Kamal::Utils
def fetch(secrets, account:, from: nil)
check_dependencies!
session = login(account)
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
fetch_secrets(full_secrets, account: account, session: session)
@@ -15,4 +16,8 @@ class Kamal::Secrets::Adapters::Base
def fetch_secrets(...)
raise NotImplementedError
end
def check_dependencies!
raise NotImplementedError
end
end

View File

@@ -25,23 +25,31 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
{}.tap do |results|
items_fields(secrets).each do |item, fields|
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
raise RuntimeError, "Could not read #{secret} from Bitwarden" unless $?.success?
raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success?
item_json = JSON.parse(item_json)
if fields.any?
fields.each do |field|
item_field = item_json["fields"].find { |f| f["name"] == field }
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
value = item_field["value"]
results["#{item}/#{field}"] = value
end
results.merge! fetch_secrets_from_fields(fields, item, item_json)
elsif item_json.dig("login", "password")
results[item] = item_json.dig("login", "password")
elsif item_json["fields"]&.any?
fields = item_json["fields"].pluck("name")
results.merge! fetch_secrets_from_fields(fields, item, item_json)
else
results[item] = item_json["login"]["password"]
raise RuntimeError, "Item #{item} is not a login type item and no fields were specified"
end
end
end
end
def fetch_secrets_from_fields(fields, item, item_json)
fields.to_h do |field|
item_field = item_json["fields"].find { |f| f["name"] == field }
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
value = item_field["value"]
[ "#{item}/#{field}", value ]
end
end
def items_fields(secrets)
{}.tap do |items|
secrets.each do |secret|
@@ -61,4 +69,13 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
result = `#{full_command}`.strip
raw ? result : JSON.parse(result)
end
def check_dependencies!
raise RuntimeError, "Bitwarden CLI is not installed" unless cli_installed?
end
def cli_installed?
`bw --version 2> /dev/null`
$?.success?
end
end

View File

@@ -27,4 +27,13 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
end
end
end
def check_dependencies!
raise RuntimeError, "LastPass CLI is not installed" unless cli_installed?
end
def cli_installed?
`lpass --version 2> /dev/null`
$?.success?
end
end

View File

@@ -58,4 +58,13 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success?
end
end
def check_dependencies!
raise RuntimeError, "1Password CLI is not installed" unless cli_installed?
end
def cli_installed?
`op --version 2> /dev/null`
$?.success?
end
end

View File

@@ -7,4 +7,8 @@ class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
def fetch_secrets(secrets, account:, session:)
secrets.to_h { |secret| [ secret, secret.reverse ] }
end
def check_dependencies!
# no op
end
end

View File

@@ -16,7 +16,7 @@ class Kamal::Secrets::Dotenv::InlineCommandSubstitution
else
if command =~ /\A\s*kamal\s*secrets\s+/
# Inline the command
inline_secrets_command(command).shellescape
inline_secrets_command(command)
else
# Execute the command and return the value
`#{command}`.chomp

View File

@@ -12,6 +12,8 @@ module Kamal::Utils
attr = "#{key}=#{escape_shell_value(value)}"
attr = self.sensitive(attr, redaction: "#{key}=[REDACTED]") if sensitive
[ argument, attr ]
elsif value == false
[ argument, "#{key}=false" ]
else
[ argument, key ]
end

View File

@@ -1,3 +1,3 @@
module Kamal
VERSION = "2.0.0"
VERSION = "2.3.0"
end

View File

@@ -130,7 +130,7 @@ class CliAppTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false)
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :exec, "kamal-proxy", "kamal-proxy", :deploy, "app-web", "--target", "\"123:80\"", "--deploy-timeout", "\"1s\"", "--drain-timeout", "\"30s\"", "--buffer-requests", "--buffer-responses", "--log-request-header", "\"Cache-Control\"", "--log-request-header", "\"Last-Modified\"", "--log-request-header", "\"User-Agent\"").raises(SSHKit::Command::Failed.new("Failed to deploy"))
.with(:docker, :exec, "kamal-proxy", "kamal-proxy", :deploy, "app-web", "--target=\"123:80\"", "--deploy-timeout=\"1s\"", "--drain-timeout=\"30s\"", "--buffer-requests", "--buffer-responses", "--log-request-header=\"Cache-Control\"", "--log-request-header=\"Last-Modified\"", "--log-request-header=\"User-Agent\"").raises(SSHKit::Command::Failed.new("Failed to deploy"))
stderred do
run_command("boot", config: :with_roles, host: nil, allow_execute_error: true).tap do |output|
@@ -190,7 +190,7 @@ class CliAppTest < CliTestCase
run_command("start").tap do |output|
assert_match "docker start app-web-999", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"999:80\" --deploy-timeout \"30s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"999:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\"", output
end
end
@@ -383,7 +383,7 @@ class CliAppTest < CliTestCase
assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename
assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal\/apps\/app\/env\/roles\/web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh\/app:latest/, output
assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target "123:80"/, output
assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target="123:80"/, output
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
end
end
@@ -392,8 +392,8 @@ class CliAppTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
run_command("boot", config: :with_proxy_roles, host: nil).tap do |output|
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"123:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --target-timeout \"10s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web2 --target \"123:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --target-timeout \"15s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"123:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --target-timeout=\"10s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web2 --target=\"123:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --target-timeout=\"15s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"", output
end
end

View File

@@ -4,7 +4,7 @@ class CliProxyTest < CliTestCase
test "boot" do
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output
end
end
@@ -18,11 +18,11 @@ class CliProxyTest < CliTestCase
exception = assert_raises do
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output
end
end
assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, please reboot to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
ensure
Thread.report_on_exception = false
end
@@ -36,7 +36,7 @@ class CliProxyTest < CliTestCase
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output
end
ensure
Thread.report_on_exception = false
@@ -57,14 +57,14 @@ class CliProxyTest < CliTestCase
assert_match "docker container stop kamal-proxy on 1.1.1.1", output
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image} on 1.1.1.1", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.1", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image} on 1.1.1.1", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"abcdefabcdef:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" on 1.1.1.1", output
assert_match "docker container stop kamal-proxy on 1.1.1.2", output
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image} on 1.1.1.2", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.2", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image} on 1.1.1.2", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"abcdefabcdef:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" on 1.1.1.2", output
end
end
@@ -198,13 +198,13 @@ class CliProxyTest < CliTestCase
assert_match "/usr/bin/env mkdir -p .kamal", output
assert_match "docker network create kamal", output
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output
assert_match "/usr/bin/env mkdir -p .kamal", output
assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output
assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output
assert_match "Uploading \"\\n\" to .kamal/apps/app/env/roles/web.env", output
assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-.* -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh/app:latest}, output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"12345678:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"12345678:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"", output
assert_match "docker container ls --all --filter name=^app-web-12345678$ --quiet | xargs docker stop", output
assert_match "docker tag dhh/app:latest dhh/app:latest", output
assert_match "/usr/bin/env mkdir -p .kamal", output
@@ -240,7 +240,7 @@ class CliProxyTest < CliTestCase
run_command("boot_config", "set").tap do |output|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
assert_match "Uploading \"--publish 80:80 --publish 443:443\" to .kamal/proxy/options on #{host}", output
assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output
end
end
end
@@ -249,7 +249,25 @@ class CliProxyTest < CliTestCase
run_command("boot_config", "set", "--publish", "false").tap do |output|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
assert_match "Uploading \"\" to .kamal/proxy/options on #{host}", output
assert_match "Uploading \"--log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output
end
end
end
test "boot_config set custom max_size" do
run_command("boot_config", "set", "--log-max-size", "100m").tap do |output|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=100m\" to .kamal/proxy/options on #{host}", output
end
end
end
test "boot_config set no log max size" do
run_command("boot_config", "set", "--log-max-size=").tap do |output|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
assert_match "Uploading \"--publish 80:80 --publish 443:443\" to .kamal/proxy/options on #{host}", output
end
end
end
@@ -258,7 +276,7 @@ class CliProxyTest < CliTestCase
run_command("boot_config", "set", "--http-port", "8080", "--https-port", "8443").tap do |output|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
assert_match "Uploading \"--publish 8080:80 --publish 8443:443\" to .kamal/proxy/options on #{host}", output
assert_match "Uploading \"--publish 8080:80 --publish 8443:443 --log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output
end
end
end
@@ -267,14 +285,14 @@ class CliProxyTest < CliTestCase
run_command("boot_config", "set", "--docker_options", "label=foo=bar", "add_host=thishost:thathost").tap do |output|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
assert_match "Uploading \"--publish 80:80 --publish 443:443 --label=foo=bar --add_host=thishost:thathost\" to .kamal/proxy/options on #{host}", output
assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m --label=foo=bar --add_host=thishost:thathost\" to .kamal/proxy/options on #{host}", output
end
end
end
test "boot_config get" do
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:cat, ".kamal/proxy/options", "||", :echo, "\"--publish 80:80 --publish 443:443\"")
.with(:cat, ".kamal/proxy/options", "||", :echo, "\"--publish 80:80 --publish 443:443 --log-opt max-size=10m\"")
.returns("--publish 80:80 --publish 8443:443 --label=foo=bar")
.twice

View File

@@ -3,7 +3,7 @@ require_relative "cli_test_case"
class CliSecretsTest < CliTestCase
test "fetch" do
assert_equal \
"{\"foo\":\"oof\",\"bar\":\"rab\",\"baz\":\"zab\"}",
"\\{\\\"foo\\\":\\\"oof\\\",\\\"bar\\\":\\\"rab\\\",\\\"baz\\\":\\\"zab\\\"\\}",
run_command("fetch", "foo", "bar", "baz", "--account", "myaccount", "--adapter", "test")
end
@@ -15,6 +15,12 @@ class CliSecretsTest < CliTestCase
assert_equal "oof", run_command("extract", "foo", "{\"abc/foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
end
test "print" do
with_test_secrets("secrets" => "SECRET1=ABC\nSECRET2=${SECRET1}DEF\n") do
assert_equal "SECRET1=ABC\nSECRET2=ABCDEF", run_command("print")
end
end
private
def run_command(*command)
stdouted { Kamal::Cli::Secrets.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }

View File

@@ -150,6 +150,27 @@ class CommanderTest < ActiveSupport::TestCase
assert_equal [ "1.1.1.2" ], @kamal.proxy_hosts
end
test "accessory hosts without filtering" do
configure_with(:deploy_with_single_accessory)
assert_equal [ "1.1.1.5" ], @kamal.accessory_hosts
configure_with(:deploy_with_accessories_on_independent_server)
assert_equal [ "1.1.1.5", "1.1.1.1", "1.1.1.2" ], @kamal.accessory_hosts
end
test "accessory hosts with role filtering" do
configure_with(:deploy_with_single_accessory)
@kamal.specific_roles = [ "web" ]
assert_equal [], @kamal.accessory_hosts
configure_with(:deploy_with_accessories_on_independent_server)
@kamal.specific_roles = [ "web" ]
assert_equal [ "1.1.1.1", "1.1.1.2" ], @kamal.accessory_hosts
@kamal.specific_roles = [ "workers" ]
assert_equal [], @kamal.accessory_hosts
end
private
def configure_with(variant)
@kamal = Kamal::Commander.new.tap do |kamal|

View File

@@ -71,6 +71,14 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
new_command(:busybox).run.join(" ")
end
test "run in custom network" do
@config[:accessories]["mysql"]["network"] = "custom"
assert_equal \
"docker run --name app-mysql --detach --restart unless-stopped --network custom --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0",
new_command(:mysql).run.join(" ")
end
test "start" do
assert_equal \
"docker container start app-mysql",

View File

@@ -115,14 +115,38 @@ class CommandsAppTest < ActiveSupport::TestCase
test "deploy" do
assert_equal \
"docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\" --deploy-timeout \"30s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"",
"docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
new_command.deploy(target: "172.1.0.2").join(" ")
end
test "deploy with SSL" do
@config[:proxy] = { "ssl" => true, "host" => "example.com" }
assert_equal \
"docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --host=\"example.com\" --tls --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
new_command.deploy(target: "172.1.0.2").join(" ")
end
test "deploy with SSL targeting multiple hosts" do
@config[:proxy] = { "ssl" => true, "hosts" => [ "example.com", "anotherexample.com" ] }
assert_equal \
"docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --host=\"example.com\" --host=\"anotherexample.com\" --tls --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
new_command.deploy(target: "172.1.0.2").join(" ")
end
test "deploy with SSL false" do
@config[:proxy] = { "ssl" => false }
assert_equal \
"docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
new_command.deploy(target: "172.1.0.2").join(" ")
end
test "remove" do
assert_equal \
"docker exec kamal-proxy kamal-proxy remove app-web --target \"172.1.0.2:80\"",
new_command.remove(target: "172.1.0.2").join(" ")
"docker exec kamal-proxy kamal-proxy remove app-web",
new_command.remove.join(" ")
end

View File

@@ -144,20 +144,45 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder.push.join(" ")
end
test "push with provenance" do
builder = new_builder_command(builder: { "provenance" => "mode=max" })
assert_equal \
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --provenance mode=max .",
builder.push.join(" ")
end
test "push with provenance false" do
builder = new_builder_command(builder: { "provenance" => false })
assert_equal \
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --provenance false .",
builder.push.join(" ")
end
test "mirror count" do
command = new_builder_command
assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ")
end
test "clone path with spaces" do
command = new_builder_command
Kamal::Git.stubs(:root).returns("/absolute/path with spaces")
clone_command = command.clone.join(" ")
clone_reset_commands = command.clone_reset_steps.map { |a| a.join(" ") }
assert_match(%r{path\\ with\\ space}, clone_command)
assert_no_match(%r{path with spaces}, clone_command)
clone_reset_commands.each do |command|
assert_match(%r{path\\ with\\ space}, command)
assert_no_match(%r{path with spaces}, command)
end
end
private
def new_builder_command(additional_config = {})
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.deep_merge(additional_config), version: "123"))
end
def build_directory
"#{Dir.tmpdir}/kamal-clones/app/kamal/"
end
def local_arch
Kamal::Utils.docker_arch
end

View File

@@ -15,7 +15,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
test "run" do
assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
new_command.run.join(" ")
end
@@ -23,7 +23,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
@config.delete(:proxy)
assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
new_command.run.join(" ")
end
@@ -113,7 +113,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
test "get_boot_options" do
assert_equal \
"cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\"",
"cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\"",
new_command.get_boot_options.join(" ")
end

View File

@@ -152,4 +152,13 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
test "options" do
assert_equal [ "--cpus", "\"4\"", "--memory", "\"2GB\"" ], @config.accessory(:redis).option_args
end
test "network_args default" do
assert_equal [ "--network", "kamal" ], @config.accessory(:mysql).network_args
end
test "network_args with configured options" do
@deploy[:accessories]["mysql"]["network"] = "database"
assert_equal [ "--network", "database" ], @config.accessory(:mysql).network_args
end
end

View File

@@ -64,7 +64,7 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
assert_equal "type=registry,ref=dhh/app-build-cache", config.builder.cache_from
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=dhh/app-build-cache", config.builder.cache_to
assert_equal "type=registry,ref=dhh/app-build-cache,mode=max,image-manifest=true,oci-mediatypes=true", config.builder.cache_to
end
test "setting registry cache when using a custom registry" do
@@ -72,14 +72,14 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_from
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_to
assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache,mode=max,image-manifest=true,oci-mediatypes=true", config.builder.cache_to
end
test "setting registry cache with image" do
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "image" => "kamal", "options" => "mode=max" } }
assert_equal "type=registry,ref=kamal", config.builder.cache_from
assert_equal "type=registry,mode=max,ref=kamal", config.builder.cache_to
assert_equal "type=registry,ref=kamal,mode=max", config.builder.cache_to
end
test "args" do
@@ -134,6 +134,16 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
assert_equal "default=$SSH_AUTH_SOCK", config.builder.ssh
end
test "provenance" do
assert_nil config.builder.provenance
end
test "setting provenance" do
@deploy[:builder]["provenance"] = "mode=max"
assert_equal "mode=max", config.builder.provenance
end
test "local disabled but no remote set" do
@deploy[:builder]["local"] = false

View File

@@ -13,15 +13,29 @@ class ConfigurationProxyTest < ActiveSupport::TestCase
assert_equal true, config.proxy.ssl?
end
test "ssl with multiple hosts passed via host" do
@deploy[:proxy] = { "ssl" => true, "host" => "example.com,anotherexample.com" }
assert_equal true, config.proxy.ssl?
end
test "ssl with multiple hosts passed via hosts" do
@deploy[:proxy] = { "ssl" => true, "hosts" => [ "example.com", "anotherexample.com" ] }
assert_equal true, config.proxy.ssl?
end
test "ssl with no host" do
@deploy[:proxy] = { "ssl" => true }
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
end
test "ssl with both host and hosts" do
@deploy[:proxy] = { "ssl" => true, host: "example.com", hosts: [ "anotherexample.com" ] }
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
end
test "ssl false" do
@deploy[:proxy] = { "ssl" => false }
assert_not config.proxy.ssl?
assert_not config.proxy.deploy_options.has_key?(:tls)
end
private

View File

@@ -222,6 +222,13 @@ class ConfigurationTest < ActiveSupport::TestCase
assert_equal "my-user", config.registry.username
end
test "destination is loaded into env" do
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_dest.yml", __dir__))
config = Kamal::Configuration.create_from config_file: dest_config_file, destination: "world"
assert_equal ENV["KAMAL_DESTINATION"], "world"
end
test "destination yml config merge" do
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_dest.yml", __dir__))
@@ -377,4 +384,15 @@ class ConfigurationTest < ActiveSupport::TestCase
assert_equal "Different roles can't share the same host for SSL: foo.example.com", exception.message
end
test "two proxy ssl roles with same host in a hosts array" do
@deploy_with_roles[:servers]["web"] = { "hosts" => [ "1.1.1.1" ], "proxy" => { "ssl" => true, "hosts" => [ "foo.example.com", "bar.example.com" ] } }
@deploy_with_roles[:servers]["workers"] = { "hosts" => [ "1.1.1.1" ], "proxy" => { "ssl" => true, "hosts" => [ "www.example.com", "foo.example.com" ] } }
exception = assert_raises(Kamal::ConfigurationError) do
Kamal::Configuration.new(@deploy_with_roles)
end
assert_equal "Different roles can't share the same host for SSL: foo.example.com", exception.message
end
end

View File

@@ -11,6 +11,16 @@ class EnvFileTest < ActiveSupport::TestCase
Kamal::EnvFile.new(env).to_s
end
test "to_s won't escape '#'" do
env = {
"foo" => '#$foo',
"bar" => '#{bar}'
}
assert_equal "foo=\#$foo\nbar=\#{bar}\n", \
Kamal::EnvFile.new(env).to_s
end
test "to_str won't escape chinese characters" do
env = {
"foo" => '你好 means hello, "欢迎" means welcome, that\'s simple! 😃 {smile}'

View File

@@ -0,0 +1,38 @@
service: app
image: dhh/app
servers:
web:
- "1.1.1.1"
- "1.1.1.2"
workers:
- "1.1.1.3"
- "1.1.1.4"
registry:
username: user
password: pw
builder:
arch: amd64
accessories:
mysql:
image: mysql:5.7
host: 1.1.1.5
port: 3306
env:
clear:
MYSQL_ROOT_HOST: '%'
secret:
- MYSQL_ROOT_PASSWORD
files:
- test/fixtures/files/my.cnf:/etc/mysql/my.cnf
directories:
- data:/var/lib/mysql
redis:
image: redis:latest
roles:
- web
port: 6379
directories:
- data:/data
readiness_delay: 0

View File

@@ -0,0 +1,29 @@
service: app
image: dhh/app
servers:
web:
- "1.1.1.1"
- "1.1.1.2"
workers:
- "1.1.1.3"
- "1.1.1.4"
registry:
username: user
password: pw
builder:
arch: amd64
accessories:
mysql:
image: mysql:5.7
host: 1.1.1.5
port: 3306
env:
clear:
MYSQL_ROOT_HOST: '%'
secret:
- MYSQL_ROOT_PASSWORD
files:
- test/fixtures/files/my.cnf:/etc/mysql/my.cnf
directories:
- data:/var/lib/mysql

View File

@@ -8,7 +8,7 @@ class AppTest < IntegrationTest
kamal :app, :stop
assert_app_is_down
assert_app_not_found
kamal :app, :start
@@ -48,7 +48,7 @@ class AppTest < IntegrationTest
kamal :app, :remove
assert_app_is_down
assert_app_not_found
assert_app_directory_removed
end
end

View File

@@ -15,14 +15,15 @@ readiness_delay: 0
proxy:
host: localhost
ssl: false
healthcheck:
interval: 1
timeout: 1
path: "/up"
response_timeout: 2
buffering:
requests: true
responses: true
requests: false
responses: false
memory: 400_000
max_request_body: 40_000_000
max_response_body: 40_000_000

View File

@@ -50,6 +50,12 @@ class IntegrationTest < ActiveSupport::TestCase
assert_equal "502", response.code
end
def assert_app_not_found
response = app_response
debug_response_code(response, "404")
assert_equal "404", response.code
end
def assert_app_is_up(version: nil, app: @app)
response = app_response(app: app)
debug_response_code(response, "200")

View File

@@ -2,23 +2,40 @@ require "test_helper"
class BitwardenAdapterTest < SecretAdapterTestCase
test "fetch" do
stub_ticks.with("bw --version 2> /dev/null")
stub_unlocked
stub_ticks.with("bw sync").returns("")
stub_mypassword
json = JSON.parse(run_command("fetch", "mypassword"))
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
expected_json = { "mypassword"=>"secret123" }
assert_equal expected_json, json
end
test "fetch with no login" do
stub_ticks.with("bw --version 2> /dev/null")
stub_unlocked
stub_ticks.with("bw sync").returns("")
stub_noteitem
error = assert_raises RuntimeError do
JSON.parse(shellunescape(run_command("fetch", "mynote")))
end
assert_match(/not a login type item/, error.message)
end
test "fetch with from" do
stub_ticks.with("bw --version 2> /dev/null")
stub_unlocked
stub_ticks.with("bw sync").returns("")
stub_myitem
json = JSON.parse(run_command("fetch", "--from", "myitem", "field1", "field2", "field3"))
json = JSON.parse(shellunescape(run_command("fetch", "--from", "myitem", "field1", "field2", "field3")))
expected_json = {
"myitem/field1"=>"secret1", "myitem/field2"=>"blam", "myitem/field3"=>"fewgrwjgk"
@@ -27,7 +44,26 @@ class BitwardenAdapterTest < SecretAdapterTestCase
assert_equal expected_json, json
end
test "fetch all with from" do
stub_ticks.with("bw --version 2> /dev/null")
stub_unlocked
stub_ticks.with("bw sync").returns("")
stub_noteitem_with_fields
json = JSON.parse(shellunescape(run_command("fetch", "mynotefields")))
expected_json = {
"mynotefields/field1"=>"secret1", "mynotefields/field2"=>"blam", "mynotefields/field3"=>"fewgrwjgk",
"mynotefields/field4"=>"auto"
}
assert_equal expected_json, json
end
test "fetch with multiple items" do
stub_ticks.with("bw --version 2> /dev/null")
stub_unlocked
stub_ticks.with("bw sync").returns("")
@@ -59,7 +95,7 @@ class BitwardenAdapterTest < SecretAdapterTestCase
JSON
json = JSON.parse(run_command("fetch", "mypassword", "myitem/field1", "myitem/field2", "myitem2/field3"))
json = JSON.parse(shellunescape(run_command("fetch", "mypassword", "myitem/field1", "myitem/field2", "myitem2/field3")))
expected_json = {
"mypassword"=>"secret123", "myitem/field1"=>"secret1", "myitem/field2"=>"blam", "myitem2/field3"=>"fewgrwjgk"
@@ -69,6 +105,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
end
test "fetch unauthenticated" do
stub_ticks.with("bw --version 2> /dev/null")
stub_ticks
.with("bw status")
.returns(
@@ -82,7 +120,7 @@ class BitwardenAdapterTest < SecretAdapterTestCase
stub_ticks.with("bw sync").returns("")
stub_mypassword
json = JSON.parse(run_command("fetch", "mypassword"))
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
expected_json = { "mypassword"=>"secret123" }
@@ -90,6 +128,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
end
test "fetch locked" do
stub_ticks.with("bw --version 2> /dev/null")
stub_ticks
.with("bw status")
.returns(
@@ -107,7 +147,7 @@ class BitwardenAdapterTest < SecretAdapterTestCase
stub_ticks.with("bw sync").returns("")
stub_mypassword
json = JSON.parse(run_command("fetch", "mypassword"))
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
expected_json = { "mypassword"=>"secret123" }
@@ -115,6 +155,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
end
test "fetch locked with session" do
stub_ticks.with("bw --version 2> /dev/null")
stub_ticks
.with("bw status")
.returns(
@@ -132,13 +174,22 @@ class BitwardenAdapterTest < SecretAdapterTestCase
stub_ticks.with("BW_SESSION=0987654321 bw sync").returns("")
stub_mypassword(session: "0987654321")
json = JSON.parse(run_command("fetch", "mypassword"))
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
expected_json = { "mypassword"=>"secret123" }
assert_equal expected_json, json
end
test "fetch without CLI installed" do
stub_ticks_with("bw --version 2> /dev/null", succeed: false)
error = assert_raises RuntimeError do
JSON.parse(shellunescape(run_command("fetch", "mynote")))
end
assert_equal "Bitwarden CLI is not installed", error.message
end
private
def run_command(*command)
stdouted do
@@ -181,6 +232,60 @@ class BitwardenAdapterTest < SecretAdapterTestCase
JSON
end
def stub_noteitem(session: nil)
stub_ticks
.with("#{"BW_SESSION=#{session} " if session}bw get item mynote")
.returns(<<~JSON)
{
"passwordHistory":null,
"revisionDate":"2024-09-28T09:07:27.461Z",
"creationDate":"2024-09-28T09:07:00.740Z",
"deletedDate":null,
"object":"item",
"id":"aaaaaaaa-cccc-eeee-0000-222222222222",
"organizationId":null,
"folderId":null,
"type":2,
"reprompt":0,
"name":"noteitem",
"notes":"NOTES",
"favorite":false,
"secureNote":{"type":0},
"collectionIds":[]
}
JSON
end
def stub_noteitem_with_fields(session: nil)
stub_ticks
.with("#{"BW_SESSION=#{session} " if session}bw get item mynotefields")
.returns(<<~JSON)
{
"passwordHistory":null,
"revisionDate":"2024-09-28T09:07:27.461Z",
"creationDate":"2024-09-28T09:07:00.740Z",
"deletedDate":null,
"object":"item",
"id":"aaaaaaaa-cccc-eeee-0000-222222222222",
"organizationId":null,
"folderId":null,
"type":2,
"reprompt":0,
"name":"noteitem",
"notes":"NOTES",
"favorite":false,
"fields":[
{"name":"field1","value":"secret1","type":1,"linkedId":null},
{"name":"field2","value":"blam","type":1,"linkedId":null},
{"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null},
{"name":"field4","value":"auto","type":1,"linkedId":null}
],
"secureNote":{"type":0},
"collectionIds":[]
}
JSON
end
def stub_myitem
stub_ticks
.with("bw get item myitem")
@@ -202,7 +307,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
"fields":[
{"name":"field1","value":"secret1","type":1,"linkedId":null},
{"name":"field2","value":"blam","type":1,"linkedId":null},
{"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null}
{"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null},
{"name":"field4","value":"auto","type":1,"linkedId":null}
],
"login":{"fido2Credentials":[],"uris":[],"username":null,"password":null,"totp":null,"passwordRevisionDate":null},"collectionIds":[]
}

View File

@@ -12,10 +12,4 @@ class SecretsInlineCommandSubstitution < SecretAdapterTestCase
substituted = Kamal::Secrets::Dotenv::InlineCommandSubstitution.call("FOO=$(blah)", nil, overwrite: false)
assert_equal "FOO=results", substituted
end
test "escapes correctly" do
Kamal::Cli::Main.expects(:start).with { |command| command == [ "secrets", "fetch", "...", "--inline" ] }.returns("{ \"foo\" : \"bar\" }")
substituted = Kamal::Secrets::Dotenv::InlineCommandSubstitution.call("SECRETS=$(kamal secrets fetch ...)", nil, overwrite: false)
assert_equal "SECRETS=\\{\\ \\\"foo\\\"\\ :\\ \\\"bar\\\"\\ \\}", substituted
end
end

View File

@@ -6,6 +6,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
end
test "fetch" do
stub_ticks.with("lpass --version 2> /dev/null")
stub_ticks.with("lpass status --color never").returns("Logged in as email@example.com.")
stub_ticks
@@ -51,7 +52,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
]
JSON
json = JSON.parse(run_command("fetch", "SECRET1", "FOLDER1/FSECRET1", "FOLDER1/FSECRET2"))
json = JSON.parse(shellunescape(run_command("fetch", "SECRET1", "FOLDER1/FSECRET1", "FOLDER1/FSECRET2")))
expected_json = {
"SECRET1"=>"secret1",
@@ -63,6 +64,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
end
test "fetch with from" do
stub_ticks.with("lpass --version 2> /dev/null")
stub_ticks.with("lpass status --color never").returns("Logged in as email@example.com.")
stub_ticks
@@ -96,7 +98,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
]
JSON
json = JSON.parse(run_command("fetch", "--from", "FOLDER1", "FSECRET1", "FSECRET2"))
json = JSON.parse(shellunescape(run_command("fetch", "--from", "FOLDER1", "FSECRET1", "FSECRET2")))
expected_json = {
"FOLDER1/FSECRET1"=>"fsecret1",
@@ -107,11 +109,13 @@ class LastPassAdapterTest < SecretAdapterTestCase
end
test "fetch with signin" do
stub_ticks.with("lpass --version 2> /dev/null")
stub_ticks_with("lpass status --color never", succeed: false).returns("Not logged in.")
stub_ticks_with("lpass login email@example.com", succeed: true).returns("")
stub_ticks.with("lpass show SECRET1 --json").returns(single_item_json)
json = JSON.parse(run_command("fetch", "SECRET1"))
json = JSON.parse(shellunescape(run_command("fetch", "SECRET1")))
expected_json = {
"SECRET1"=>"secret1"
@@ -120,6 +124,15 @@ class LastPassAdapterTest < SecretAdapterTestCase
assert_equal expected_json, json
end
test "fetch without CLI installed" do
stub_ticks_with("lpass --version 2> /dev/null", succeed: false)
error = assert_raises RuntimeError do
JSON.parse(shellunescape(run_command("fetch", "SECRET1", "FOLDER1/FSECRET1", "FOLDER1/FSECRET2")))
end
assert_equal "LastPass CLI is not installed", error.message
end
private
def run_command(*command)
stdouted do

View File

@@ -2,6 +2,7 @@ require "test_helper"
class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
test "fetch" do
stub_ticks.with("op --version 2> /dev/null")
stub_ticks.with("op account get --account myaccount 2> /dev/null")
stub_ticks
@@ -44,7 +45,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
]
JSON
json = JSON.parse(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1", "section/SECRET2", "section2/SECRET3"))
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1", "section/SECRET2", "section2/SECRET3")))
expected_json = {
"myvault/myitem/section/SECRET1"=>"VALUE1",
@@ -56,6 +57,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
end
test "fetch with multiple items" do
stub_ticks.with("op --version 2> /dev/null")
stub_ticks.with("op account get --account myaccount 2> /dev/null")
stub_ticks
@@ -103,7 +105,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
}
JSON
json = JSON.parse(run_command("fetch", "--from", "op://myvault", "myitem/section/SECRET1", "myitem/section/SECRET2", "myitem2/section2/SECRET3"))
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault", "myitem/section/SECRET1", "myitem/section/SECRET2", "myitem2/section2/SECRET3")))
expected_json = {
"myvault/myitem/section/SECRET1"=>"VALUE1",
@@ -115,6 +117,8 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
end
test "fetch with signin, no session" do
stub_ticks.with("op --version 2> /dev/null")
stub_ticks_with("op account get --account myaccount 2> /dev/null", succeed: false)
stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("")
@@ -122,7 +126,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
.with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1\" --format \"json\" --account \"myaccount\"")
.returns(single_item_json)
json = JSON.parse(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1"))
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1")))
expected_json = {
"myvault/myitem/section/SECRET1"=>"VALUE1"
@@ -132,6 +136,8 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
end
test "fetch with signin and session" do
stub_ticks.with("op --version 2> /dev/null")
stub_ticks_with("op account get --account myaccount 2> /dev/null", succeed: false)
stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("1234567890")
@@ -139,7 +145,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
.with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1\" --format \"json\" --account \"myaccount\" --session \"1234567890\"")
.returns(single_item_json)
json = JSON.parse(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1"))
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1")))
expected_json = {
"myvault/myitem/section/SECRET1"=>"VALUE1"
@@ -148,6 +154,15 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
assert_equal expected_json, json
end
test "fetch without CLI installed" do
stub_ticks_with("op --version 2> /dev/null", succeed: false)
error = assert_raises RuntimeError do
JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1", "section/SECRET2", "section2/SECRET3")))
end
assert_equal "1Password CLI is not installed", error.message
end
private
def run_command(*command)
stdouted do

View File

@@ -31,4 +31,18 @@ class SecretsTest < ActiveSupport::TestCase
assert_equal "JKL", Kamal::Secrets.new(destination: "nodest")["SECRET2"]
end
end
test "no secrets files" do
with_test_secrets do
error = assert_raises(Kamal::ConfigurationError) do
Kamal::Secrets.new["SECRET"]
end
assert_equal "Secret 'SECRET' not found, no secret files (.kamal/secrets-common, .kamal/secrets) provided", error.message
error = assert_raises(Kamal::ConfigurationError) do
Kamal::Secrets.new(destination: "dest")["SECRET"]
end
assert_equal "Secret 'SECRET' not found, no secret files (.kamal/secrets-common, .kamal/secrets.dest) provided", error.message
end
end
end

View File

@@ -86,4 +86,8 @@ class SecretAdapterTestCase < ActiveSupport::TestCase
stub_ticks.with { |c| c == command && (succeed ? `true` : `false`) }
Kamal::Secrets::Adapters::Base.any_instance.stubs(:`)
end
def shellunescape(string)
"\"#{string}\"".undump.gsub(/\\([{}])/, "\\1")
end
end

View File

@@ -2,8 +2,8 @@ require "test_helper"
class UtilsTest < ActiveSupport::TestCase
test "argumentize" do
assert_equal [ "--label", "foo=\"\\`bar\\`\"", "--label", "baz=\"qux\"", "--label", :quux ], \
Kamal::Utils.argumentize("--label", { foo: "`bar`", baz: "qux", quux: nil })
assert_equal [ "--label", "foo=\"\\`bar\\`\"", "--label", "baz=\"qux\"", "--label", :quux, "--label", "quuz=false" ], \
Kamal::Utils.argumentize("--label", { foo: "`bar`", baz: "qux", quux: nil, quuz: false })
end
test "argumentize with redacted" do