chronos-ruby 1.1.3 → 1.1.4
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 +32 -0
- data/README.md +189 -96
- data/docs/adr/legacy-deploy-cli.md +21 -0
- data/docs/compatibility.md +1 -1
- data/docs/deploy-cli.md +36 -0
- data/docs/legacy-test-environment.md +59 -0
- data/docs/modules/sidekiq-legacy.md +1 -1
- data/docs/release-1.1.4-readiness.md +44 -0
- data/exe/chronos +3 -0
- data/lib/chronos/agent.rb +16 -5
- data/lib/chronos/cli.rb +117 -0
- data/lib/chronos/core/bounded_copy.rb +69 -0
- data/lib/chronos/core/notice.rb +4 -28
- data/lib/chronos/core/sanitizer.rb +1 -1
- data/lib/chronos/core/sql_lexer.rb +73 -0
- data/lib/chronos/core/sql_normalizer.rb +7 -7
- data/lib/chronos/core/telemetry_event.rb +5 -18
- data/lib/chronos/deploy/configuration.rb +96 -0
- data/lib/chronos/deploy/error.rb +14 -0
- data/lib/chronos/deploy/installer.rb +65 -0
- data/lib/chronos/deploy/providers.rb +50 -0
- data/lib/chronos/deploy/runner.rb +44 -0
- data/lib/chronos/deploy/secure_directory.rb +125 -0
- data/lib/chronos/rails/active_record_query_inspector.rb +2 -4
- data/lib/chronos/rake_tasks.rb +13 -1
- data/lib/chronos/version.rb +1 -1
- data/lib/generators/chronos/deploy/install/install_generator.rb +26 -0
- metadata +33 -3
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "chronos/deploy/error"
|
|
4
|
+
|
|
5
|
+
module Chronos
|
|
6
|
+
module Deploy
|
|
7
|
+
# @responsibility Validate and resolve public options, canonical ENV and legacy aliases.
|
|
8
|
+
# Resolves explicit CLI configuration without booting an application or agent.
|
|
9
|
+
class Configuration
|
|
10
|
+
FIELDS = %w(project_id project_key host environment service_name deploy_id revision version).freeze
|
|
11
|
+
ALIASES = {"environment" => "DEPLOY_ENVIRONMENT", "service_name" => "SERVICE_NAME",
|
|
12
|
+
"deploy_id" => "DEPLOY_ID", "revision" => "GIT_SHA", "version" => "APP_VERSION"}.freeze
|
|
13
|
+
HOST_ERROR = "Host must be an absolute HTTPS URL without credentials, query or fragment.".freeze
|
|
14
|
+
|
|
15
|
+
def self.read(options, env)
|
|
16
|
+
root = File.realpath(File.expand_path(options[:root] || Dir.pwd))
|
|
17
|
+
public_values = read_public(root)
|
|
18
|
+
values = FIELDS.each_with_object({}) do |field, result|
|
|
19
|
+
result[field] = resolve(field, options, env, public_values)
|
|
20
|
+
end
|
|
21
|
+
values["host"] ||= "https://chronosmonitor.com.br"
|
|
22
|
+
validate_host!(values["host"])
|
|
23
|
+
values
|
|
24
|
+
rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EACCES
|
|
25
|
+
raise Error, "Configuration root or file is unavailable; check --root and permissions."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.read_public(root)
|
|
29
|
+
path = File.join(root, "config/chronos-deploy.yml")
|
|
30
|
+
if File.symlink?(File.join(root, "config")) || File.symlink?(path)
|
|
31
|
+
raise Error, "Configuration path must not contain symlinks."
|
|
32
|
+
end
|
|
33
|
+
return {} unless File.exist?(path)
|
|
34
|
+
raise Error, "Public configuration must be a regular file." unless File.file?(path)
|
|
35
|
+
raise Error, "Public configuration exceeds 64 KiB." if File.size(path) > 65_536
|
|
36
|
+
|
|
37
|
+
parsed = YAML.safe_load(File.read(path))
|
|
38
|
+
values = parsed.nil? ? {} : parsed
|
|
39
|
+
raise ArgumentError unless values.is_a?(Hash) && (values.keys - (FIELDS - ["project_key"])).empty?
|
|
40
|
+
|
|
41
|
+
values.each_value { |value| validate_text!(value) }
|
|
42
|
+
values
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.resolve(field, options, env, public_values)
|
|
46
|
+
prefix = %w(revision version).include?(field) ? "CHRONOS_DEPLOY_" : "CHRONOS_"
|
|
47
|
+
primary = env["#{prefix}#{field.upcase}"]
|
|
48
|
+
alternate = env[ALIASES[field]] if ALIASES[field]
|
|
49
|
+
if primary && alternate && primary != alternate
|
|
50
|
+
raise Error, "Conflicting canonical and alias environment variables; keep only one source."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
value = options[field.to_sym] || primary || alternate || public_values[field]
|
|
54
|
+
validate_text!(value)
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.validate_text!(value)
|
|
59
|
+
raise ArgumentError unless value.nil? || value.is_a?(String)
|
|
60
|
+
return if value.nil?
|
|
61
|
+
return if value.valid_encoding? && value.encoding.ascii_compatible? && !value.match(/[[:cntrl:]]/)
|
|
62
|
+
|
|
63
|
+
raise Error, "Configuration values must be text without control characters."
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.validate_host!(host)
|
|
67
|
+
uri = URI.parse(host)
|
|
68
|
+
valid = uri.scheme == "https" && uri.host && !uri.userinfo && !uri.query && !uri.fragment
|
|
69
|
+
raise Error, HOST_ERROR unless valid
|
|
70
|
+
rescue URI::InvalidURIError
|
|
71
|
+
raise Error, HOST_ERROR
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.validate!(values, command)
|
|
75
|
+
required = %w(project_id project_key environment)
|
|
76
|
+
required << "deploy_id" if command != "verify"
|
|
77
|
+
missing = required.select { |field| values[field].to_s.strip.empty? }
|
|
78
|
+
raise Error.new("Supply the required configuration fields.", :missing_fields => missing) unless missing.empty?
|
|
79
|
+
|
|
80
|
+
validate_release!(values) unless command == "verify"
|
|
81
|
+
%w(environment service_name deploy_id revision version).each do |field|
|
|
82
|
+
if values[field] && values[field].bytesize > 128
|
|
83
|
+
raise Error, "Deployment metadata exceeds the 128-byte protocol limit."
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.validate_release!(values)
|
|
90
|
+
return unless values["revision"].to_s.strip.empty? && values["version"].to_s.strip.empty?
|
|
91
|
+
|
|
92
|
+
raise Error, "Supply the revision or version of the published artifact."
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Deploy
|
|
3
|
+
# @responsibility Carry static actionable errors and allowlisted diagnostics without raw input.
|
|
4
|
+
# Messages must be static: never interpolate input or exception messages.
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
attr_reader :details
|
|
7
|
+
|
|
8
|
+
def initialize(message, details = {})
|
|
9
|
+
super(message)
|
|
10
|
+
@details = details
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require "chronos/deploy/error"
|
|
2
|
+
require "chronos/deploy/providers"
|
|
3
|
+
|
|
4
|
+
module Chronos
|
|
5
|
+
module Deploy
|
|
6
|
+
# @responsibility Generate provider fragments while preserving existing customer integrations.
|
|
7
|
+
# Explicit, idempotent generation of static fragments below a caller-selected root.
|
|
8
|
+
class Installer
|
|
9
|
+
PATHS = Providers::PATHS
|
|
10
|
+
|
|
11
|
+
def install(options)
|
|
12
|
+
raise Error, "Use --check without --force." if options[:force] && options[:check]
|
|
13
|
+
|
|
14
|
+
relative = PATHS[options[:provider]]
|
|
15
|
+
raise Error, "Choose a supported --provider; see chronos --help." unless relative
|
|
16
|
+
|
|
17
|
+
root = installation_root(options)
|
|
18
|
+
require "chronos/deploy/secure_directory"
|
|
19
|
+
SecureDirectory.with_root(root) do |directory|
|
|
20
|
+
install_into(directory, relative, options)
|
|
21
|
+
end
|
|
22
|
+
rescue Errno::EEXIST, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP
|
|
23
|
+
raise Error, "Integration destination is unavailable or changed; inspect it before retrying."
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def install_into(directory, relative, options)
|
|
29
|
+
content = Providers.template(options[:provider])
|
|
30
|
+
existing = directory.with_parent(relative, false) { |parent, name| parent && parent.read(name) }
|
|
31
|
+
return "unchanged" if unchanged?(existing, relative, content, options)
|
|
32
|
+
raise Error, "Integration is missing; run install without --check." if options[:check]
|
|
33
|
+
return "would_write_#{relative}" if options[:dry_run]
|
|
34
|
+
|
|
35
|
+
directory.with_parent(relative, true) do |parent, name|
|
|
36
|
+
current = parent.read(name)
|
|
37
|
+
unless unchanged?(current, relative, content, options)
|
|
38
|
+
parent.write(name, content, options[:provider] == "kamal", options[:force])
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
"created_#{relative}; merge after successful publication; supply explicit published identity and revision"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def installation_root(options)
|
|
45
|
+
root = File.expand_path(options[:root] || Dir.pwd)
|
|
46
|
+
unless File.directory?(root) && !File.symlink?(root)
|
|
47
|
+
raise Error, "Installation root must be an existing directory, not a symlink."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
root
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def unchanged?(existing, relative, content, options)
|
|
54
|
+
return false if existing.nil?
|
|
55
|
+
return true if existing == content
|
|
56
|
+
return false if options[:force] && !options[:check]
|
|
57
|
+
|
|
58
|
+
proposed = content.lines.map { |line| "+#{line}" }.join
|
|
59
|
+
raise Error.new("Existing integration differs; merge the proposed template or explicitly use --force.",
|
|
60
|
+
:path => relative,
|
|
61
|
+
:diff => "--- existing (content withheld for privacy)\n+++ proposed\n#{proposed}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Deploy
|
|
3
|
+
# Static integration fragments never interpolate customer input or credentials.
|
|
4
|
+
module Providers
|
|
5
|
+
PATHS = {
|
|
6
|
+
"kamal" => ".kamal/hooks/post-deploy",
|
|
7
|
+
"capistrano" => "lib/capistrano/tasks/chronos_deploy.rake",
|
|
8
|
+
"github-actions" => "config/chronos/deploy/github-actions.steps.yml",
|
|
9
|
+
"gitlab-ci" => "config/chronos/deploy/gitlab-ci.job.yml",
|
|
10
|
+
"jenkins" => "config/chronos/deploy/jenkins.post.groovy",
|
|
11
|
+
"azure-devops" => "config/chronos/deploy/azure-pipelines.steps.yml"
|
|
12
|
+
}.freeze
|
|
13
|
+
COMMAND = "bundle exec chronos deploy notify --on-error=warn".freeze
|
|
14
|
+
MARKER = "# Chronos deploy template v1; credentials from environment only.\n".freeze
|
|
15
|
+
INSTRUCTIONS = "# Supply CHRONOS_DEPLOY_ID and CHRONOS_DEPLOY_REVISION/VERSION " \
|
|
16
|
+
"from the actual publication.\n".freeze
|
|
17
|
+
CAPISTRANO = <<-'RUBY'.freeze
|
|
18
|
+
# Do not also load chronos/capistrano. Runs locally once after publication.
|
|
19
|
+
namespace :chronos_cli do
|
|
20
|
+
task :notify do
|
|
21
|
+
if Rake::Task.task_defined?("chronos:notify_deploy")
|
|
22
|
+
raise "Remove the duplicate chronos/capistrano integration"
|
|
23
|
+
end
|
|
24
|
+
system("bundle", "exec", "chronos", "deploy", "notify", "--on-error=warn")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
after "deploy:published", "chronos_cli:notify"
|
|
28
|
+
RUBY
|
|
29
|
+
BODIES = {
|
|
30
|
+
"kamal" => "exec #{COMMAND}\n",
|
|
31
|
+
"capistrano" => CAPISTRANO,
|
|
32
|
+
"github-actions" => "# Merge into the real job after successful publication.\n" \
|
|
33
|
+
"- name: Notify Chronos\n run: #{COMMAND}\n",
|
|
34
|
+
"gitlab-ci" => "# Merge script into the successful deployment job; preserve artifact outputs.\n" \
|
|
35
|
+
".chronos_deploy_notification:\n script:\n - #{COMMAND}\n",
|
|
36
|
+
"jenkins" => "// Merge into existing post { success { ... } }; export actual publication outputs.\n" \
|
|
37
|
+
"sh '#{COMMAND}'\n",
|
|
38
|
+
"azure-devops" => "# Merge after successful publication in the same job.\n" \
|
|
39
|
+
"- script: #{COMMAND}\n displayName: Notify Chronos\n condition: succeeded()\n"
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
def self.template(provider)
|
|
43
|
+
comments = MARKER + INSTRUCTIONS
|
|
44
|
+
comments = comments.gsub(/^#/, "//") if provider == "jenkins"
|
|
45
|
+
shebang = provider == "kamal" ? "#!/bin/sh\n" : ""
|
|
46
|
+
shebang + comments + BODIES.fetch(provider)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
require "chronos"
|
|
3
|
+
require "chronos/deploy/configuration"
|
|
4
|
+
|
|
5
|
+
module Chronos
|
|
6
|
+
module Deploy
|
|
7
|
+
# @responsibility Execute the existing deploy or receipt API with bounded transport and guaranteed close.
|
|
8
|
+
# One explicit operation through the existing agent, always closed on exit.
|
|
9
|
+
class Runner
|
|
10
|
+
def call(command, values, timeout)
|
|
11
|
+
Configuration.validate!(values, command)
|
|
12
|
+
begin
|
|
13
|
+
configure(values, timeout, command)
|
|
14
|
+
outcome = if command == "verify"
|
|
15
|
+
Chronos.verify_integration
|
|
16
|
+
else
|
|
17
|
+
Chronos.notify_deploy({:deploy_id => values["deploy_id"], :revision => values["revision"],
|
|
18
|
+
:version => values["version"]}, timeout)
|
|
19
|
+
end
|
|
20
|
+
ensure
|
|
21
|
+
closed = Chronos.close(timeout)
|
|
22
|
+
end
|
|
23
|
+
command == "notify" ? outcome == true && closed != false : outcome
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def configure(values, timeout, command)
|
|
29
|
+
Chronos.configure do |config|
|
|
30
|
+
values.each do |key, value|
|
|
31
|
+
attribute = key == "version" ? "app_version" : key
|
|
32
|
+
config.public_send("#{attribute}=", value) unless value.nil?
|
|
33
|
+
end
|
|
34
|
+
config.logger = Logger.new(File::NULL)
|
|
35
|
+
config.timeout = timeout
|
|
36
|
+
config.open_timeout = timeout
|
|
37
|
+
config.max_retries = 0
|
|
38
|
+
config.backlog_size = 0
|
|
39
|
+
config.dependency_reporting = false if command == "verify"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
require "fiddle/import"
|
|
2
|
+
require "securerandom"
|
|
3
|
+
require "chronos/deploy/error"
|
|
4
|
+
|
|
5
|
+
module Chronos
|
|
6
|
+
module Deploy
|
|
7
|
+
# @responsibility Anchor installer reads and writes to directory descriptors without following symlinks.
|
|
8
|
+
# POSIX openat/mkdirat/linkat/renameat prevent parent-path replacement from redirecting writes.
|
|
9
|
+
class SecureDirectory
|
|
10
|
+
# Fixed POSIX entry points; no shell, dynamic symbol names, or user-provided native code.
|
|
11
|
+
module Native
|
|
12
|
+
extend Fiddle::Importer
|
|
13
|
+
dlload Fiddle::Handle::DEFAULT
|
|
14
|
+
extern "int openat(int, const char*, int, unsigned int)"
|
|
15
|
+
extern "int mkdirat(int, const char*, unsigned int)"
|
|
16
|
+
extern "int linkat(int, const char*, int, const char*, int)"
|
|
17
|
+
extern "int renameat(int, const char*, int, const char*)"
|
|
18
|
+
extern "int unlinkat(int, const char*, int)"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.with_root(root)
|
|
22
|
+
raise Error, "Secure installation requires POSIX no-follow support." unless File.const_defined?(:NOFOLLOW)
|
|
23
|
+
|
|
24
|
+
handle = File.open(root, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
|
|
25
|
+
raise Error, "Installation root must be a directory." unless handle.stat.directory?
|
|
26
|
+
|
|
27
|
+
yield new(handle)
|
|
28
|
+
ensure
|
|
29
|
+
handle.close if handle && !handle.closed?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def with_parent(relative, create)
|
|
33
|
+
handles = []
|
|
34
|
+
current = self
|
|
35
|
+
parts = relative.split("/")
|
|
36
|
+
parts[0...-1].each do |part|
|
|
37
|
+
child = current.directory(part, create)
|
|
38
|
+
return yield(nil, parts.last) unless child
|
|
39
|
+
|
|
40
|
+
handles << child
|
|
41
|
+
current = self.class.new(child)
|
|
42
|
+
end
|
|
43
|
+
yield current, parts.last
|
|
44
|
+
ensure
|
|
45
|
+
handles.reverse_each(&:close)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize(handle)
|
|
49
|
+
@handle = handle
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def directory(name, create)
|
|
53
|
+
file = open_file(name, File::RDONLY | File::NONBLOCK)
|
|
54
|
+
if file.nil? && create
|
|
55
|
+
status = Native.mkdirat(@handle.fileno, name, 0o755)
|
|
56
|
+
check!(status) unless status.zero? || Fiddle.last_error == Errno::EEXIST::Errno
|
|
57
|
+
file = open_file(name, File::RDONLY | File::NONBLOCK)
|
|
58
|
+
end
|
|
59
|
+
return nil unless file
|
|
60
|
+
return file if file.stat.directory?
|
|
61
|
+
|
|
62
|
+
file.close
|
|
63
|
+
raise Error, "Installation parent must be a directory."
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def read(name)
|
|
67
|
+
file = open_file(name, File::RDONLY | File::NONBLOCK)
|
|
68
|
+
return nil unless file
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
raise Error, "Integration must be a regular file of at most 64 KiB." unless file.stat.file?
|
|
72
|
+
|
|
73
|
+
content = file.read(65_537) || ""
|
|
74
|
+
raise Error, "Integration must be a regular file of at most 64 KiB." if content.bytesize > 65_536
|
|
75
|
+
|
|
76
|
+
content
|
|
77
|
+
ensure
|
|
78
|
+
file.close
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def write(name, content, executable, force)
|
|
83
|
+
temporary = ".chronos-deploy-#{SecureRandom.hex(16)}"
|
|
84
|
+
file = open_file(temporary, File::WRONLY | File::CREAT | File::EXCL, 0o600)
|
|
85
|
+
raise Error, "Installation directory is unavailable." unless file
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
file.write(content)
|
|
89
|
+
file.chmod(executable ? 0o755 : 0o644)
|
|
90
|
+
file.close
|
|
91
|
+
if force
|
|
92
|
+
check!(Native.renameat(@handle.fileno, temporary, @handle.fileno, name))
|
|
93
|
+
else
|
|
94
|
+
check!(Native.linkat(@handle.fileno, temporary, @handle.fileno, name, 0))
|
|
95
|
+
end
|
|
96
|
+
ensure
|
|
97
|
+
file.close unless file.closed?
|
|
98
|
+
Native.unlinkat(@handle.fileno, temporary, 0)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def open_file(name, flags, mode = 0)
|
|
105
|
+
if name.empty? || name.include?("/") || name.include?("\0") || %w(. ..).include?(name)
|
|
106
|
+
raise Error, "Unsafe installation path component."
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
descriptor = Native.openat(@handle.fileno, name, flags | File::NOFOLLOW, mode)
|
|
110
|
+
return nil if descriptor < 0 && Fiddle.last_error == Errno::ENOENT::Errno
|
|
111
|
+
|
|
112
|
+
check!(descriptor)
|
|
113
|
+
file = File.for_fd(descriptor, (flags & File::WRONLY).zero? ? "r" : "w")
|
|
114
|
+
file.close_on_exec = true
|
|
115
|
+
file
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def check!(status)
|
|
119
|
+
return status if status >= 0
|
|
120
|
+
|
|
121
|
+
raise Error, "Installation path changed, contains a symlink, or is unavailable; inspect it before retrying."
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "json"
|
|
2
|
+
require "chronos/core/sql_lexer"
|
|
2
3
|
|
|
3
4
|
module Chronos
|
|
4
5
|
module Rails
|
|
@@ -63,10 +64,7 @@ module Chronos
|
|
|
63
64
|
return false unless connection
|
|
64
65
|
return false unless (query["operation"] || query[:operation]).to_s == "SELECT"
|
|
65
66
|
return false if table(query).empty?
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
normalized = (query["normalized_query"] || query[:normalized_query]).to_s
|
|
69
|
-
normalized.sub(/;\s*\z/, "") !~ /;/
|
|
67
|
+
Core::SqlLexer.new(raw_sql, adapter_name(connection)).single_select?
|
|
70
68
|
end
|
|
71
69
|
|
|
72
70
|
def indexes(connection, table_name)
|
data/lib/chronos/rake_tasks.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Chronos
|
|
|
7
7
|
#
|
|
8
8
|
# @responsibility Register the integration verification task with deterministic JSON output.
|
|
9
9
|
# @motivation Give Rails and plain Ruby operators one repeatable end-to-end credential check.
|
|
10
|
-
# @limits
|
|
10
|
+
# @limits Deploy reads explicit CLI ENV; verification uses the configured agent. Neither prints credentials.
|
|
11
11
|
# @collaborators Rake and Chronos.verify_integration.
|
|
12
12
|
# @thread_safety Installation checks the process-global Rake registry and is intended for boot time.
|
|
13
13
|
# @compatibility Rake versions compatible with Ruby 2.2.10 through Ruby 2.6.
|
|
@@ -22,6 +22,7 @@ module Chronos
|
|
|
22
22
|
TASK_NAME = "chronos:verify_integration".freeze
|
|
23
23
|
|
|
24
24
|
def self.install(options = {})
|
|
25
|
+
install_deploy(options)
|
|
25
26
|
return false if ::Rake::Task.task_defined?(TASK_NAME)
|
|
26
27
|
|
|
27
28
|
output = options[:output] || $stdout
|
|
@@ -37,5 +38,16 @@ module Chronos
|
|
|
37
38
|
end
|
|
38
39
|
true
|
|
39
40
|
end
|
|
41
|
+
|
|
42
|
+
def self.install_deploy(options)
|
|
43
|
+
return if ::Rake::Task.task_defined?("chronos:deploy")
|
|
44
|
+
|
|
45
|
+
task "chronos:deploy" do
|
|
46
|
+
require "chronos/cli"
|
|
47
|
+
status = Chronos::CLI.new(options[:output] || $stdout, options[:env] || ENV).run(["deploy", "notify"])
|
|
48
|
+
(options[:exit] || proc { |code| exit(code) }).call(status) unless status.zero?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
private_class_method :install_deploy
|
|
40
52
|
end
|
|
41
53
|
end
|
data/lib/chronos/version.rb
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "chronos/deploy/installer"
|
|
3
|
+
|
|
4
|
+
module Chronos
|
|
5
|
+
module Generators
|
|
6
|
+
module Deploy
|
|
7
|
+
# @responsibility Delegate explicit Rails generation to the shared deploy installer.
|
|
8
|
+
# Rails adapter for the same explicit installer used by the CLI.
|
|
9
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
10
|
+
namespace "chronos:deploy:install"
|
|
11
|
+
class_option :provider, :type => :string, :required => true
|
|
12
|
+
class_option :dry_run, :type => :boolean, :default => false
|
|
13
|
+
class_option :check, :type => :boolean, :default => false
|
|
14
|
+
class_option :force, :type => :boolean, :default => false
|
|
15
|
+
|
|
16
|
+
def install_deploy
|
|
17
|
+
result = Chronos::Deploy::Installer.new.install(
|
|
18
|
+
:root => destination_root, :provider => options[:provider],
|
|
19
|
+
:dry_run => options[:dry_run], :check => options[:check], :force => options[:force]
|
|
20
|
+
)
|
|
21
|
+
say result
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chronos-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antonio Jefferson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -44,6 +44,20 @@ dependencies:
|
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
46
|
version: 12.3.3
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: minitest
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - '='
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 5.10.3
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - '='
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 5.10.3
|
|
47
61
|
- !ruby/object:Gem::Dependency
|
|
48
62
|
name: rspec
|
|
49
63
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -93,7 +107,8 @@ description: Cliente oficial do Chronos para capturar e enviar exceções, métr
|
|
|
93
107
|
dados em https://chronosmonitor.com.br.
|
|
94
108
|
email:
|
|
95
109
|
- antoniojeferson96@gmail.com
|
|
96
|
-
executables:
|
|
110
|
+
executables:
|
|
111
|
+
- chronos
|
|
97
112
|
extensions: []
|
|
98
113
|
extra_rdoc_files: []
|
|
99
114
|
files:
|
|
@@ -129,12 +144,15 @@ files:
|
|
|
129
144
|
- docs/adr/ADR-017-deploy-events-and-correlation.md
|
|
130
145
|
- docs/adr/ADR-018-pre-1.0-hardening.md
|
|
131
146
|
- docs/adr/ADR-019-bounded-query-diagnostics.md
|
|
147
|
+
- docs/adr/legacy-deploy-cli.md
|
|
132
148
|
- docs/architecture.md
|
|
133
149
|
- docs/compatibility.md
|
|
134
150
|
- docs/configuration.md
|
|
135
151
|
- docs/data-collected.md
|
|
152
|
+
- docs/deploy-cli.md
|
|
136
153
|
- docs/deprecation-policy.md
|
|
137
154
|
- docs/examples/plain-ruby.md
|
|
155
|
+
- docs/legacy-test-environment.md
|
|
138
156
|
- docs/migration-from-airbrake.md
|
|
139
157
|
- docs/modules/active-job.md
|
|
140
158
|
- docs/modules/apm-aggregation.md
|
|
@@ -171,9 +189,11 @@ files:
|
|
|
171
189
|
- docs/release-1.1-readiness.md
|
|
172
190
|
- docs/release-1.1.2-readiness.md
|
|
173
191
|
- docs/release-1.1.3-readiness.md
|
|
192
|
+
- docs/release-1.1.4-readiness.md
|
|
174
193
|
- docs/security-review.md
|
|
175
194
|
- docs/semver.md
|
|
176
195
|
- docs/troubleshooting.md
|
|
196
|
+
- exe/chronos
|
|
177
197
|
- lib/chronos.rb
|
|
178
198
|
- lib/chronos/adapters.rb
|
|
179
199
|
- lib/chronos/adapters/net_http_transport.rb
|
|
@@ -192,12 +212,14 @@ files:
|
|
|
192
212
|
- lib/chronos/application/retry_policy.rb
|
|
193
213
|
- lib/chronos/application/verify_integration.rb
|
|
194
214
|
- lib/chronos/capistrano.rb
|
|
215
|
+
- lib/chronos/cli.rb
|
|
195
216
|
- lib/chronos/configuration.rb
|
|
196
217
|
- lib/chronos/configuration/apm_validation.rb
|
|
197
218
|
- lib/chronos/configuration/snapshot.rb
|
|
198
219
|
- lib/chronos/configuration/validation.rb
|
|
199
220
|
- lib/chronos/core.rb
|
|
200
221
|
- lib/chronos/core/backtrace_parser.rb
|
|
222
|
+
- lib/chronos/core/bounded_copy.rb
|
|
201
223
|
- lib/chronos/core/breadcrumb.rb
|
|
202
224
|
- lib/chronos/core/cache_normalizer.rb
|
|
203
225
|
- lib/chronos/core/correlation_context.rb
|
|
@@ -212,9 +234,16 @@ files:
|
|
|
212
234
|
- lib/chronos/core/safe_serializer.rb
|
|
213
235
|
- lib/chronos/core/sanitizer.rb
|
|
214
236
|
- lib/chronos/core/sensitive_value_filter.rb
|
|
237
|
+
- lib/chronos/core/sql_lexer.rb
|
|
215
238
|
- lib/chronos/core/sql_normalizer.rb
|
|
216
239
|
- lib/chronos/core/sql_query_analyzer.rb
|
|
217
240
|
- lib/chronos/core/telemetry_event.rb
|
|
241
|
+
- lib/chronos/deploy/configuration.rb
|
|
242
|
+
- lib/chronos/deploy/error.rb
|
|
243
|
+
- lib/chronos/deploy/installer.rb
|
|
244
|
+
- lib/chronos/deploy/providers.rb
|
|
245
|
+
- lib/chronos/deploy/runner.rb
|
|
246
|
+
- lib/chronos/deploy/secure_directory.rb
|
|
218
247
|
- lib/chronos/errors.rb
|
|
219
248
|
- lib/chronos/integrations.rb
|
|
220
249
|
- lib/chronos/integrations/active_job.rb
|
|
@@ -245,6 +274,7 @@ files:
|
|
|
245
274
|
- lib/chronos/ruby/version.rb
|
|
246
275
|
- lib/chronos/sidekiq.rb
|
|
247
276
|
- lib/chronos/version.rb
|
|
277
|
+
- lib/generators/chronos/deploy/install/install_generator.rb
|
|
248
278
|
- lib/generators/chronos/install/install_generator.rb
|
|
249
279
|
- lib/generators/chronos/install/templates/chronos.rb
|
|
250
280
|
homepage: https://github.com/antoniojefferson/chronos-ruby
|