Module: Kryptic
- Defined in:
- lib/kryptic.rb
Defined Under Namespace
Classes: Result
Constant Summary collapse
- PROTOCOL_VERSION =
1
Class Method Summary collapse
-
.find_kryptic_json ⇒ Object
Walks up from the working directory looking for kryptic.json.
-
.inject!(environment: nil, project_id: nil, timeout_ms: nil) ⇒ Object
Call before the app reads ENV, e.g.
- .request(project_id, environment, timeout_seconds) ⇒ Object
-
.round_trip_named_pipe(path, line, timeout_seconds) ⇒ Object
The daemon serves a byte-mode named pipe, so a plain file handle works.
- .skip_reason ⇒ Object
- .socket_path ⇒ Object
- .warn_once(message) ⇒ Object
- .windows? ⇒ Boolean
Class Method Details
.find_kryptic_json ⇒ Object
Walks up from the working directory looking for kryptic.json.
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/kryptic.rb', line 137 def find_kryptic_json directory = Dir.pwd loop do candidate = File.join(directory, "kryptic.json") if File.file?(candidate) begin return JSON.parse(File.read(candidate)) rescue JSON::ParserError warn_once "could not parse #{candidate} - ignoring it." return nil end end parent = File.dirname(directory) return nil if parent == directory directory = parent end end |
.inject!(environment: nil, project_id: nil, timeout_ms: nil) ⇒ Object
Call before the app reads ENV, e.g. in config/application.rb:
Kryptic.inject! if Rails.env.development?
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/kryptic.rb', line 22 def inject!(environment: nil, project_id: nil, timeout_ms: nil) reason = skip_reason return Result.new(injected: 0, skipped: true, reason: reason) if reason config = find_kryptic_json project_id ||= ENV["KRYPTIC_PROJECT_ID"] || config&.fetch("projectId", nil) unless project_id warn_once "no kryptic.json found (and no KRYPTIC_PROJECT_ID set) - nothing to inject." return Result.new(injected: 0, skipped: true, reason: "no_project") end environment ||= ENV["KRYPTIC_ENV"] || config&.fetch("defaultEnvironment", nil) || "development" timeout_ms ||= (ENV["KRYPTIC_TIMEOUT_MS"] || 2000).to_i begin response = request(project_id, environment, timeout_ms / 1000.0) rescue StandardError => e warn_once "daemon not reachable (#{e.class}: #{e.}) - continuing without injected secrets." return Result.new(injected: 0, skipped: true, reason: "daemon_unreachable") end unless response["ok"] error = response["error"] || "internal" warn_once "daemon refused the request (#{error}): #{response["message"]}" return Result.new(injected: 0, skipped: true, reason: error) end injected = 0 (response["secrets"] || []).each do |secret| key = secret["key"] next if key.nil? || key.empty? || ENV.key?(key) # real environment always wins ENV[key] = secret["value"].to_s injected += 1 end Result.new(injected: injected, skipped: false) end |
.request(project_id, environment, timeout_seconds) ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/kryptic.rb', line 90 def request(project_id, environment, timeout_seconds) payload = { v: PROTOCOL_VERSION, type: "secrets", projectId: project_id, environment: environment } line = JSON.generate(payload) + "\n" path = socket_path if windows? && path.start_with?('\\\\.\\pipe\\') JSON.parse(round_trip_named_pipe(path, line, timeout_seconds)) else Timeout.timeout(timeout_seconds) do UNIXSocket.open(path) do |socket| socket.write(line) socket.gets("\n").then { |raw| JSON.parse(raw) } end end end end |
.round_trip_named_pipe(path, line, timeout_seconds) ⇒ Object
The daemon serves a byte-mode named pipe, so a plain file handle works. The timeout covers connecting (the pipe can briefly report "busy" between served clients); the read then blocks until the daemon replies, which it does immediately or not at all; matching the .NET client's semantics.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/kryptic.rb', line 111 def round_trip_named_pipe(path, line, timeout_seconds) deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_seconds pipe = nil begin pipe = File.open(path, "r+b") rescue SystemCallError raise IOError, "timed out connecting to the daemon pipe" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline sleep 0.05 retry end begin pipe.write(line) pipe.flush response = pipe.gets("\n") raise IOError, "connection closed" unless response response ensure pipe.close end end |
.skip_reason ⇒ Object
62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/kryptic.rb', line 62 def skip_reason return "disabled" if ENV["KRYPTIC_DISABLED"] == "true" # Rails/Rack conventions first, then the generic ones. %w[RAILS_ENV RACK_ENV APP_ENV ENVIRONMENT].each do |variable| value = ENV[variable].to_s.downcase return "#{variable.downcase}_#{value}" if %w[production prod staging].include?(value) end nil end |
.socket_path ⇒ Object
74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/kryptic.rb', line 74 def socket_path return ENV["KRYPTIC_SOCKET_PATH"] if ENV["KRYPTIC_SOCKET_PATH"] return '\\\\.\\pipe\\kryptic-daemon' if windows? if RUBY_PLATFORM.include?("linux") && ENV["XDG_RUNTIME_DIR"] return File.join(ENV["XDG_RUNTIME_DIR"], "kryptic-daemon.sock") end "/tmp/kryptic-daemon.sock" end |
.warn_once(message) ⇒ Object
156 157 158 159 160 |
# File 'lib/kryptic.rb', line 156 def warn_once() return if ENV["KRYPTIC_SILENT"] == "true" warn "[kryptic] #{}" end |
.windows? ⇒ Boolean
86 87 88 |
# File 'lib/kryptic.rb', line 86 def windows? RUBY_PLATFORM.match?(/mswin|mingw|cygwin/) end |