docktor_rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +70 -0
- data/Rakefile +17 -0
- data/exe/docktor_rails +6 -0
- data/lib/docktor_rails/checks/base_check.rb +35 -0
- data/lib/docktor_rails/checks/compose_build_paths.rb +66 -0
- data/lib/docktor_rails/checks/compose_entrypoint_exec.rb +82 -0
- data/lib/docktor_rails/checks/compose_entrypoint_sanity.rb +87 -0
- data/lib/docktor_rails/checks/compose_env_file_present.rb +47 -0
- data/lib/docktor_rails/checks/compose_file_present.rb +32 -0
- data/lib/docktor_rails/checks/compose_healthchecks.rb +37 -0
- data/lib/docktor_rails/checks/compose_platform_risk.rb +53 -0
- data/lib/docktor_rails/checks/rails_master_key_required.rb +62 -0
- data/lib/docktor_rails/checks/shell_scripts_crlf.rb +53 -0
- data/lib/docktor_rails/cli.rb +91 -0
- data/lib/docktor_rails/compose.rb +24 -0
- data/lib/docktor_rails/dotenv.rb +28 -0
- data/lib/docktor_rails/guidance.rb +48 -0
- data/lib/docktor_rails/platform.rb +63 -0
- data/lib/docktor_rails/preflight/runner.rb +67 -0
- data/lib/docktor_rails/reporters/json_reporter.rb +43 -0
- data/lib/docktor_rails/reporters/text_reporter.rb +78 -0
- data/lib/docktor_rails/result.rb +29 -0
- data/lib/docktor_rails/version.rb +5 -0
- data/lib/docktor_rails.rb +10 -0
- data/sig/docktor_rails.rbs +4 -0
- metadata +134 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3e9a0546a0a1bfb8849cd3c5f8cd2767f5a49ac7c3e8077fb1be9f964e494fc9
|
|
4
|
+
data.tar.gz: '0099dfd0a1ad6f689a02c321a75e3e6e95a619abd17186becc0630ddae2c5ce4'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 69695f70efc8e1aee5a28ee5696e14244430c3fc251ff414097d400508bc2f1e940f526b9ed7d14aa82d8f97d9d3ceae185dbf1d6960cb9aa05c891ec2c9557a
|
|
7
|
+
data.tar.gz: f8c1911b3b59b0b3bec51d9033f3bb6d1f6264fb9cca7deb2c013ed34e07d4ea15b567b5c78c780dd62335f30168f58d3f18f1f5f47dfe113daea2c455c3778a
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-06-11
|
|
4
|
+
|
|
5
|
+
- Initial release
|
|
6
|
+
- Added preflight checks for Compose file presence, build path validity, env_file presence, entrypoint sanity/exec, CRLF in shell scripts, db/redis healthcheck warnings, platform pin risk, and Rails master key availability.
|
|
7
|
+
- Added `--root` for scanning a target directory.
|
|
8
|
+
- Added `--format json` output with context (host OS/arch, root).
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FAllS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# DocktorRails
|
|
2
|
+
DocktorRails is a tiny preflight checker for Rails Docker/Compose dev environments.
|
|
3
|
+
It catches common cross-machine issues early (CRLF shell scripts, missing compose-referenced files, build path mistakes, platform pinning risk) and provides CI-friendly JSON output.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
Add it to your Rails app:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
bundle add docktor_rails
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
Run in your project root:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle exec docktor_rails diagnose
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Scan another directory (useful in mono-repos):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bundle exec docktor_rails diagnose --root ../some_app
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
CI/automation (JSON):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle exec docktor_rails diagnose --format json --output preflight.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
- `--verbose`: show passing checks
|
|
33
|
+
- `--quiet`: only show errors/warnings + summary (good for CI logs)
|
|
34
|
+
|
|
35
|
+
## Exit codes
|
|
36
|
+
- `0`: no failing checks
|
|
37
|
+
- `1`: one or more failing checks
|
|
38
|
+
- `2`: tool/internal error
|
|
39
|
+
|
|
40
|
+
## GitHub Actions example
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
name: Preflight
|
|
44
|
+
on:
|
|
45
|
+
pull_request:
|
|
46
|
+
push:
|
|
47
|
+
branches: [ main ]
|
|
48
|
+
|
|
49
|
+
jobs:
|
|
50
|
+
preflight:
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
- uses: ruby/setup-ruby@v1
|
|
55
|
+
with:
|
|
56
|
+
ruby-version: "3.3"
|
|
57
|
+
bundler-cache: true
|
|
58
|
+
- run: bundle exec docktor_rails diagnose --format json --output preflight.json
|
|
59
|
+
- if: always()
|
|
60
|
+
uses: actions/upload-artifact@v4
|
|
61
|
+
with:
|
|
62
|
+
name: docktor-rails-preflight
|
|
63
|
+
path: preflight.json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Contributing
|
|
67
|
+
Issues and pull requests are welcome.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
task default: %i[spec]
|
|
9
|
+
|
|
10
|
+
# Optional lint task (not required for build/publish)
|
|
11
|
+
begin
|
|
12
|
+
require "rubocop/rake_task"
|
|
13
|
+
RuboCop::RakeTask.new
|
|
14
|
+
task lint: :rubocop
|
|
15
|
+
rescue LoadError
|
|
16
|
+
# rubocop not installed
|
|
17
|
+
end
|
data/exe/docktor_rails
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../result"
|
|
4
|
+
|
|
5
|
+
module DocktorRails
|
|
6
|
+
module Checks
|
|
7
|
+
class BaseCheck
|
|
8
|
+
def id
|
|
9
|
+
raise NotImplementedError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def name
|
|
13
|
+
id
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run(_ctx)
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def pass(message, files: nil, hint: nil)
|
|
23
|
+
Result.new(id: id, status: :pass, message: message, files: files, hint: hint)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def warn(message, files: nil, hint: nil)
|
|
27
|
+
Result.new(id: id, status: :warn, message: message, files: files, hint: hint)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def fail(message, files: nil, hint: nil)
|
|
31
|
+
Result.new(id: id, status: :fail, message: message, files: files, hint: hint)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_check"
|
|
4
|
+
require_relative "../compose"
|
|
5
|
+
|
|
6
|
+
module DocktorRails
|
|
7
|
+
module Checks
|
|
8
|
+
class ComposeBuildPaths < BaseCheck
|
|
9
|
+
def id
|
|
10
|
+
"compose.build_paths"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(ctx)
|
|
14
|
+
root = ctx.fetch(:root)
|
|
15
|
+
compose_path = Compose.find_file(root)
|
|
16
|
+
return pass("No compose file found (skipping build path check)") unless compose_path
|
|
17
|
+
|
|
18
|
+
doc = Compose.load_file(compose_path)
|
|
19
|
+
services = Compose.services(doc)
|
|
20
|
+
|
|
21
|
+
problems = []
|
|
22
|
+
|
|
23
|
+
services.each do |svc_name, svc|
|
|
24
|
+
next unless svc.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
build = svc["build"]
|
|
27
|
+
next if build.nil?
|
|
28
|
+
|
|
29
|
+
context, dockerfile = extract_build(build)
|
|
30
|
+
context ||= "."
|
|
31
|
+
|
|
32
|
+
context_abs = File.expand_path(context, root)
|
|
33
|
+
unless Dir.exist?(context_abs)
|
|
34
|
+
problems << "#{svc_name}: build.context missing (#{context})"
|
|
35
|
+
next
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
dockerfile ||= "Dockerfile"
|
|
39
|
+
dockerfile_abs = File.expand_path(dockerfile, context_abs)
|
|
40
|
+
unless File.file?(dockerfile_abs)
|
|
41
|
+
problems << "#{svc_name}: build.dockerfile missing (#{File.join(context, dockerfile)})"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if problems.empty?
|
|
46
|
+
pass("Compose build paths look valid")
|
|
47
|
+
else
|
|
48
|
+
fail("Compose build path issues detected", files: problems)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def extract_build(build)
|
|
55
|
+
case build
|
|
56
|
+
when String
|
|
57
|
+
[build, nil]
|
|
58
|
+
when Hash
|
|
59
|
+
[build["context"], build["dockerfile"]]
|
|
60
|
+
else
|
|
61
|
+
[nil, nil]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_check"
|
|
4
|
+
require_relative "../compose"
|
|
5
|
+
|
|
6
|
+
module DocktorRails
|
|
7
|
+
module Checks
|
|
8
|
+
class ComposeEntrypointExec < BaseCheck
|
|
9
|
+
def id
|
|
10
|
+
"compose.entrypoint_exec"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(ctx)
|
|
14
|
+
root = ctx.fetch(:root)
|
|
15
|
+
platform = ctx[:platform]
|
|
16
|
+
|
|
17
|
+
if platform&.windows?
|
|
18
|
+
return pass("Windows host: skipping executable/shebang checks")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
compose_path = Compose.find_file(root)
|
|
22
|
+
return pass("No compose file found (skipping entrypoint exec check)") unless compose_path
|
|
23
|
+
|
|
24
|
+
doc = Compose.load_file(compose_path)
|
|
25
|
+
services = Compose.services(doc)
|
|
26
|
+
|
|
27
|
+
entrypoint_files = services.flat_map do |_svc_name, svc|
|
|
28
|
+
next [] unless svc.is_a?(Hash)
|
|
29
|
+
|
|
30
|
+
ep = svc["entrypoint"]
|
|
31
|
+
ep = normalize_entrypoint(ep)
|
|
32
|
+
next [] unless ep
|
|
33
|
+
|
|
34
|
+
file = entrypoint_file_candidate(ep)
|
|
35
|
+
file ? [file] : []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
entrypoint_files.uniq!
|
|
39
|
+
return pass("No entrypoint file paths detected") if entrypoint_files.empty?
|
|
40
|
+
|
|
41
|
+
problems = []
|
|
42
|
+
|
|
43
|
+
entrypoint_files.each do |file|
|
|
44
|
+
abs = File.expand_path(file, root)
|
|
45
|
+
next unless File.file?(abs) # existence handled elsewhere
|
|
46
|
+
|
|
47
|
+
problems << "#{file}: not executable" unless File.executable?(abs)
|
|
48
|
+
|
|
49
|
+
first = File.open(abs, "rb", &:readline) rescue ""
|
|
50
|
+
problems << "#{file}: missing shebang" unless first.start_with?("#!")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if problems.empty?
|
|
54
|
+
pass("Entrypoint executable/shebang OK")
|
|
55
|
+
else
|
|
56
|
+
fail("Entrypoint executable/shebang issues detected", files: problems)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def normalize_entrypoint(value)
|
|
63
|
+
case value
|
|
64
|
+
when Array
|
|
65
|
+
value.first
|
|
66
|
+
when String
|
|
67
|
+
value.strip
|
|
68
|
+
else
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def entrypoint_file_candidate(entrypoint)
|
|
74
|
+
first = entrypoint.split(/\s+/).first
|
|
75
|
+
return nil if first.nil? || first.empty?
|
|
76
|
+
|
|
77
|
+
looks_like_path = first.include?("/") || first.end_with?(".sh") || first.start_with?(".")
|
|
78
|
+
looks_like_path ? first : nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_check"
|
|
4
|
+
require_relative "../compose"
|
|
5
|
+
|
|
6
|
+
module DocktorRails
|
|
7
|
+
module Checks
|
|
8
|
+
class ComposeEntrypointSanity < BaseCheck
|
|
9
|
+
def id
|
|
10
|
+
"compose.entrypoint_sanity"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(ctx)
|
|
14
|
+
root = ctx.fetch(:root)
|
|
15
|
+
compose_path = Compose.find_file(root)
|
|
16
|
+
return pass("No compose file found (skipping entrypoint check)") unless compose_path
|
|
17
|
+
|
|
18
|
+
doc = Compose.load_file(compose_path)
|
|
19
|
+
services = Compose.services(doc)
|
|
20
|
+
|
|
21
|
+
entrypoints = services.flat_map do |svc_name, svc|
|
|
22
|
+
next [] unless svc.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
ep = svc["entrypoint"]
|
|
25
|
+
ep = normalize_entrypoint(ep)
|
|
26
|
+
next [] unless ep
|
|
27
|
+
|
|
28
|
+
[{ service: svc_name, entrypoint: ep }]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
return pass("No compose entrypoints referenced") if entrypoints.empty?
|
|
32
|
+
|
|
33
|
+
failures = []
|
|
34
|
+
entrypoints.each do |ep|
|
|
35
|
+
file = entrypoint_file_candidate(ep.fetch(:entrypoint))
|
|
36
|
+
next unless file
|
|
37
|
+
|
|
38
|
+
abs = File.expand_path(file, root)
|
|
39
|
+
unless abs.start_with?(File.expand_path(root) + File::SEPARATOR)
|
|
40
|
+
failures << { file: file, message: "entrypoint points outside repo root" }
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless File.file?(abs)
|
|
45
|
+
failures << { file: file, message: "file not found" }
|
|
46
|
+
next
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if File.binread(abs).include?("\r\n")
|
|
50
|
+
failures << { file: file, message: "CRLF line endings" }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if failures.empty?
|
|
55
|
+
pass("Entrypoint files look sane")
|
|
56
|
+
else
|
|
57
|
+
files = failures.map { |f| f.fetch(:file) }
|
|
58
|
+
fail("Compose entrypoint issues detected", files: files, hint: "Ensure entrypoint scripts exist and use LF line endings (not CRLF).")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def normalize_entrypoint(value)
|
|
65
|
+
case value
|
|
66
|
+
when Array
|
|
67
|
+
value.first
|
|
68
|
+
when String
|
|
69
|
+
value.strip
|
|
70
|
+
else
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def entrypoint_file_candidate(entrypoint)
|
|
76
|
+
return nil if entrypoint.nil? || entrypoint.empty?
|
|
77
|
+
|
|
78
|
+
# Strings like "bash -lc ..." are commands, not file paths.
|
|
79
|
+
first = entrypoint.split(/\s+/).first
|
|
80
|
+
return nil if first.nil? || first.empty?
|
|
81
|
+
|
|
82
|
+
looks_like_path = first.include?("/") || first.end_with?(".sh") || first.start_with?(".")
|
|
83
|
+
looks_like_path ? first : nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_check"
|
|
4
|
+
require_relative "../compose"
|
|
5
|
+
|
|
6
|
+
module DocktorRails
|
|
7
|
+
module Checks
|
|
8
|
+
class ComposeEnvFilePresent < BaseCheck
|
|
9
|
+
def id
|
|
10
|
+
"compose.env_file_present"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(ctx)
|
|
14
|
+
root = ctx.fetch(:root)
|
|
15
|
+
compose_path = Compose.find_file(root)
|
|
16
|
+
return pass("No compose file found (skipping env_file check)") unless compose_path
|
|
17
|
+
|
|
18
|
+
doc = Compose.load_file(compose_path)
|
|
19
|
+
services = Compose.services(doc)
|
|
20
|
+
|
|
21
|
+
missing = []
|
|
22
|
+
services.each do |_name, svc|
|
|
23
|
+
next unless svc.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
env_file = svc["env_file"]
|
|
26
|
+
files = case env_file
|
|
27
|
+
when String then [env_file]
|
|
28
|
+
when Array then env_file
|
|
29
|
+
else []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
files.each do |f|
|
|
33
|
+
next unless f.is_a?(String)
|
|
34
|
+
path = File.expand_path(f, root)
|
|
35
|
+
missing << f unless File.file?(path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if missing.empty?
|
|
40
|
+
pass("All compose env_file entries exist")
|
|
41
|
+
else
|
|
42
|
+
fail("Missing compose env_file files", files: missing.uniq.sort)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_check"
|
|
4
|
+
|
|
5
|
+
module DocktorRails
|
|
6
|
+
module Checks
|
|
7
|
+
class ComposeFilePresent < BaseCheck
|
|
8
|
+
CANDIDATES = ["compose.yml", "docker-compose.yml"].freeze
|
|
9
|
+
|
|
10
|
+
def id
|
|
11
|
+
"compose.file_present"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run(ctx)
|
|
15
|
+
root = ctx.fetch(:root)
|
|
16
|
+
found = CANDIDATES.map { |f| File.join(root, f) }.find { |p| File.file?(p) }
|
|
17
|
+
|
|
18
|
+
if found
|
|
19
|
+
pass("Compose file present", files: [relative_to_root(found, root)])
|
|
20
|
+
else
|
|
21
|
+
fail("No compose.yml or docker-compose.yml found", hint: "Add a compose.yml (or docker-compose.yml) at the repo root.")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def relative_to_root(path, root)
|
|
28
|
+
path.delete_prefix(root + File::SEPARATOR)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_check"
|
|
4
|
+
require_relative "../compose"
|
|
5
|
+
|
|
6
|
+
module DocktorRails
|
|
7
|
+
module Checks
|
|
8
|
+
class ComposeHealthchecks < BaseCheck
|
|
9
|
+
def id
|
|
10
|
+
"compose.healthchecks"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(ctx)
|
|
14
|
+
root = ctx.fetch(:root)
|
|
15
|
+
compose_path = Compose.find_file(root)
|
|
16
|
+
return pass("No compose file found (skipping healthcheck check)") unless compose_path
|
|
17
|
+
|
|
18
|
+
doc = Compose.load_file(compose_path)
|
|
19
|
+
services = Compose.services(doc)
|
|
20
|
+
|
|
21
|
+
targets = %w[db redis].select { |name| services.key?(name) }
|
|
22
|
+
return pass("No db/redis services detected") if targets.empty?
|
|
23
|
+
|
|
24
|
+
missing = targets.reject do |name|
|
|
25
|
+
svc = services[name]
|
|
26
|
+
svc.is_a?(Hash) && svc.key?("healthcheck") && !svc["healthcheck"].nil?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if missing.empty?
|
|
30
|
+
pass("Healthchecks present for db/redis")
|
|
31
|
+
else
|
|
32
|
+
warn("Missing healthchecks for: #{missing.join(", ")}", hint: "Add compose healthchecks so readiness is deterministic across machines.")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_check"
|
|
4
|
+
require_relative "../compose"
|
|
5
|
+
|
|
6
|
+
module DocktorRails
|
|
7
|
+
module Checks
|
|
8
|
+
class ComposePlatformRisk < BaseCheck
|
|
9
|
+
def id
|
|
10
|
+
"compose.platform_risk"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(ctx)
|
|
14
|
+
root = ctx.fetch(:root)
|
|
15
|
+
platform = ctx[:platform]
|
|
16
|
+
compose_path = Compose.find_file(root)
|
|
17
|
+
return pass("No compose file found (skipping platform check)") unless compose_path
|
|
18
|
+
|
|
19
|
+
return pass("Unknown host architecture (skipping platform risk)") if platform.nil? || platform.arch == :unknown
|
|
20
|
+
|
|
21
|
+
doc = Compose.load_file(compose_path)
|
|
22
|
+
services = Compose.services(doc)
|
|
23
|
+
|
|
24
|
+
pins = services.filter_map do |name, svc|
|
|
25
|
+
next unless svc.is_a?(Hash)
|
|
26
|
+
p = svc["platform"]
|
|
27
|
+
next unless p.is_a?(String)
|
|
28
|
+
[name, p]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
return pass("No compose platform pins") if pins.empty?
|
|
32
|
+
|
|
33
|
+
risky = pins.select { |_name, p| mismatch?(platform, p) }
|
|
34
|
+
|
|
35
|
+
if risky.empty?
|
|
36
|
+
pass("Compose platform pins match host")
|
|
37
|
+
else
|
|
38
|
+
warn("Compose platform pins may not match this host", files: risky.map { |n, p| "#{n}: #{p}" })
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def mismatch?(platform, pin)
|
|
45
|
+
pin = pin.downcase
|
|
46
|
+
return platform.arm64? && pin.include?("amd64")
|
|
47
|
+
return platform.amd64? && (pin.include?("arm64") || pin.include?("aarch64"))
|
|
48
|
+
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_check"
|
|
4
|
+
require_relative "../dotenv"
|
|
5
|
+
|
|
6
|
+
module DocktorRails
|
|
7
|
+
module Checks
|
|
8
|
+
class RailsMasterKeyRequired < BaseCheck
|
|
9
|
+
def id
|
|
10
|
+
"env.rails_master_key"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(ctx)
|
|
14
|
+
root = ctx.fetch(:root)
|
|
15
|
+
|
|
16
|
+
encrypted_files = encrypted_credentials_files(root)
|
|
17
|
+
return pass("No encrypted credentials detected") if encrypted_files.empty?
|
|
18
|
+
|
|
19
|
+
# Satisfy via file (common for local dev).
|
|
20
|
+
master_key_path = File.join(root, "config", "master.key")
|
|
21
|
+
if File.file?(master_key_path) && !File.read(master_key_path).strip.empty?
|
|
22
|
+
return pass("master.key present", files: ["config/master.key"])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Or via environment variable (common for CI/prod).
|
|
26
|
+
if ENV.key?("RAILS_MASTER_KEY") && !ENV.fetch("RAILS_MASTER_KEY").to_s.empty?
|
|
27
|
+
return pass("RAILS_MASTER_KEY is set in ENV")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Or via dotenv file commonly used for docker/compose flows.
|
|
31
|
+
dotenv_path = File.join(root, ".env.docker")
|
|
32
|
+
dotenv = Dotenv.parse_file(dotenv_path)
|
|
33
|
+
if dotenv.key?("RAILS_MASTER_KEY") && !dotenv.fetch("RAILS_MASTER_KEY").to_s.empty?
|
|
34
|
+
return pass("RAILS_MASTER_KEY present in .env.docker", files: [".env.docker"])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
fail(
|
|
38
|
+
"Missing master key for encrypted credentials",
|
|
39
|
+
files: encrypted_files,
|
|
40
|
+
hint: "Provide config/master.key (local), or set RAILS_MASTER_KEY (CI/prod), or add it to .env.docker."
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def encrypted_credentials_files(root)
|
|
47
|
+
files = []
|
|
48
|
+
|
|
49
|
+
default = File.join(root, "config", "credentials.yml.enc")
|
|
50
|
+
files << "config/credentials.yml.enc" if File.file?(default)
|
|
51
|
+
|
|
52
|
+
# Rails also supports per-environment credentials in config/credentials/*.yml.enc
|
|
53
|
+
Dir.glob(File.join(root, "config", "credentials", "*.yml.enc")).each do |path|
|
|
54
|
+
rel = path.delete_prefix(root + File::SEPARATOR)
|
|
55
|
+
files << rel
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
files.uniq
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|