598 lines
11 KiB
Ruby
598 lines
11 KiB
Ruby
require "bundler/setup"
|
|
require "sinatra"
|
|
require "sinatra/cookies"
|
|
require "sinatra/multi_route"
|
|
require "sinatra/quiet_logger"
|
|
require "time"
|
|
require "fileutils"
|
|
require "json"
|
|
require "singleton"
|
|
|
|
require "securerandom"
|
|
require "random/formatter"
|
|
require "ulid"
|
|
require "anyflake"
|
|
|
|
require "jwt"
|
|
|
|
require "httparty"
|
|
|
|
$LOAD_PATH.unshift File.dirname(__FILE__) + "/lib"
|
|
|
|
require "config"
|
|
|
|
VERSION = "0.1.4"
|
|
|
|
CHUNK_SIZE = 1024**2
|
|
SESSION_SECRET_HEX_LENGTH = 64
|
|
JWT_SECRET_HEX_LENGTH = 64
|
|
DEFAULT_FLAKEY = 50
|
|
|
|
NAME = "kubernaut".freeze
|
|
ENV_PREFIX = NAME.upcase
|
|
|
|
CLK_TCK = 100
|
|
PROC_UPTIME_PATH = "/proc/uptime".freeze
|
|
|
|
SECONDS_PER_YEAR = 31_556_952
|
|
SECONDS_PER_MINUTE = 60
|
|
SECONDS_PER_HOUR = 3_600
|
|
SECONDS_PER_DAY = 86_400
|
|
SECONDS_PER_WEEK = 604_800
|
|
SECONDS_PER_MONTH = SECONDS_PER_YEAR / 12
|
|
|
|
DURATION_PARTS = [
|
|
[SECONDS_PER_YEAR, "year", "y"],
|
|
[SECONDS_PER_MONTH, "month", "m"],
|
|
[SECONDS_PER_WEEK, "week", "w"],
|
|
[SECONDS_PER_DAY, "day", "d"],
|
|
[SECONDS_PER_HOUR, "hour", "h"],
|
|
[SECONDS_PER_MINUTE, "minute", "m"],
|
|
[1, "second", "s"]
|
|
].freeze
|
|
|
|
config = Config.new
|
|
|
|
set :quiet_logger_prefixes, %w[livez readyz]
|
|
set :session_secret, config.session_secret.unwrap
|
|
set :public_folder, __dir__ + "/static"
|
|
|
|
register Sinatra::QuietLogger
|
|
|
|
module Sinatra
|
|
module RequestHeadersHelper
|
|
def req_headers
|
|
hash = request.env.select { |k, _| k.start_with? "HTTP_" }
|
|
.collect { |k, v| [k.gsub(/^HTTP_/, "").tr("_", "-").downcase, v] }
|
|
.sort
|
|
h = Rack::Headers.new
|
|
h.merge hash
|
|
end
|
|
end
|
|
|
|
helpers RequestHeadersHelper
|
|
end
|
|
|
|
module State
|
|
def enable
|
|
FileUtils.touch(@file)
|
|
end
|
|
|
|
def disable
|
|
File.delete @file if File.file? @file
|
|
end
|
|
|
|
def enabled?
|
|
File.file? @file
|
|
end
|
|
|
|
def toggle
|
|
return enable unless enabled?
|
|
|
|
disable
|
|
end
|
|
end
|
|
|
|
module UpDown
|
|
def up
|
|
enable
|
|
end
|
|
|
|
def down
|
|
disable
|
|
end
|
|
|
|
def to_s
|
|
enabled? ? "up" : "down"
|
|
end
|
|
|
|
def to_json(*_args)
|
|
return unless enabled?
|
|
|
|
JSON.generate({"status" => "ok"})
|
|
end
|
|
end
|
|
|
|
class TickTock
|
|
def initialize
|
|
@pid = ppid
|
|
@procfs_f = format "/proc/%s/stat", @pid
|
|
end
|
|
|
|
def uptime
|
|
x = ""
|
|
File.open @procfs_f do |f|
|
|
f.each_line do |l|
|
|
x = l.strip.split
|
|
end
|
|
end
|
|
system_uptime - (Integer(x[21]) / CLK_TCK)
|
|
end
|
|
|
|
def started_at
|
|
Time.now - uptime
|
|
end
|
|
end
|
|
|
|
class Health
|
|
include Singleton
|
|
include State
|
|
include UpDown
|
|
|
|
def initialize
|
|
@file = "/dev/shm/healthy"
|
|
end
|
|
|
|
def healthy?
|
|
enabled?
|
|
end
|
|
end
|
|
|
|
class Ready
|
|
include Singleton
|
|
include State
|
|
include UpDown
|
|
|
|
def initialize
|
|
@file = "/dev/shm/ready"
|
|
end
|
|
|
|
def ready?
|
|
enabled?
|
|
end
|
|
end
|
|
|
|
class Sleep
|
|
include Singleton
|
|
include State
|
|
|
|
def initialize
|
|
@file = "/dev/shm/sleepy"
|
|
end
|
|
|
|
def asleep?
|
|
enabled?
|
|
end
|
|
|
|
def wake
|
|
disable
|
|
end
|
|
|
|
def sleep
|
|
enable
|
|
end
|
|
end
|
|
|
|
def ppid
|
|
pid = ENV.fetch "PUMA_PID", Process.pid
|
|
begin
|
|
Integer pid
|
|
rescue ArgumentError
|
|
-1
|
|
end
|
|
end
|
|
|
|
def system_uptime
|
|
uptime_s = File.read PROC_UPTIME_PATH
|
|
Float uptime_s.strip.split[0]
|
|
end
|
|
|
|
##
|
|
# Convert to human friendly time interval
|
|
#
|
|
# @param [Integer] time in seconds
|
|
def human_time(seconds, delim = " ")
|
|
s = []
|
|
DURATION_PARTS.each do |divisor, unit, abbrev|
|
|
q = seconds / divisor
|
|
r = seconds % divisor
|
|
s.push "#{q}#{abbrev}" if q.positive?
|
|
seconds = r
|
|
end
|
|
|
|
s.join delim
|
|
end
|
|
|
|
Health.instance.up
|
|
Ready.instance.up
|
|
Sleep.instance.wake
|
|
|
|
def flaky(pct = DEFAULT_FLAKEY)
|
|
r = Random.rand(0..100)
|
|
unless r < (100 - pct)
|
|
halt 500, "so unreliable"
|
|
end
|
|
end
|
|
|
|
enable :sessions
|
|
|
|
puts "#{NAME} #{VERSION} staring, per aspera ad astra"
|
|
|
|
configure do
|
|
mime_type :json, "application/json"
|
|
end
|
|
|
|
before do
|
|
# content_type 'text/plain'
|
|
sleep(1) while Sleep.instance.asleep? && request.path_info != "/livez/sleep"
|
|
content_type :text if request.path_info.start_with? "/_cat"
|
|
request.session_options[:skip] = !request.path_info.start_with?("/session")
|
|
|
|
if params.has_key? :flaky
|
|
begin
|
|
pct = Integer(params[:flaky])
|
|
pct = pct.clamp(0, 100)
|
|
rescue => e
|
|
logger.warn "#{e.message}: falling back to default flaky percentage of #{DEFAULT_FLAKEY}"
|
|
pct = DEFAULT_FLAKEY
|
|
end
|
|
flaky(pct)
|
|
end
|
|
end
|
|
|
|
helpers do
|
|
def jsonify(obj, opts: nil, pretty: false)
|
|
buf = if pretty
|
|
JSON.pretty_generate obj, opts:
|
|
else
|
|
JSON.generate(obj, opts:)
|
|
end
|
|
"#{buf}\n"
|
|
end
|
|
|
|
def protected! hidden = false
|
|
return if authorized?
|
|
|
|
if hidden
|
|
halt 404, "Not Found"
|
|
else
|
|
headers["WWW-Authenticate"] = 'Basic realm="Restricted Area"'
|
|
halt 401, "Unauthorized"
|
|
end
|
|
end
|
|
|
|
def authorized?
|
|
@auth ||= Rack::Auth::Basic::Request.new(request.env)
|
|
@auth.provided? and
|
|
@auth.basic? and
|
|
@auth.credentials and
|
|
@auth.credentials == ["qwer", "asdf"]
|
|
end
|
|
|
|
def hostname
|
|
ENV["HOSTNAME"]
|
|
end
|
|
end
|
|
|
|
get "/" do
|
|
"hello there!\n"
|
|
end
|
|
|
|
get "/env", provides: "json" do
|
|
pretty = params.key? :pretty
|
|
|
|
jsonify ENV.sort.to_h, pretty:
|
|
end
|
|
|
|
get "/headers", provides: "json" do
|
|
pretty = params.key? :pretty
|
|
h = req_headers
|
|
|
|
jsonify h, pretty:
|
|
end
|
|
|
|
get "/uptime", provides: "json" do
|
|
tt = TickTock.new
|
|
x = {started_at: tt.started_at, seconds: tt.uptime.to_i, human: human_time(tt.uptime.to_i)}
|
|
|
|
jsonify x
|
|
end
|
|
|
|
post "/api/livez/toggle" do
|
|
Health.instance.toggle
|
|
"ok\n"
|
|
end
|
|
|
|
post "/api/livez/sleep" do
|
|
Sleep.instance.toggle
|
|
"ok\n"
|
|
end
|
|
|
|
get "/livez" do
|
|
error 503 unless Health.instance.healthy?
|
|
|
|
return Health.instance.to_json if request.env["HTTP_ACCEPT"] == "application/json"
|
|
|
|
Health.instance.to_s
|
|
end
|
|
|
|
get "/readyz" do
|
|
error 503 unless Ready.instance.ready?
|
|
|
|
return Ready.instance.to_json if request.env["HTTP_ACCEPT"] == "application/json"
|
|
|
|
Ready.instance.to_s
|
|
end
|
|
|
|
get "/uuid" do
|
|
n = params.fetch(:n, 1).to_i
|
|
stream do |out|
|
|
n.times do |_|
|
|
out << format("%s\n", SecureRandom.uuid)
|
|
end
|
|
end
|
|
end
|
|
|
|
get "/ulid" do
|
|
n = params.fetch(:n, 1).to_i
|
|
stream do |out|
|
|
n.times do |_|
|
|
out << format("%s\n", ULID.generate)
|
|
end
|
|
end
|
|
end
|
|
|
|
get "/snowflake" do
|
|
n = params.fetch(:n, 1).to_i
|
|
epoch = Time.new(2016, 7, 1, 0, 0, 0).strftime("%s%L").to_i
|
|
node_id = 1
|
|
af = AnyFlake.new(epoch, node_id)
|
|
stream do |out|
|
|
n.times do |_|
|
|
out << format("%s\n", af.next_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
post "/quit" do
|
|
Process.kill("TERM", ppid)
|
|
nil
|
|
end
|
|
|
|
post "/halt" do
|
|
Process.kill("QUIT", ppid)
|
|
nil
|
|
end
|
|
|
|
get "/pid", provides: "json" do
|
|
pretty = params.key? :pretty
|
|
|
|
jsonify({ppid: ppid, pid: Process.pid}, pretty:)
|
|
end
|
|
|
|
get "/token" do
|
|
exp = Time.now.to_i + SECONDS_PER_MINUTE * 2
|
|
payload = {name: "anonymous", exp: exp, jti: Random.uuid}
|
|
expires_at = Time.at(exp).to_datetime
|
|
token = JWT.encode payload, JWT_SECRET, "HS256"
|
|
x = {token: token, expires_at: expires_at}
|
|
|
|
jsonify x
|
|
end
|
|
|
|
get "/token/validate" do
|
|
token = req_headers["authorization"].split[1]
|
|
payload = JWT.decode token, JWT_SECRET, true, algorithm: "HS256"
|
|
|
|
jsonify payload
|
|
end
|
|
|
|
post "/session" do
|
|
session.merge! params
|
|
|
|
jsonify session.to_hash
|
|
end
|
|
|
|
get "/session" do
|
|
j = session.to_hash
|
|
j[:hostname] = ENV["HOSTNAME"]
|
|
|
|
jsonify j
|
|
end
|
|
|
|
get "/cookies" do
|
|
jsonify response.headers
|
|
end
|
|
|
|
get "/config", provides: "json" do
|
|
pretty = params.key? :pretty
|
|
|
|
jsonify config.as_json, pretty:
|
|
end
|
|
|
|
get "/_cat" do
|
|
stream do |out|
|
|
out << "=^.^=\n"
|
|
x = Sinatra::Application.routes.map do |method, route|
|
|
route.map do |route|
|
|
route.first.to_s
|
|
end
|
|
end
|
|
x.flatten.sort.uniq.each do |route|
|
|
out << "#{route}\n" if route.start_with? "/_cat"
|
|
end
|
|
end
|
|
end
|
|
|
|
get "/_cat/headers" do
|
|
stream do |out|
|
|
req_headers.each do |k, v|
|
|
out << "#{k}: #{v.inspect}\n"
|
|
end
|
|
end
|
|
end
|
|
|
|
get "/_cat/env" do
|
|
stream do |out|
|
|
ENV.sort.each do |k, v|
|
|
out << "#{k}=#{v}\n"
|
|
end
|
|
end
|
|
end
|
|
|
|
get "/_cat/cookies" do
|
|
stream do |out|
|
|
cookies.each do |k, v|
|
|
out << "#{k}=#{v}\n"
|
|
end
|
|
end
|
|
end
|
|
|
|
get "/_cat/config" do
|
|
stream do |out|
|
|
config.instance_variables.each do |k|
|
|
k = k.to_s.delete_prefix "@"
|
|
begin
|
|
v = config.send(k)
|
|
rescue NoMethodError
|
|
next
|
|
end
|
|
|
|
out << "#{k}(#{v.to_s.length})=#{v}\n"
|
|
end
|
|
end
|
|
end
|
|
|
|
get "/_cat/pid" do
|
|
stream do |out|
|
|
{ppid: ppid, pid: Process.pid}.sort.each do |k, v|
|
|
out << "#{k}=#{v}\n"
|
|
end
|
|
end
|
|
end
|
|
|
|
route :delete, :get, :patch, :post, :put, "/status/:code" do
|
|
# hello
|
|
code = Integer(params[:code])
|
|
status code if code.between? 100, 599
|
|
end
|
|
|
|
get "/chunked/:delay" do
|
|
content_type "application/x-ndjson"
|
|
|
|
delay = Float(params[:delay])
|
|
stream do |out|
|
|
30.times do |i|
|
|
out << jsonify({id: i, message: (i % 2).zero? ? "tick" : "tock"})
|
|
sleep delay
|
|
end
|
|
end
|
|
end
|
|
|
|
route :delete, :get, :patch, :post, :put, "/auth/basic", provides: "json" do
|
|
pretty = params.key? :pretty
|
|
|
|
if params.key? :hidden
|
|
protected! hidden: true
|
|
else
|
|
protected!
|
|
end
|
|
|
|
jsonify({authenticated: true, user: @auth.username}, pretty:)
|
|
end
|
|
|
|
def human_size_to_bytes(size)
|
|
units = %i[b kb mb gb tb pb eb zb yb rb qb]
|
|
|
|
number, unit = size.split(/(?<=\d)(?=[A-Za-z])/)
|
|
|
|
raise ArgumentError, "the unit is not recognized" if unit.nil?
|
|
|
|
number = Float(number)
|
|
unit = unit.downcase.to_sym
|
|
|
|
exponent = units.find_index(unit)
|
|
|
|
number *= (1024**exponent)
|
|
Integer(number.ceil)
|
|
end
|
|
|
|
MAX_DOWNLOAD_SIZE = "1GB"
|
|
|
|
get "/bytes/:size" do
|
|
size = params[:size]
|
|
n = [human_size_to_bytes(size), human_size_to_bytes(MAX_DOWNLOAD_SIZE)].min
|
|
|
|
headers["content-type"] = "application/octet-stream"
|
|
headers["content-length"] = n
|
|
|
|
headers["content-disposition"] = "attachment; filename=\"#{params[:f]}\"" if params.key? :f
|
|
|
|
def generate_bytes(number, byte = "\x00", block_size = 4096)
|
|
raise ArgumentError, "'byte' must be 1 byte" unless byte.b.length == 1
|
|
|
|
bytes_written = 0
|
|
block = byte * block_size
|
|
Enumerator.new do |g|
|
|
while bytes_written < number
|
|
remaining_bytes = number - bytes_written
|
|
bytes_to_write = [block_size, remaining_bytes].min
|
|
g.yield block[0, bytes_to_write]
|
|
bytes_written += bytes_to_write
|
|
end
|
|
end
|
|
end
|
|
|
|
generate_bytes(Integer(n))
|
|
end
|
|
|
|
get "/api/caas" do
|
|
send_file Dir[__dir__ + "/static/cat*.jpg"].sample
|
|
end
|
|
|
|
get "/meow" do
|
|
caas_host = ENV.fetch "CAAS_SERVICE_HOST", nil
|
|
caas_port = ENV.fetch "CAAS_SERVICE_PORT", nil
|
|
url = "http://#{caas_host}:#{caas_port}/"
|
|
unless caas_host && caas_port
|
|
url = url("/api/caas")
|
|
end
|
|
tmp_file = Tempfile.open binmode: true do |f|
|
|
# f.chmod 0o644
|
|
response = HTTParty.get(url, stream_body: true) do |fragment|
|
|
if [301, 302].include? fragment.code
|
|
print "skip writing for redirect"
|
|
elsif fragment.code == 200
|
|
f.write fragment
|
|
else
|
|
raise StandardError, "non-success status code while streaming #{fragment.code}"
|
|
end
|
|
end
|
|
|
|
content_type response.headers["content-type"]
|
|
f
|
|
end
|
|
|
|
tmp_file.open do |f|
|
|
stream do |out|
|
|
out << f.read
|
|
until f.eof?
|
|
out << f.read(CHUNK_SIZE)
|
|
end
|
|
end
|
|
ensure
|
|
f.close
|
|
f.unlink
|
|
end
|
|
end
|