first commit

This commit is contained in:
Ryan Cavicchioni 2024-07-02 15:22:32 -05:00
commit e2b0cf9794
Signed by: ryanc
GPG Key ID: 877EEDAF9245103D
9 changed files with 594 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.bundle
.cache
.local
.ruby-lsp
.ash_history

48
Dockerfile Normal file
View File

@ -0,0 +1,48 @@
FROM ruby:alpine as base
WORKDIR /app
RUN <<EOT
gem update --system --no-document
gem install -N bundler
apk update
apk upgrade --no-cache
EOT
FROM base AS build
RUN <<EOT
apk add gcc musl-dev ruby-dev make
EOT
COPY Gemfile* .
RUN <<EOT
bundle config set --local without development
bundle install
EOT
FROM build as dev
WORKDIR /app
RUN <<EOT
bundle install
EOT
CMD [ "sleep", "infinity" ]
FROM base
# RUN useradd ruby --home /app --shell /bin/sh
RUN adduser ruby -h /app -D
USER ruby:ruby
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build --chown=ruby:ruby /app /app
COPY --chown=ruby:ruby . .
EXPOSE 4567
CMD [ "bundle", "exec", "rackup", "--host", "0.0.0.0", "--port", "4567" ]

20
Gemfile Normal file
View File

@ -0,0 +1,20 @@
source "https://rubygems.org"
gem "sinatra"
gem 'sinatra-contrib'
gem "puma"
gem "rackup"
gem "anyflake"
gem "ksuid"
gem "nanoid"
gem "ulid"
gem "uuid7"
gem 'jwt'
group :development do
gem "ruby-lsp"
gem "rubocop"
gem "rbs"
end

103
Gemfile.lock Normal file
View File

@ -0,0 +1,103 @@
GEM
remote: https://rubygems.org/
specs:
anyflake (0.0.1)
ast (2.4.2)
base64 (0.2.0)
json (2.7.2)
jwt (2.8.2)
base64
ksuid (1.0.0)
language_server-protocol (3.17.0.3)
logger (1.6.0)
multi_json (1.15.0)
mustermann (3.0.0)
ruby2_keywords (~> 0.0.1)
nanoid (2.0.0)
nio4r (2.7.3)
parallel (1.25.1)
parser (3.3.3.0)
ast (~> 2.4.1)
racc
prism (0.30.0)
puma (6.4.2)
nio4r (~> 2.0)
racc (1.8.0)
rack (3.1.3)
rack-protection (4.0.0)
base64 (>= 0.1.0)
rack (>= 3.0.0, < 4)
rack-session (2.0.0)
rack (>= 3.0.0)
rackup (2.1.0)
rack (>= 3)
webrick (~> 1.8)
rainbow (3.1.1)
rbs (3.5.1)
logger
regexp_parser (2.9.2)
rexml (3.3.0)
strscan
rubocop (1.64.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.3)
parser (>= 3.3.1.0)
ruby-lsp (0.17.3)
language_server-protocol (~> 3.17.0)
prism (>= 0.29.0, < 0.31)
rbs (>= 3, < 4)
sorbet-runtime (>= 0.5.10782)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
sinatra (4.0.0)
mustermann (~> 3.0)
rack (>= 3.0.0, < 4)
rack-protection (= 4.0.0)
rack-session (>= 2.0.0, < 3)
tilt (~> 2.0)
sinatra-contrib (4.0.0)
multi_json (>= 0.0.2)
mustermann (~> 3.0)
rack-protection (= 4.0.0)
sinatra (= 4.0.0)
tilt (~> 2.0)
sorbet-runtime (0.5.11435)
strscan (3.1.0)
tilt (2.3.0)
ulid (1.4.0)
unicode-display_width (2.5.0)
uuid7 (0.2.0)
zeitwerk (~> 2.4)
webrick (1.8.1)
zeitwerk (2.6.15)
PLATFORMS
ruby
x86_64-linux-musl
DEPENDENCIES
anyflake
jwt
ksuid
nanoid
puma
rackup
rbs
rubocop
ruby-lsp
sinatra
sinatra-contrib
ulid
uuid7
BUNDLED WITH
2.5.13

331
app.rb Normal file
View File

@ -0,0 +1,331 @@
require "bundler/setup"
require "sinatra"
require "sinatra/json"
require "sinatra/cookies"
require "time"
require "fileutils"
require "json"
require "securerandom"
require "random/formatter"
require "ulid"
require "anyflake"
require "jwt"
SESSION_SECRET_HEX_LENGTH = 64
set :session_secret, ENV.fetch('SESSION_SECRET') { SecureRandom.hex(SESSION_SECRET_HEX_LENGTH) }
CLK_TCK = 100
PID_FILE_PATH = "/run/pid".freeze
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
JWT_SECRET = SecureRandom.bytes(64).freeze
module Sinatra
module RequestHeadersHelper
def req_headers
hash = request.env.select { |k, _| k.start_with? "HTTP_" }
.collect { |k, v| [k.gsub(/^HTTP_/, "").gsub(/_/, "-").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 = master_pid
@procfs_f = format "/proc/%s/stat", @pid
puts @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 = "./healthy"
end
def healthy?
enabled?
end
end
class Ready
include Singleton
include State
include UpDown
def initialize
@file = "./ready"
end
def ready?
enabled?
end
end
class Sleep
include Singleton
include State
def initialize
@file = "./sleep"
end
def asleep?
enabled?
end
def wake
disable
end
def sleep
enable
end
end
def master_pid
pid_s = File.read PID_FILE_PATH
Integer pid_s.strip
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
enable :sessions
configure do
mime_type :json, "application/json"
end
before do
# content_type 'text/plain'
sleep(1) while Sleep.instance.asleep? unless request.path_info == "/livez/sleep"
end
get "/" do
"hello there!\n"
end
get "/env" do
stream do |out|
ENV.sort.each do |k, v|
out << "#{k}=#{v}\n"
end
end
end
get "/headers", provides: "json" do
h = req_headers
return JSON.pretty_generate h if params.key? "pretty"
JSON.generate h
end
get "/headers" do
stream do |out|
req_headers.each do |k, v|
out << "#{k}: #{v.inspect}\n"
end
end
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 "/livez/uptime" do
tt = TickTock.new
x = { started_at: tt.started_at, seconds: tt.uptime.to_i, human: human_time(tt.uptime.to_i) }
json x
end
post "/livez/toggle" do
Health.instance.toggle
"ok\n"
end
post "/livez/sleep" do
Sleep.instance.toggle
"ok\n"
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", master_pid)
nil
end
post "/halt" do
Process.kill("QUIT", master_pid)
nil
end
get "/pid" do
JSON.generate({ puma: master_pid, pid: Process.pid })
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"
token
x = { :token => token, :expires_at => expires_at }
json x
end
get "/token/validate" do
token = req_headers["authorization"].split[1]
payload = JWT.decode token, JWT_SECRET, true, algorithm: "HS256"
json payload
end
post "/session" do
session.merge! params
json session.to_hash
end
get "/session" do
json session.to_hash
end
get "/cookies" do
json response.headers
end

6
config.ru Normal file
View File

@ -0,0 +1,6 @@
require 'bundler/setup'
require 'sinatra'
require './app'
run Sinatra::Application

3
config/puma.rb Normal file
View File

@ -0,0 +1,3 @@
# workers 3
pidfile '/run/pid'
preload_app!

15
docker-compose.yml Normal file
View File

@ -0,0 +1,15 @@
services:
web:
build:
context: .
target: dev
ports:
- "4567:4567"
volumes:
- .:/app
environment:
{}
# WEB_CONCURRENCY: 3
command:
- sleep
- infinity

63
sig/app.rbs Normal file
View File

@ -0,0 +1,63 @@
module State
def enable: () -> untyped
def disable: () -> (untyped | nil)
def enabled?: () -> bool
def toggle: () -> untyped
end
module UpDown
def up: () -> untyped
def down: () -> untyped
def to_s: () -> ("up" | "down")
def to_json: (*untyped _args) -> (nil | untyped)
end
class Health
@file: untyped
include Singleton
include State
include UpDown
def initialize: () -> void
def healthy?: () -> bool
end
class Ready
@file: untyped
include Singleton
include State
include UpDown
def initialize: () -> void
def ready?: () -> bool
end
class Sleep
@file: untyped
include Singleton
include State
def initialize: () -> void
def asleep?: () -> bool
def wake: () -> untyped
def sleep: () -> untyped
end