Initial commit

This commit is contained in:
Yaser
2026-08-13 19:42:08 +03:30
commit d59bf30ef0
173 changed files with 11702 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
# frozen_string_literal: true
require 'yaml'
module Opendax
# Loads config/app.yml and deep-merges an environment overlay when present.
#
# Overlay resolution (first match wins):
# 1. OPENDAX_ENV=local|staging|production → config/app.{env}.yml
# 2. If OPENDAX_ENV unset and config/app.local.yml exists → use local overlay
#
# Never commit real secrets to config/app.yml — use app.local.yml or app.production.yml.
module ConfigLoader
BASE_PATH = 'config/app.yml'
module_function
def load
base = load_yaml_hash!(BASE_PATH, 'base config')
overlay_path = resolve_overlay_path
return base unless overlay_path && File.exist?(overlay_path)
overlay = load_yaml_hash!(overlay_path, 'overlay config')
deep_merge(base, overlay)
end
def load_yaml_hash!(path, label)
data = YAML.load_file(path)
return data if data.is_a?(Hash)
raise TypeError,
"#{label} #{path} must be a YAML mapping (Hash), got #{data.class}. " \
'Check syntax — quote values that contain colons (e.g. database passwords).'
end
def resolve_overlay_path
env = ENV['OPENDAX_ENV'].to_s.strip
if env.empty?
return 'config/app.local.yml' if File.exist?('config/app.local.yml')
return nil
end
"config/app.#{env}.yml"
end
def active_overlay
path = resolve_overlay_path
path if path && File.exist?(path)
end
def deep_merge(base, overlay)
unless base.is_a?(Hash) && overlay.is_a?(Hash)
raise TypeError,
"deep_merge expects Hash + Hash, got #{base.class} + #{overlay.class}"
end
base.merge(overlay) do |key, old_val, new_val|
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
deep_merge(old_val, new_val)
elsif old_val.is_a?(Hash) && !new_val.is_a?(Hash)
raise TypeError,
"Config type mismatch at #{key}: base has Hash but overlay has " \
"#{new_val.class}. Quote scalar values in app.staging.yml (passwords with ':' need quotes)."
else
new_val
end
end
end
end
end

46
lib/opendax/payload.rb Normal file
View File

@@ -0,0 +1,46 @@
require 'jwt'
module Opendax
class Error < StandardError
end
class Payload
def initialize(params)
@secret = params[:secret]
@expire = params[:expire] || 600
raise Opendax::Error.new unless @secret
raise Opendax::Error.new unless @expire > 0
end
def generate!(params)
raise Opendax::Error.new unless params[:service]
raise Opendax::Error.new unless params[:image]
token = params.merge({
'iat': Time.now.to_i,
'exp': (Time.now + @expire).to_i
})
JWT.encode token, @secret, 'HS256'
end
def decode!(token)
JWT.decode(token, @secret, true, {
algorithm: 'HS256'
}).first
end
def safe_decode(token)
begin
decode!(token)
rescue JWT::ExpiredSignature
rescue JWT::ImmatureSignature
rescue JWT::VerificationError
rescue JWT::DecodeError
nil
end
end
end
end

79
lib/opendax/renderer.rb Normal file
View File

@@ -0,0 +1,79 @@
# frozen_string_literal: true
require 'openssl'
require 'sshkey'
require 'pathname'
require 'yaml'
require 'base64'
require 'fileutils'
module Opendax
# Renderer is class for rendering Opendax templates.
class Renderer
TEMPLATE_PATH = Pathname.new('./templates')
BARONG_KEY = 'config/secrets/barong.key'
APPLOGIC_KEY = 'config/secrets/applogic.key'
SSH_KEY = 'config/secrets/app.key'
def render
@config ||= config
@utils ||= utils
@name ||= @config['app']['name'].downcase
@barong_key ||= OpenSSL::PKey::RSA.new(File.read(BARONG_KEY), '')
@applogic_key ||= OpenSSL::PKey::RSA.new(File.read(APPLOGIC_KEY), '')
@barong_private_key ||= Base64.urlsafe_encode64(@barong_key.to_pem)
@barong_public_key ||= Base64.urlsafe_encode64(@barong_key.public_key.to_pem)
@applogic_private_key ||= Base64.urlsafe_encode64(@applogic_key.to_pem)
@applogic_public_key ||= Base64.urlsafe_encode64(@applogic_key.public_key.to_pem)
Dir.glob("#{TEMPLATE_PATH}/**/*.erb", File::FNM_DOTMATCH).each do |file|
output_file = template_name(file)
FileUtils.chmod 0o644, output_file if File.exist?(output_file)
render_file(file, output_file)
FileUtils.chmod 0o444, output_file if @config['render_protect']
end
end
def render_file(file, out_file)
puts "Rendering #{out_file}"
result = ERB.new(File.read(file), trim_mode: '-').result(binding)
dir = File.dirname(out_file)
FileUtils.mkdir(dir) unless Dir.exist?(dir)
File.write(out_file, result)
end
def ssl_helper(arg)
@config['ssl']['enabled'] ? arg << 's' : arg
end
def template_name(file)
path = Pathname.new(file)
out_path = path.relative_path_from(TEMPLATE_PATH).sub('.erb', '')
File.join('.', out_path)
end
def render_keys
generate_key(BARONG_KEY)
generate_key(APPLOGIC_KEY)
generate_key(SSH_KEY, public: true)
end
def generate_key(filename, public: false)
return if File.file?(filename)
key = SSHKey.generate(type: 'RSA', bits: 2048)
File.write(filename, key.private_key)
File.write("#{filename}.pub", key.ssh_public_key) if public
end
def config
Opendax::ConfigLoader.load
end
def utils
YAML.load_file('./config/utils.yml')
end
end
end

81
lib/opendax/vault.rb Normal file
View File

@@ -0,0 +1,81 @@
# frozen_string_literal: true
module Opendax
class Vault
POLICIES_NAMES = ["barong", "finex_engine", "peatio_rails", "peatio_crypto", "peatio_upstream", "peatio_matching"]
def vault_secrets_path
'config/vault-secrets.yml'
end
def vault_exec(command)
`docker-compose exec -T vault sh -c '#{command}'`
end
def secrets(command, endpoints, options = '')
endpoints.each { |endpoint| vault_exec("vault secrets #{command} #{options} #{endpoint}") }
end
def setup
puts '----- Checking Vault status -----'
vault_status = YAML.safe_load(vault_exec('vault status -format yaml'))
return if vault_status.nil?
if vault_status['initialized']
puts '----- Vault is initialized -----'
begin
vault_secrets = YAML.safe_load(File.read(vault_secrets_path))
rescue SystemCallError => e
puts 'Vault keys are missing'
return
end
vault_root_token = vault_secrets['root_token']
unseal_keys = vault_secrets['unseal_keys_b64'][0, 3]
else
puts '----- Initializing Vault -----'
vault_init_output = YAML.safe_load(vault_exec('vault operator init -format yaml --recovery-shares=3 --recovery-threshold=2'))
File.write(vault_secrets_path, YAML.dump(vault_init_output))
vault_root_token = vault_init_output['root_token']
unseal_keys = vault_init_output['unseal_keys_b64'][0, 3]
end
if vault_status['sealed']
puts '----- Unsealing Vault -----'
unseal_keys.each { |key| vault_exec("vault operator unseal #{key}") }
else
puts '----- Vault is unsealed -----'
end
return vault_root_token if vault_status['initialized']
puts '----- Vault login -----'
vault_exec("vault login #{vault_root_token}")
puts '----- Configuring the endpoints -----'
secrets('enable', %w[totp transit])
secrets('disable', ['secret'])
secrets('enable', ['kv'], '-path=secret -version=1')
vault_root_token
end
def load_policies(deployment_name, vault_root_token)
puts '----- Vault login -----'
vault_exec("vault login #{vault_root_token}")
tokens = {}
POLICIES_NAMES.each do |policy|
name = "#{deployment_name.downcase}_#{policy}"
puts "Loading #{name} policy..."
vault_exec("vault policy write #{name} /tmp/policies/#{policy}.hcl")
puts "Creating the #{name} token..."
vault_token_create_output = YAML.safe_load(vault_exec("vault token create -policy=#{name} -renewable=true -ttl=240h -period=240h -format=yaml"))
tokens["#{policy}_token"] = vault_token_create_output["auth"]["client_token"]
end
tokens
end
end
end

74
lib/opendax/webhook.rb Normal file
View File

@@ -0,0 +1,74 @@
require 'sinatra/base'
require 'json'
require 'yaml'
require_relative 'payload'
require_relative 'renderer'
class Webhook < Sinatra::Base
CONFIG_PATH = 'config/app.yml'.freeze
set :show_exceptions, false
def initialize
super
@services = %w[barong peatio frontend AdminTower applogic finex-engine finex-api]
secret = ENV['WEBHOOK_JWT_SECRET']
raise 'WEBHOOK_JWT_SECRET is not set' if secret.to_s.empty?
@decoder = Opendax::Payload.new(secret: secret)
end
def update_config(service, image)
config = YAML.load_file(CONFIG_PATH)
config["images"][service] = image
File.open(CONFIG_PATH, 'w') {|f| f.write config.to_yaml }
end
before do
content_type 'application/json'
end
get '/deploy/ping' do
'pong'
end
get '/deploy/:token' do |token|
decoded = @decoder.safe_decode(token)
return answer(400, 'invalid token') unless decoded
service = decoded['service']
image = decoded['image']
return answer(400, 'service is not specified') unless service
return answer(400, 'image is not specified') unless image
return answer(404, 'unknown service') unless @services.include? service
return answer(400, 'invalid image') if (%r(^(([-_\w\.]){,20}(\/|:))+([-\w\.]{,20})$) =~ image) == nil
system "docker image pull #{image}"
unless $?.success?
system("docker image inspect #{image} > /dev/null")
return answer(404, 'invalid image') unless $?.success?
end
if $?.success?
update_config(service, image)
renderer = Opendax::Renderer.new
renderer.render
system "docker-compose up -Vd #{service}"
end
return answer(500, 'could not restart container') unless $?.success?
return answer(200, "service #{service} updated with image #{image}")
end
def answer(response_status, message)
status response_status
{
message: message
}.to_json
end
end