kamal-lint 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -49
- data/README.md +75 -130
- data/action.yml +8 -1
- data/lib/kamal/lint/check.rb +2 -13
- data/lib/kamal/lint/checks/image_registry_mismatch.rb +12 -3
- data/lib/kamal/lint/checks/kamal_parse_error.rb +30 -0
- data/lib/kamal/lint/checks/kamal_secrets_not_gitignored.rb +43 -13
- data/lib/kamal/lint/checks/missing_service_name.rb +1 -21
- data/lib/kamal/lint/checks/traefik_legacy_keys.rb +2 -35
- data/lib/kamal/lint/checks/unknown_keys.rb +96 -0
- data/lib/kamal/lint/cli.rb +2 -9
- data/lib/kamal/lint/finding.rb +2 -6
- data/lib/kamal/lint/formatters/github.rb +7 -7
- data/lib/kamal/lint/formatters/human.rb +45 -36
- data/lib/kamal/lint/formatters/json.rb +3 -11
- data/lib/kamal/lint/loader.rb +63 -70
- data/lib/kamal/lint/runner.rb +49 -49
- data/lib/kamal/lint/version.rb +1 -1
- data/lib/kamal/lint.rb +2 -0
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 10d72a6911e515326e690ea6c0c65c74742378edf3f0c0229d725b9f623f75db
|
|
4
|
+
data.tar.gz: 63de632c1ff0ac7a21e180dd38e638464de147b81f69ea48654ddca5e07e7b28
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae304091aa74c7cbd9f3f85e455029cd85cb0bdfa0f3c26d49764e347733d7363c15fef2eb1a01c70df96521e7e8e868a0f17f92c9402e2996a37d9f0dc3cc5c
|
|
7
|
+
data.tar.gz: 66568b9b1bcfb8a0287a5e083e97a24520343366a9f6229f428df0715a341da15db5081efc4e8013444baa74cdc8e6f301e03a928ce39db6c994d6eb7e10fa69
|
data/CHANGELOG.md
CHANGED
|
@@ -1,52 +1,5 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Release notes are auto-generated from merged commits and pull requests, and live with each tagged release:
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [Unreleased]
|
|
9
|
-
|
|
10
|
-
### Added
|
|
11
|
-
- `--include-kamal-errors` flag to opt-in to surfacing errors from Kamal's own loader (off by default; use `kamal config` for parse-time checks).
|
|
12
|
-
|
|
13
|
-
### Fixed
|
|
14
|
-
- `image-registry-mismatch` no longer false-positives when `registry.server` is set to Docker Hub (`docker.io`, `index.docker.io`, `registry.hub.docker.com`) and the image lacks an explicit registry prefix — Docker Hub resolves unprefixed images automatically.
|
|
15
|
-
|
|
16
|
-
### Internal
|
|
17
|
-
- Renamed `LICENSE` → `MIT-LICENSE` to match Kamal's convention.
|
|
18
|
-
- Added `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `.ruby-version`, `bin/release`.
|
|
19
|
-
- Added `Release` GitHub Actions workflow for tag-triggered publishing via RubyGems trusted publishing (OIDC).
|
|
20
|
-
- Added `actionlint` and `zizmor` workflow security audits in CI.
|
|
21
|
-
- Added `dependabot.yml` for weekly bundler + GitHub Actions updates.
|
|
22
|
-
- Added PR template and issue templates (bug report, new check proposal).
|
|
23
|
-
|
|
24
|
-
## [0.1.0] - 2026-05-11
|
|
25
|
-
|
|
26
|
-
### Added
|
|
27
|
-
- Initial release.
|
|
28
|
-
- 16 checks across reference integrity, coherence, and smells:
|
|
29
|
-
- `secret-not-declared`
|
|
30
|
-
- `accessory-role-undefined`
|
|
31
|
-
- `proxy-host-not-in-role`
|
|
32
|
-
- `image-registry-mismatch`
|
|
33
|
-
- `builder-registry-secret-undeclared`
|
|
34
|
-
- `ssl-without-host`
|
|
35
|
-
- `empty-web-role`
|
|
36
|
-
- `traefik-legacy-keys` (autofixable)
|
|
37
|
-
- `boot-limit-exceeds-hosts`
|
|
38
|
-
- `accessory-host-undefined`
|
|
39
|
-
- `missing-service-name` (autofixable)
|
|
40
|
-
- `kamal-secrets-not-gitignored` (autofixable)
|
|
41
|
-
- `secret-in-env-clear`
|
|
42
|
-
- `missing-proxy-healthcheck`
|
|
43
|
-
- `accessory-image-latest`
|
|
44
|
-
- `registry-without-explicit-server`
|
|
45
|
-
- Three output formatters: `human`, `json`, `github` (GitHub Actions annotations).
|
|
46
|
-
- `--fix` for the safe autofix subset.
|
|
47
|
-
- `-d/--destination` for linting destination override files.
|
|
48
|
-
- Auto-detection of installed Kamal version with version-gated checks.
|
|
49
|
-
- `kamal-lint` GitHub Action (composite) for one-line CI integration.
|
|
50
|
-
|
|
51
|
-
[Unreleased]: https://github.com/davafons/kamal-lint/compare/v0.1.0...HEAD
|
|
52
|
-
[0.1.0]: https://github.com/davafons/kamal-lint/releases/tag/v0.1.0
|
|
5
|
+
<https://github.com/davafons/kamal-lint/releases>
|
data/README.md
CHANGED
|
@@ -7,184 +7,129 @@
|
|
|
7
7
|
<a href="https://rubygems.org/gems/kamal-lint"><img src="https://img.shields.io/gem/dt/kamal-lint" alt="Downloads"></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
-
Static linter for [Kamal](https://kamal-deploy.org) `config/deploy.yml`. Catches
|
|
10
|
+
Static linter for [Kamal](https://kamal-deploy.org) `config/deploy.yml`. Catches missing secrets, role/registry mismatches, and proxy footguns that Kamal silently allows.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
config: config/deploy.yml
|
|
16
|
-
|
|
17
|
-
✖ error config/deploy.yml:9
|
|
18
|
-
env.secret references `RAILS_MASTER_KEY` but it isn't declared in .kamal/secrets
|
|
19
|
-
[secret-not-declared]
|
|
20
|
-
|
|
21
|
-
⚠ warning config/deploy.yml:18 (autofixable)
|
|
22
|
-
`traefik:` block is Kamal 1.x legacy and is ignored in Kamal 2+; use `proxy:` instead
|
|
23
|
-
[traefik-legacy-keys]
|
|
24
|
-
|
|
25
|
-
Summary: 1 error, 1 warning, 1 autofixable
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Why
|
|
29
|
-
|
|
30
|
-
Kamal's own loader only checks "does this YAML parse into my schema?" It happily accepts a config that references secrets you never declared, points at registries you don't use, names roles that don't exist, or still uses Kamal 1.x `traefik:` keys. By the time you find out, you've already shipped a broken deploy.
|
|
31
|
-
|
|
32
|
-
`kamal-lint` runs in CI (or pre-commit) and catches these before they hit production.
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="docs/preview.svg" alt="kamal-lint output sample" width="760">
|
|
14
|
+
</p>
|
|
33
15
|
|
|
34
16
|
## Install
|
|
35
17
|
|
|
36
|
-
Add to your project's `Gemfile`:
|
|
37
|
-
|
|
38
18
|
```ruby
|
|
19
|
+
# Gemfile
|
|
39
20
|
group :development, :test do
|
|
40
21
|
gem "kamal-lint", require: false
|
|
41
22
|
end
|
|
42
23
|
```
|
|
43
24
|
|
|
44
|
-
Then:
|
|
45
|
-
|
|
46
25
|
```bash
|
|
47
|
-
bundle install
|
|
48
26
|
bundle exec kamal-lint
|
|
49
27
|
```
|
|
50
28
|
|
|
51
|
-
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
**Default: lint base + every destination override at once.**
|
|
52
32
|
|
|
53
33
|
```bash
|
|
54
|
-
|
|
55
|
-
kamal-lint
|
|
34
|
+
bundle exec kamal-lint
|
|
56
35
|
```
|
|
57
36
|
|
|
58
|
-
|
|
37
|
+
Auto-discovers `config/deploy.*.yml` files next to your base `config/deploy.yml` and lints each (base alone, then each destination merged onto base). Output groups findings by destination — bugs that live in a `deploy.production.yml` override show up *only* under `[production]`:
|
|
59
38
|
|
|
60
39
|
```
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-c, --config-file PATH Path to deploy.yml (default: config/deploy.yml)
|
|
64
|
-
-d, --destination NAME Lint with destination override applied
|
|
65
|
-
(e.g. -d production → config/deploy.production.yml)
|
|
66
|
-
-f, --format FORMAT human (default) | json | github
|
|
67
|
-
--fail-on LEVEL error | warning (default) | info
|
|
68
|
-
--fix Apply safe autofixes in-place
|
|
69
|
-
--kamal-version VER Override detected Kamal version
|
|
70
|
-
--include-kamal-errors Also surface errors from Kamal's own loader
|
|
71
|
-
(off by default; use `kamal config` for that)
|
|
72
|
-
--no-color Disable colored output
|
|
73
|
-
--list-checks Print all registered checks
|
|
74
|
-
--version Print kamal-lint version
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Exit codes
|
|
40
|
+
[base] config/deploy.yml
|
|
41
|
+
✓ No issues found.
|
|
78
42
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
| `1` | Findings present at or above `--fail-on` severity |
|
|
83
|
-
| `2` | Config file not found / unreadable |
|
|
84
|
-
|
|
85
|
-
## Checks
|
|
43
|
+
[production] config/deploy.production.yml
|
|
44
|
+
⚠ warning ...
|
|
45
|
+
`traefik:` block is Kamal 1.x legacy ...
|
|
86
46
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
| `secret-not-declared` | error | | `env.secret` references a key absent from `.kamal/secrets` |
|
|
90
|
-
| `accessory-role-undefined` | error | | accessory `roles:` lists a role not in `servers` |
|
|
91
|
-
| `role-hosts-empty` | error | | a role under `servers:` has no hosts (silent no-op deploy) |
|
|
92
|
-
| `image-registry-mismatch` | error | | `image:` registry prefix ≠ `builder.registry.server` |
|
|
93
|
-
| `builder-registry-secret-undeclared` | error | | registry username/password references undeclared secret |
|
|
94
|
-
| `ssl-without-host` | error | | `proxy.ssl: true` without `host:` (Let's Encrypt won't work) |
|
|
95
|
-
| `empty-web-role` | error | | `servers:` empty or has no hosts in any role |
|
|
96
|
-
| `accessory-placement-missing` | error | | accessory has no `host`/`hosts`/`roles` declared |
|
|
97
|
-
| `missing-service-name` | error | ✓ | `service:` not set |
|
|
98
|
-
| `traefik-legacy-keys` | warning | ✓ | Kamal 1.x `traefik:` block (silently ignored by Kamal 2+) |
|
|
99
|
-
| `boot-limit-exceeds-hosts` | warning | | `boot.limit` greater than the number of hosts |
|
|
100
|
-
| `kamal-secrets-not-gitignored` | warning | ✓ | `.kamal/secrets` exists but isn't gitignored |
|
|
101
|
-
| `secret-in-env-clear` | warning | | `env.clear` value looks like a secret (`*_KEY`/`*_TOKEN`/`*_SECRET`/etc.) |
|
|
102
|
-
| `missing-proxy-healthcheck` | warning | | `proxy:` block with no `healthcheck:` (no zero-downtime guarantee) |
|
|
103
|
-
| `accessory-image-latest` | warning | | accessory pinned to `:latest` or unpinned |
|
|
104
|
-
| `registry-without-explicit-server` | warning | | `registry.server` missing; image silently defaults to Docker Hub |
|
|
105
|
-
|
|
106
|
-
Run `kamal-lint list-checks` for the same table in your terminal, including the Kamal version range each check applies to.
|
|
107
|
-
|
|
108
|
-
## Autofix
|
|
109
|
-
|
|
110
|
-
`--fix` rewrites your config in-place for the safe subset:
|
|
111
|
-
|
|
112
|
-
- `traefik-legacy-keys` → translates `traefik:` to a `proxy:` block (host, ssl, app_port)
|
|
113
|
-
- `missing-service-name` → infers `service:` from the project directory name
|
|
114
|
-
- `kamal-secrets-not-gitignored` → appends `.kamal/secrets` to `.gitignore`
|
|
47
|
+
[staging] config/deploy.staging.yml
|
|
48
|
+
✓ No issues found.
|
|
115
49
|
|
|
116
|
-
|
|
117
|
-
bundle exec kamal-lint --fix
|
|
50
|
+
Summary: 1 warning across 3 configs
|
|
118
51
|
```
|
|
119
52
|
|
|
120
|
-
|
|
53
|
+
Exits `1` if any findings are at or above `--fail-on` (default: `warning`), `0` otherwise.
|
|
121
54
|
|
|
122
|
-
|
|
55
|
+
**Narrow to a single destination:**
|
|
123
56
|
|
|
124
57
|
```bash
|
|
125
58
|
bundle exec kamal-lint -d production
|
|
126
59
|
```
|
|
127
60
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
## CI / GitHub Actions
|
|
61
|
+
Useful in CI matrix jobs or when debugging one destination. Skips auto-discovery; only lints `deploy.yml + deploy.production.yml`.
|
|
131
62
|
|
|
132
|
-
|
|
63
|
+
**Other knobs:**
|
|
133
64
|
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
runs-on: ubuntu-latest
|
|
140
|
-
steps:
|
|
141
|
-
- uses: actions/checkout@v4
|
|
142
|
-
- uses: ruby/setup-ruby@v1
|
|
143
|
-
with:
|
|
144
|
-
bundler-cache: true
|
|
145
|
-
- run: bundle exec kamal-lint --format=github
|
|
65
|
+
```bash
|
|
66
|
+
bundle exec kamal-lint -c infra/deploy.yml # non-default config path
|
|
67
|
+
bundle exec kamal-lint list-checks # show every registered check
|
|
68
|
+
bundle exec kamal-lint --format=json # machine-readable output
|
|
69
|
+
bundle exec kamal-lint --include-kamal-errors # also surface Kamal's loader errors
|
|
146
70
|
```
|
|
147
71
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
A composite Action wrapper is also published in this repo at `action.yml`:
|
|
72
|
+
**In CI:**
|
|
151
73
|
|
|
152
74
|
```yaml
|
|
153
|
-
- uses: davafons/kamal-lint@v0
|
|
75
|
+
- uses: davafons/kamal-lint@v0.1.0
|
|
154
76
|
with:
|
|
155
|
-
config-file: config/deploy.yml
|
|
156
|
-
destination: production
|
|
157
77
|
fail-on: warning
|
|
158
78
|
```
|
|
159
79
|
|
|
160
|
-
|
|
80
|
+
`--format=github` is set automatically so findings show as inline annotations in the PR view. By default the action lints all destinations; set `destination: production` to narrow.
|
|
161
81
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
| kamal-lint | supported kamal |
|
|
165
|
-
|---|---|
|
|
166
|
-
| `0.1.x` | `>= 2.0`, `< 3.0` |
|
|
82
|
+
## Checks
|
|
167
83
|
|
|
168
|
-
|
|
84
|
+
| ID | Severity | What it catches |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `secret-not-declared` | error | `env.secret` (top-level or per-accessory) references a key that isn't declared in `.kamal/secrets`. Kamal would fail at deploy time. |
|
|
87
|
+
| `accessory-role-undefined` | error | An accessory's `roles:` lists a role name that isn't defined under `servers:`. The accessory won't deploy to anything. |
|
|
88
|
+
| `role-hosts-empty` | error | A role under `servers:` has no hosts. Deploys to that role silently no-op. |
|
|
89
|
+
| `image-registry-mismatch` | error | `image:` doesn't include the prefix of `registry.server`. Kamal would push/pull from the wrong registry. (Docker Hub is exempt — unprefixed images resolve there automatically.) |
|
|
90
|
+
| `builder-registry-secret-undeclared` | error | `registry.username` or `registry.password` references a secret name that isn't in `.kamal/secrets`. |
|
|
91
|
+
| `ssl-without-host` | error | `proxy.ssl: true` without a `host:` (or `hosts:`). Let's Encrypt provisioning has nothing to issue against. |
|
|
92
|
+
| `empty-web-role` | error | `servers:` is empty or every role has no hosts. Nothing would be deployed. |
|
|
93
|
+
| `accessory-placement-missing` | error | An accessory has none of `host`, `hosts`, or `roles` declared, so Kamal has no idea where to put it. |
|
|
94
|
+
| `missing-service-name` | error | `service:` is not set. Kamal can't name the container. |
|
|
95
|
+
| `traefik-legacy-keys` | warning | A `traefik:` block is still present. Kamal 2+ uses `proxy:` and silently ignores the old block. |
|
|
96
|
+
| `boot-limit-exceeds-hosts` | warning | `boot.limit` is greater than the total number of hosts, so the rolling-deploy limit has no effect. |
|
|
97
|
+
| `kamal-secrets-not-gitignored` | warning | `.kamal/secrets` exists in the repo but isn't matched by `.gitignore`. Real credentials are one `git add .` away from a commit. |
|
|
98
|
+
| `secret-in-env-clear` | warning | A key in `env.clear` looks like a secret (`*_KEY`, `*_TOKEN`, `*_SECRET`, `*PASSWORD*`). Move it to `env.secret` + `.kamal/secrets`. |
|
|
99
|
+
| `missing-proxy-healthcheck` | warning | The `proxy:` block has no `healthcheck:`. Kamal-proxy can't verify a new release before cutting traffic — zero-downtime deploys may fail. |
|
|
100
|
+
| `accessory-image-latest` | warning | An accessory's `image:` is pinned to `:latest` (or has no tag). Updates can change unexpectedly between deploys. |
|
|
101
|
+
| `registry-without-explicit-server` | warning | `registry` is set but `registry.server` isn't. Kamal silently defaults to Docker Hub. |
|
|
102
|
+
| `kamal-parse-error` | error | *Opt-in.* Surfaces errors from Kamal's own loader. Enable with `--include-kamal-errors`. Useful in CI as a complement to `kamal config`. |
|
|
103
|
+
|
|
104
|
+
Reasoning behind each finding is also in the message text — paste a finding into search and you'll usually land on the relevant Kamal doc.
|
|
105
|
+
|
|
106
|
+
## Flags
|
|
169
107
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
108
|
+
```
|
|
109
|
+
-c, --config-file PATH config/deploy.yml
|
|
110
|
+
-d, --destination NAME lint deploy.<name>.yml merged onto base
|
|
111
|
+
-f, --format FORMAT human | json | github
|
|
112
|
+
--fail-on LEVEL error | warning | info
|
|
113
|
+
--kamal-version VER override detected Kamal version
|
|
114
|
+
--include-kamal-errors also surface Kamal's loader errors
|
|
176
115
|
```
|
|
177
116
|
|
|
178
|
-
|
|
117
|
+
Exit codes: `0` clean · `1` findings at/above `--fail-on` · `2` config missing.
|
|
179
118
|
|
|
180
|
-
|
|
181
|
-
BUNDLE_ONLY=rubocop bundle exec rubocop
|
|
182
|
-
```
|
|
119
|
+
## Kamal versions
|
|
183
120
|
|
|
184
|
-
|
|
121
|
+
| kamal-lint | kamal | tested against |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `0.1.x` | `>= 2.0`, `< 3.0` | latest 2.x |
|
|
185
124
|
|
|
186
|
-
|
|
125
|
+
`kamal-lint` reuses your installed Kamal's loader, so it auto-tracks whatever's in your `Gemfile.lock`. Older 2.x versions likely work but aren't covered by CI — if you hit a bug on an older Kamal, please [open an issue](https://github.com/davafons/kamal-lint/issues). Override the detected version with `--kamal-version 2.5.0` for matrix runs.
|
|
187
126
|
|
|
188
|
-
##
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
bin/setup # install
|
|
131
|
+
bin/test # run tests
|
|
132
|
+
bin/console # IRB with kamal-lint loaded
|
|
133
|
+
```
|
|
189
134
|
|
|
190
|
-
|
|
135
|
+
Contributions: [CONTRIBUTING.md](./CONTRIBUTING.md) · Security: [SECURITY.md](./SECURITY.md) · License: [MIT](./MIT-LICENSE).
|
data/action.yml
CHANGED
|
@@ -58,4 +58,11 @@ runs:
|
|
|
58
58
|
if [ "$KL_FIX" = "true" ]; then
|
|
59
59
|
ARGS+=(--fix)
|
|
60
60
|
fi
|
|
61
|
-
|
|
61
|
+
|
|
62
|
+
# Prefer bundler if there's a Gemfile referencing kamal-lint;
|
|
63
|
+
# fall back to a globally-installed gem otherwise.
|
|
64
|
+
if [ -f Gemfile ] && bundle list 2>/dev/null | grep -q '^ \* kamal-lint '; then
|
|
65
|
+
bundle exec kamal-lint "${ARGS[@]}"
|
|
66
|
+
else
|
|
67
|
+
kamal-lint "${ARGS[@]}"
|
|
68
|
+
fi
|
data/lib/kamal/lint/check.rb
CHANGED
|
@@ -34,12 +34,6 @@ module Kamal
|
|
|
34
34
|
@until_version
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
# Mark this check as autofix-capable in the registry listing.
|
|
38
|
-
def autofixable(value = nil)
|
|
39
|
-
@autofixable = value unless value.nil?
|
|
40
|
-
@autofixable || false
|
|
41
|
-
end
|
|
42
|
-
|
|
43
37
|
def applies_to?(kamal_version)
|
|
44
38
|
return true if kamal_version.nil?
|
|
45
39
|
|
|
@@ -66,25 +60,20 @@ module Kamal
|
|
|
66
60
|
|
|
67
61
|
private
|
|
68
62
|
|
|
69
|
-
def finding(message:, line: nil, column: nil
|
|
63
|
+
def finding(message:, line: nil, column: nil)
|
|
70
64
|
Finding.new(
|
|
71
65
|
check_id: self.class.id,
|
|
72
66
|
severity: self.class.severity,
|
|
73
67
|
message: message,
|
|
74
68
|
file: context.file_for_finding,
|
|
75
69
|
line: line,
|
|
76
|
-
column: column
|
|
77
|
-
autofix: autofix
|
|
70
|
+
column: column
|
|
78
71
|
)
|
|
79
72
|
end
|
|
80
73
|
|
|
81
74
|
def parsed
|
|
82
75
|
context.parsed
|
|
83
76
|
end
|
|
84
|
-
|
|
85
|
-
def secrets
|
|
86
|
-
context.secrets
|
|
87
|
-
end
|
|
88
77
|
end
|
|
89
78
|
end
|
|
90
79
|
end
|
|
@@ -14,6 +14,9 @@ module Kamal
|
|
|
14
14
|
# configured registry is Docker Hub under any of its canonical names.
|
|
15
15
|
DOCKER_HUB_HOSTS = %w[docker.io index.docker.io registry.hub.docker.com].freeze
|
|
16
16
|
|
|
17
|
+
# A leading path segment is treated as a registry host when it contains
|
|
18
|
+
# a `.` or `:` (e.g. `ghcr.io`, `localhost:5000`) — Kamal sees that as
|
|
19
|
+
# a host and won't add the configured server in front of it.
|
|
17
20
|
def call
|
|
18
21
|
image = parsed["image"]
|
|
19
22
|
registry = parsed["registry"] || parsed.dig("builder", "registry") || {}
|
|
@@ -23,11 +26,17 @@ module Kamal
|
|
|
23
26
|
normalized_server = server.sub(%r{/+\z}, "")
|
|
24
27
|
return [] if DOCKER_HUB_HOSTS.include?(normalized_server)
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
first_segment = image.split("/", 2).first.to_s
|
|
30
|
+
looks_like_host = first_segment.include?(".") || first_segment.include?(":")
|
|
31
|
+
|
|
32
|
+
# Unprefixed `org/repo` is the canonical Kamal style — the registry
|
|
33
|
+
# server is prepended automatically. Only flag when the image already
|
|
34
|
+
# carries a registry host that disagrees with `registry.server`.
|
|
35
|
+
return [] unless looks_like_host
|
|
36
|
+
return [] if image.start_with?("#{normalized_server}/")
|
|
28
37
|
|
|
29
38
|
[ finding(
|
|
30
|
-
message: "image `#{image}`
|
|
39
|
+
message: "image `#{image}` is prefixed with a registry host that disagrees with `registry.server: #{server}`; Kamal will push to the wrong registry",
|
|
31
40
|
line: context.line_for([ "image" ])
|
|
32
41
|
) ]
|
|
33
42
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kamal
|
|
4
|
+
module Lint
|
|
5
|
+
module Checks
|
|
6
|
+
# Opt-in: surfaces errors from Kamal's own loader as findings.
|
|
7
|
+
# Off by default because Kamal's parse-level validation is what
|
|
8
|
+
# `kamal config` is for — the linter's value-add is the cross-section
|
|
9
|
+
# checks. Enabled with `--include-kamal-errors`.
|
|
10
|
+
class KamalParseError < Check
|
|
11
|
+
id "kamal-parse-error"
|
|
12
|
+
severity :error
|
|
13
|
+
since "2.0.0"
|
|
14
|
+
title "Kamal's own loader rejected this config"
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
return [] unless context.include_kamal_errors
|
|
18
|
+
return [] unless context.kamal_load_error
|
|
19
|
+
|
|
20
|
+
[ finding(
|
|
21
|
+
message: "kamal could not load this config: #{context.kamal_load_error.message}",
|
|
22
|
+
line: 1
|
|
23
|
+
) ]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Lint.registry.register(KamalParseError)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -7,23 +7,27 @@ module Kamal
|
|
|
7
7
|
id "kamal-secrets-not-gitignored"
|
|
8
8
|
severity :warning
|
|
9
9
|
since "2.0.0"
|
|
10
|
-
autofixable true
|
|
11
10
|
title ".kamal/secrets is not in .gitignore"
|
|
12
11
|
|
|
13
12
|
# We only flag when:
|
|
14
13
|
# - a .kamal/secrets file exists (so there's something to leak), AND
|
|
15
|
-
# - it is NOT covered by .gitignore (or .gitignore is missing)
|
|
14
|
+
# - it is NOT covered by .gitignore (or .gitignore is missing), AND
|
|
15
|
+
# - it contains at least one raw literal value (not just shell
|
|
16
|
+
# substitutions like `$(cmd)`, `${VAR}`, or `$VAR`). Files that
|
|
17
|
+
# only reference secrets via substitution are safe to commit.
|
|
16
18
|
def call
|
|
17
19
|
return [] unless File.exist?(context.secrets_path)
|
|
18
20
|
return [] if gitignored?
|
|
21
|
+
return [] unless contains_literal_secret?
|
|
19
22
|
|
|
20
23
|
[ finding(
|
|
21
|
-
message: ".kamal/secrets exists but isn't ignored by .gitignore;
|
|
22
|
-
line: 1
|
|
23
|
-
autofix: method(:apply_fix)
|
|
24
|
+
message: ".kamal/secrets exists but isn't ignored by .gitignore; add `.kamal/secrets` to .gitignore",
|
|
25
|
+
line: 1
|
|
24
26
|
) ]
|
|
25
27
|
end
|
|
26
28
|
|
|
29
|
+
private
|
|
30
|
+
|
|
27
31
|
def gitignored?
|
|
28
32
|
return false unless File.exist?(context.gitignore_path)
|
|
29
33
|
|
|
@@ -39,14 +43,40 @@ module Kamal
|
|
|
39
43
|
end
|
|
40
44
|
end
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
# A value is "safe" when it sources its content from outside the file:
|
|
47
|
+
# FOO=$(cmd ...) # command substitution
|
|
48
|
+
# FOO=${VAR:-fallback} # parameter expansion
|
|
49
|
+
# FOO=$VAR # plain variable
|
|
50
|
+
# FOO= # empty
|
|
51
|
+
# Anything else (literal token, quoted string, fragment) is treated as
|
|
52
|
+
# a raw secret and triggers the finding.
|
|
53
|
+
SAFE_VALUE = /\A
|
|
54
|
+
\s* # leading whitespace
|
|
55
|
+
(?:
|
|
56
|
+
\z # empty
|
|
57
|
+
|
|
|
58
|
+
\$\(.*\) # $( ... )
|
|
59
|
+
|
|
|
60
|
+
\$\{.*\} # ${ ... }
|
|
61
|
+
|
|
|
62
|
+
\$[A-Za-z_][A-Za-z0-9_]* # $VAR
|
|
63
|
+
)
|
|
64
|
+
\s*\z
|
|
65
|
+
/x
|
|
66
|
+
|
|
67
|
+
def contains_literal_secret?
|
|
68
|
+
return false unless File.exist?(context.secrets_path)
|
|
69
|
+
|
|
70
|
+
File.foreach(context.secrets_path).any? do |raw|
|
|
71
|
+
line = raw.strip
|
|
72
|
+
next false if line.empty? || line.start_with?("#")
|
|
73
|
+
|
|
74
|
+
line = line.sub(/\Aexport\s+/, "")
|
|
75
|
+
_name, eq, value = line.partition("=")
|
|
76
|
+
next false if eq.empty?
|
|
77
|
+
|
|
78
|
+
!SAFE_VALUE.match?(value)
|
|
79
|
+
end
|
|
50
80
|
end
|
|
51
81
|
end
|
|
52
82
|
|
|
@@ -7,7 +7,6 @@ module Kamal
|
|
|
7
7
|
id "missing-service-name"
|
|
8
8
|
severity :error
|
|
9
9
|
since "2.0.0"
|
|
10
|
-
autofixable true
|
|
11
10
|
title "`service:` is required and missing"
|
|
12
11
|
|
|
13
12
|
def call
|
|
@@ -16,28 +15,9 @@ module Kamal
|
|
|
16
15
|
|
|
17
16
|
[ finding(
|
|
18
17
|
message: "`service:` is required; without it Kamal can't name the deployed container",
|
|
19
|
-
line: 1
|
|
20
|
-
autofix: method(:apply_fix)
|
|
18
|
+
line: 1
|
|
21
19
|
) ]
|
|
22
20
|
end
|
|
23
|
-
|
|
24
|
-
def apply_fix(ctx)
|
|
25
|
-
file = ctx.file_for_finding
|
|
26
|
-
text = File.read(file)
|
|
27
|
-
parsed = YAML.safe_load(text, aliases: true) || {}
|
|
28
|
-
return false if parsed["service"].is_a?(String) && !parsed["service"].empty?
|
|
29
|
-
|
|
30
|
-
name = File.basename(ctx.working_dir).gsub(/[^A-Za-z0-9_-]/, "-")
|
|
31
|
-
return false if name.empty?
|
|
32
|
-
|
|
33
|
-
# Parse-and-dump so the fix composes safely with other autofixes
|
|
34
|
-
# that may also rewrite the file. The trade-off is that comments
|
|
35
|
-
# in the original YAML are lost — documented in the README.
|
|
36
|
-
File.write(file, YAML.dump({ "service" => name }.merge(parsed)))
|
|
37
|
-
true
|
|
38
|
-
rescue
|
|
39
|
-
false
|
|
40
|
-
end
|
|
41
21
|
end
|
|
42
22
|
|
|
43
23
|
Lint.registry.register(MissingServiceName)
|
|
@@ -7,49 +7,16 @@ module Kamal
|
|
|
7
7
|
id "traefik-legacy-keys"
|
|
8
8
|
severity :warning
|
|
9
9
|
since "2.0.0"
|
|
10
|
-
autofixable true
|
|
11
10
|
title "Kamal 1.x `traefik:` keys present (use `proxy:` in Kamal 2+)"
|
|
12
11
|
|
|
13
12
|
def call
|
|
14
13
|
return [] unless parsed.key?("traefik")
|
|
15
14
|
|
|
16
15
|
[ finding(
|
|
17
|
-
message: "`traefik:` block is Kamal 1.x legacy and is ignored in Kamal 2+;
|
|
18
|
-
line: context.line_for([ "traefik" ])
|
|
19
|
-
autofix: method(:apply_fix)
|
|
16
|
+
message: "`traefik:` block is Kamal 1.x legacy and is ignored in Kamal 2+; replace it with a `proxy:` block",
|
|
17
|
+
line: context.line_for([ "traefik" ])
|
|
20
18
|
) ]
|
|
21
19
|
end
|
|
22
|
-
|
|
23
|
-
def apply_fix(ctx)
|
|
24
|
-
file = ctx.file_for_finding
|
|
25
|
-
text = File.read(file)
|
|
26
|
-
parsed = YAML.safe_load(text, aliases: true) || {}
|
|
27
|
-
return false unless parsed.is_a?(Hash) && parsed.key?("traefik")
|
|
28
|
-
|
|
29
|
-
traefik = parsed.delete("traefik") || {}
|
|
30
|
-
proxy = parsed["proxy"] || {}
|
|
31
|
-
|
|
32
|
-
# Conservative translation:
|
|
33
|
-
# - traefik.host → proxy.host
|
|
34
|
-
# - traefik.ssl_redirect → proxy.ssl: true (Kamal 2 handles SSL via proxy.ssl)
|
|
35
|
-
# - traefik.args.entryPoints.address: ":<port>" → proxy.app_port: <port>
|
|
36
|
-
if (host = traefik["host"]) && !proxy["host"]
|
|
37
|
-
proxy["host"] = host
|
|
38
|
-
end
|
|
39
|
-
if traefik["ssl_redirect"] == true || traefik.dig("args", "entrypoints.websecure.address")
|
|
40
|
-
proxy["ssl"] = true unless proxy.key?("ssl")
|
|
41
|
-
end
|
|
42
|
-
if (addr = traefik.dig("args", "entrypoints.web.address")) && addr.is_a?(String)
|
|
43
|
-
port = addr.scan(/\d+/).first
|
|
44
|
-
proxy["app_port"] = port.to_i if port && !proxy.key?("app_port")
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
parsed["proxy"] = proxy unless proxy.empty?
|
|
48
|
-
File.write(file, YAML.dump(parsed))
|
|
49
|
-
true
|
|
50
|
-
rescue => _e
|
|
51
|
-
false
|
|
52
|
-
end
|
|
53
20
|
end
|
|
54
21
|
|
|
55
22
|
Lint.registry.register(TraefikLegacyKeys)
|