8 Commits

Author SHA1 Message Date
c66d4676e3 rename project to Kubernaut
Some checks failed
Gitea Actions Demo / lint (push) Failing after 2m8s
Gitea Actions Demo / test (push) Has been skipped
Gitea Actions Demo / release-image (push) Has been skipped
2025-03-09 15:46:29 -05:00
a93cab4de5 remove namespace from kustomization
Some checks failed
Gitea Actions Demo / lint (push) Failing after 2m2s
Gitea Actions Demo / test (push) Has been skipped
Gitea Actions Demo / release-image (push) Has been skipped
2025-03-09 15:38:57 -05:00
b2e4fcbce1 add route to test chunked encoding 2025-03-09 15:38:34 -05:00
86ba2e6c1a add route to dump bytes
Some checks failed
Gitea Actions Demo / lint (push) Failing after 2m11s
Gitea Actions Demo / test (push) Has been skipped
Gitea Actions Demo / release-image (push) Has been skipped
2025-03-09 15:36:49 -05:00
3a78bf5d03 add routes to dump configuration 2025-03-09 15:36:07 -05:00
fde1dd14b5 use jsonify() helper
Some checks failed
Gitea Actions Demo / test (push) Blocked by required conditions
Gitea Actions Demo / release-image (push) Blocked by required conditions
Gitea Actions Demo / lint (push) Has been cancelled
2025-03-09 15:35:40 -05:00
a4955d35fa add configuration class 2025-03-09 15:35:40 -05:00
aa907dfa5f add class to wrap sensitive values 2025-03-09 15:35:40 -05:00
16 changed files with 263 additions and 46 deletions

View File

@ -69,4 +69,4 @@ jobs:
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
push: true push: true
tags: git.kill0.net/ryanc/kipunji:latest tags: git.kill0.net/ryanc/kubernaut:latest

111
app.rb
View File

@ -13,9 +13,14 @@ require "anyflake"
require "jwt" require "jwt"
SESSION_SECRET_HEX_LENGTH = 64 $LOAD_PATH.unshift File.dirname(__FILE__) + "/lib"
set :session_secret, ENV.fetch("SESSION_SECRET") { SecureRandom.hex(SESSION_SECRET_HEX_LENGTH) } require "config"
SESSION_SECRET_HEX_LENGTH = 64
JWT_SECRET_HEX_LENGTH = 64
ENV_PREFIX = "KUBERNAUT"
CLK_TCK = 100 CLK_TCK = 100
PID_FILE_PATH = "/run/app/pid".freeze PID_FILE_PATH = "/run/app/pid".freeze
@ -38,7 +43,9 @@ DURATION_PARTS = [
[1, "second", "s"] [1, "second", "s"]
].freeze ].freeze
JWT_SECRET = SecureRandom.bytes(64).freeze config = Config.new
set :session_secret, config.session_secret.unwrap
module Sinatra module Sinatra
module RequestHeadersHelper module RequestHeadersHelper
@ -209,12 +216,13 @@ before do
end end
helpers do helpers do
def json(obj, opts: nil, pretty: false) def jsonify(obj, opts: nil, pretty: false)
if pretty buf = if pretty
JSON.pretty_generate obj, opts: JSON.pretty_generate obj, opts:
else else
JSON.generate(obj, opts:) JSON.generate(obj, opts:)
end end
"#{buf}\n"
end end
def protected! hidden = false def protected! hidden = false
@ -244,14 +252,14 @@ end
get "/env", provides: "json" do get "/env", provides: "json" do
pretty = params.key? :pretty pretty = params.key? :pretty
json ENV.sort.to_h, pretty: jsonify ENV.sort.to_h, pretty:
end end
get "/headers", provides: "json" do get "/headers", provides: "json" do
pretty = params.key? :pretty pretty = params.key? :pretty
h = req_headers h = req_headers
json h, pretty: jsonify h, pretty:
end end
get "/livez" do get "/livez" do
@ -266,7 +274,7 @@ get "/livez/uptime" do
tt = TickTock.new tt = TickTock.new
x = {started_at: tt.started_at, seconds: tt.uptime.to_i, human: human_time(tt.uptime.to_i)} x = {started_at: tt.started_at, seconds: tt.uptime.to_i, human: human_time(tt.uptime.to_i)}
json x jsonify x
end end
post "/livez/toggle" do post "/livez/toggle" do
@ -330,7 +338,7 @@ end
get "/pid" do get "/pid" do
pretty = params.key? :pretty pretty = params.key? :pretty
json({puma: master_pid, pid: Process.pid}, pretty:) jsonify({puma: master_pid, pid: Process.pid}, pretty:)
end end
get "/token" do get "/token" do
@ -340,31 +348,37 @@ get "/token" do
token = JWT.encode payload, JWT_SECRET, "HS256" token = JWT.encode payload, JWT_SECRET, "HS256"
x = {token: token, expires_at: expires_at} x = {token: token, expires_at: expires_at}
json x jsonify x
end end
get "/token/validate" do get "/token/validate" do
token = req_headers["authorization"].split[1] token = req_headers["authorization"].split[1]
payload = JWT.decode token, JWT_SECRET, true, algorithm: "HS256" payload = JWT.decode token, JWT_SECRET, true, algorithm: "HS256"
json payload jsonify payload
end end
post "/session" do post "/session" do
session.merge! params session.merge! params
json session.to_hash jsonify session.to_hash
end end
get "/session" do get "/session" do
j = session.to_hash j = session.to_hash
j[:hostname] = ENV["HOSTNAME"] j[:hostname] = ENV["HOSTNAME"]
json j jsonify j
end end
get "/cookies" do get "/cookies" do
json response.headers jsonify response.headers
end
get "/config", provides: "json" do
pretty = params.key? :pretty
jsonify config.as_json, pretty:
end end
get "/_cat/headers" do get "/_cat/headers" do
@ -391,6 +405,21 @@ get "/_cat/cookies" do
end 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
route :delete, :get, :patch, :post, :put, "/status/:code" do route :delete, :get, :patch, :post, :put, "/status/:code" do
# hello # hello
code = Integer(params[:code]) code = Integer(params[:code])
@ -398,11 +427,14 @@ route :delete, :get, :patch, :post, :put, "/status/:code" do
end end
get "/chunked/:delay" do get "/chunked/:delay" do
content_type "application/x-ndjson"
delay = Float(params[:delay]) delay = Float(params[:delay])
stream do |out| stream do |out|
out << "Hello, world!\n" 30.times do |i|
out << jsonify({id: i, message: i % 2 == 0 ? "tick" : "tock"})
sleep delay sleep delay
out << "Hello, world!\n" end
end end
end end
@ -415,5 +447,50 @@ route :delete, :get, :patch, :post, :put, "/auth/basic", provides: "json" do
protected! protected!
end end
json({authenticated: true, user: @auth.username}, pretty:) 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 end

View File

@ -2,7 +2,7 @@
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: kipunji-configmap name: kubernaut-configmap
namespace: kipunji namespace: kubernaut
data: data:
KIPUNJI_CAT: kilwin KUBERNAUT_CAT: kilwin

View File

@ -2,22 +2,22 @@
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: kipunji name: kubernaut
annotations: annotations:
reloader.stakater.com/auto: "true" reloader.stakater.com/auto: "true"
spec: spec:
replicas: 5 replicas: 5
selector: selector:
matchLabels: matchLabels:
app: kipunji app: kubernaut
template: template:
metadata: metadata:
labels: labels:
app: kipunji app: kubernaut
spec: spec:
containers: containers:
- name: kipunji - name: kubernaut
image: git.kill0.net/ryanc/kipunji:latest image: git.kill0.net/ryanc/kubernaut:latest
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- name: sinatra-web - name: sinatra-web
@ -26,12 +26,12 @@ spec:
- name: SESSION_SECRET - name: SESSION_SECRET
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: kipunji-session-secret name: kubernaut-session-secret
key: session_secret key: session_secret
optional: true optional: true
envFrom: envFrom:
- configMapRef: - configMapRef:
name: kipunji-configmap name: kubernaut-configmap
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /livez path: /livez

View File

@ -1,7 +1,7 @@
--- ---
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
namespace: kipunji namespace: kubernaut
resources: resources:
- secret.yaml - secret.yaml
- configmap.yaml - configmap.yaml

View File

@ -3,13 +3,13 @@ apiVersion: bitnami.com/v1alpha1
kind: SealedSecret kind: SealedSecret
metadata: metadata:
creationTimestamp: null creationTimestamp: null
name: kipunji-session-secret name: kubernaut-session-secret
namespace: kipunji namespace: kubernaut
spec: spec:
encryptedData: encryptedData:
session_secret: AgCY08t0AU418znEZt5d252J+lH+fwYki2g6jdJpfdRfVQjnA+b52P0KWrs/x5pB0PKab6Z3JY/Tz0SQCaoIsCR4IzUO3a095aulRqb6Qr1Lz8udBVta4JJMZLmo26tuUfVHlpD1d6J8rkBSm8vzckFLkOA1Wfl/9rS3K4qwiDogA5pI0ULghFkeEx1yKdRwPq0k8PuvOvLUJ6oNq3e5n+B/BrVWdQ+7XQxUq/AMANJrDbe+RD33f99LArHYA7bFMbY8YRazXSTAkeunpTlxTjuGZKYvJKupo29LHz2OVbZVX/hI0nZkdVpcgqvbxF6Vw9CuCeAmtKYl7A3qsAWqDLUdP3hRLsk2P9RDNhEzYWh4ml8APzziWzihdJbGEjwLy7HsHgKslM0XbBnRQDlxp/JtvcWdjQp33A+QOON32zOKHi+qJjDYyGebS1+xkPbnyb1MPSJVAtFpj7dlLbFekLFDZEbXuJYUl1wKdFOIjJHmNK/MTEV2kOhtiVj/aeKgSXwor9hR7Uxzs5ZSawp9uWw+hpr58EX6I+RtfO4yjFC6FjnagiU6SlI1Q2F7/nv82g1UWTYMpNN5bduS1YFWmsnXvK+W7YQHpSForr5ndtCSHmclbXb5Fc33sywC5u6Bi2Gu5/MW6d73BOog5BC3QtOuEQ044Q+cuU3RIlKADBqKLzZmHlmukyyGuZfXJnGjlWGKp3J1KecucTo6XC9QHpUkjXEKdlE63mOI1VuOGyBIHl60v4bnWiBg+aDZVHipz4JLKsVB0HOgBBK7+tOX6tr1GDG/F7Nz/i9ebzUV6i8Ec1jHf+2ZcTtBkNXBIkHc84+4Qd33/gOuP+lizLfIhfQ3DFWbwyfYumpVbeapyYhB0CE= session_secret: AgCY08t0AU418znEZt5d252J+lH+fwYki2g6jdJpfdRfVQjnA+b52P0KWrs/x5pB0PKab6Z3JY/Tz0SQCaoIsCR4IzUO3a095aulRqb6Qr1Lz8udBVta4JJMZLmo26tuUfVHlpD1d6J8rkBSm8vzckFLkOA1Wfl/9rS3K4qwiDogA5pI0ULghFkeEx1yKdRwPq0k8PuvOvLUJ6oNq3e5n+B/BrVWdQ+7XQxUq/AMANJrDbe+RD33f99LArHYA7bFMbY8YRazXSTAkeunpTlxTjuGZKYvJKupo29LHz2OVbZVX/hI0nZkdVpcgqvbxF6Vw9CuCeAmtKYl7A3qsAWqDLUdP3hRLsk2P9RDNhEzYWh4ml8APzziWzihdJbGEjwLy7HsHgKslM0XbBnRQDlxp/JtvcWdjQp33A+QOON32zOKHi+qJjDYyGebS1+xkPbnyb1MPSJVAtFpj7dlLbFekLFDZEbXuJYUl1wKdFOIjJHmNK/MTEV2kOhtiVj/aeKgSXwor9hR7Uxzs5ZSawp9uWw+hpr58EX6I+RtfO4yjFC6FjnagiU6SlI1Q2F7/nv82g1UWTYMpNN5bduS1YFWmsnXvK+W7YQHpSForr5ndtCSHmclbXb5Fc33sywC5u6Bi2Gu5/MW6d73BOog5BC3QtOuEQ044Q+cuU3RIlKADBqKLzZmHlmukyyGuZfXJnGjlWGKp3J1KecucTo6XC9QHpUkjXEKdlE63mOI1VuOGyBIHl60v4bnWiBg+aDZVHipz4JLKsVB0HOgBBK7+tOX6tr1GDG/F7Nz/i9ebzUV6i8Ec1jHf+2ZcTtBkNXBIkHc84+4Qd33/gOuP+lizLfIhfQ3DFWbwyfYumpVbeapyYhB0CE=
template: template:
metadata: metadata:
creationTimestamp: null creationTimestamp: null
name: kipunji-session-secret name: kubernaut-session-secret
namespace: kipunji namespace: kubernaut

View File

@ -2,11 +2,11 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: kipunji name: kubernaut
spec: spec:
ports: ports:
- name: web - name: web
port: 80 port: 80
targetPort: sinatra-web targetPort: sinatra-web
selector: selector:
app: kipunji app: kubernaut

View File

@ -2,8 +2,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
metadata: metadata:
name: kipunji name: kubernaut
namespace: kipunji
resources: resources:
- namespace.yaml - namespace.yaml
- ./app - ./app

View File

@ -2,19 +2,19 @@
kind: Deployment kind: Deployment
apiVersion: apps/v1 apiVersion: apps/v1
metadata: metadata:
name: kipunji-memcached name: kubernaut-memcached
spec: spec:
selector: selector:
matchLabels: matchLabels:
app: kipunji-memcached app: kubernaut-memcached
template: template:
metadata: metadata:
labels: labels:
app: kipunji-memcached app: kubernaut-memcached
spec: spec:
containers: containers:
- name: kipunji-memcached - name: kubernaut-memcached
image: memcached:latest image: memcached:latest
ports: ports:
- name: memcached - name: memcached

View File

@ -1,7 +1,7 @@
--- ---
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
namespace: kipunji namespace: kubernaut
resources: resources:
- deployment.yaml - deployment.yaml
- services.yaml - services.yaml

View File

@ -2,7 +2,7 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: kipunji-memcached name: kubernaut-memcached
spec: spec:
ports: ports:
@ -10,4 +10,4 @@ spec:
port: 11211 port: 11211
targetPort: memcached targetPort: memcached
selector: selector:
app: kipunji-memcached app: kubernaut-memcached

View File

@ -2,6 +2,6 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: kipunji name: kubernaut
labels: {} labels: {}

50
lib/config.rb Normal file
View File

@ -0,0 +1,50 @@
require "sensitive"
class Config
attr_accessor :cat
attr_reader :jwt_secret, :session_secret
def initialize(prefix = ENV_PREFIX, jwt_secret = nil, session_secret = nil, cat = nil)
@prefix = prefix
@cat = cat
session_secret ||= ENV.fetch "SESSION_SECRET" do
SecureRandom.hex SESSION_SECRET_HEX_LENGTH
end
jwt_secret ||= fetch_env "JWT_SECRET" do
SecureRandom.hex JWT_SECRET_HEX_LENGTH
end
@session_secret = Sensitive.new session_secret
@jwt_secret = Sensitive.new jwt_secret
@cat ||= ENV.fetch "#{@prefix}_CAT", nil
end
def fetch_env(name, &)
ENV.fetch "#{@prefix}_#{name}", &
end
def as_json(options = nil)
{jwt_secret: jwt_secret, session_secret: @session_secret, cat: @cat}
end
def to_json(options = nil)
if options &&
options.key?(:pretty) &&
options[:pretty] == true
JSON.pretty_generate as_json(options)
else
JSON.generate as_json(options)
end
end
def session_secret=(v)
@session_secret = Sensitive.new v
end
def jwt_secret=(v)
@jwt_secret = Sensitive.new v
end
end

39
lib/sensitive.rb Normal file
View File

@ -0,0 +1,39 @@
class Sensitive
alias_method :eql?, :==
alias_method :equal?, :==
def initialize(v, ch: "*", head: 2, tail: 2)
@v = v
@ch = ch
@head = head
@tail = tail
end
def mask(v)
"".concat(v[0, @head], @ch * (v.length - (@head + @tail)), v[-@tail, @tail])
end
def unwrap
@v
end
def length
@v.length
end
def to_s
mask @v
end
def inspect
"#<#{self.class.name} @v=#{wrap}>"
end
def hash
@v.hash
end
def ==(other)
other.is_a?(Sensitive) && other.hash == hash
end
end

26
spec/sensitive_spec.rb Normal file
View File

@ -0,0 +1,26 @@
require "minitest/autorun"
$LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
require "sensitive"
ALPHABET = ('a' .. 'z').reduce(:concat)
describe "Sensitive" do
before do
@s = Sensitive.new ALPHABET
end
it "test initialize" do
_(@s.to_s).must_equal "ab" + "*" * 22 + "yz"
end
it "test initialize" do
_(@s.unwrap).must_equal ALPHABET
end
it "test using different mask character" do
s = Sensitive.new ALPHABET, ch: "x"
_(s.to_s).must_equal "x"
end
end

26
test/test_sensitive.rb Normal file
View File

@ -0,0 +1,26 @@
require "minitest/autorun"
$LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
require "sensitive"
ALPHABET = ("a".."z").reduce(:concat)
class TestSensitive < Minitest::Test
def setup
@s = Sensitive.new ALPHABET
end
def test_initialize
assert_equal @s.to_s, "ab" + "*" * 22 + "yz"
end
def test_unwrap
assert_equal @s.unwrap, ALPHABET
end
def test_using_a_different_mask_character
s = Sensitive.new ALPHABET, ch: "x"
assert_equal s.to_s, "ab" + "x" * 22 + "yz"
end
end