diff --git a/Dockerfile.ruby b/Dockerfile.ruby new file mode 100644 index 0000000..4965402 --- /dev/null +++ b/Dockerfile.ruby @@ -0,0 +1,11 @@ +FROM ruby:3.3-slim + +WORKDIR /kenaito-conf-ruby-client-demo + +# Copy client library +COPY kenaito-conf-ruby-client/ /kenaito-conf-ruby-client/ + +# Copy demo +COPY kenaito-conf-ruby-client-demo/app.rb /kenaito-conf-ruby-client-demo/app.rb + +ENTRYPOINT ["ruby", "app.rb"] diff --git a/README.md b/README.md index f27cc22..7998049 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - **后端**: Go + Gin + xorm + MySQL - **前端**: Vue 2 + Vite 5 + Element UI 2 -- **客户端**: Go / Java(Spring Boot 2.x)/ Python / Node.js / Perl / Lua / PHP / .NET / C / C++ / Rust / Ruby +- **客户端**: Go / Java(Spring Boot 2.x)/ Python / Node.js / Perl / Lua / PHP / .NET / C++ / Rust / Ruby ## 项目结构 @@ -35,7 +35,9 @@ kenaito-conf/ ├── kenaito-conf-cpp-client/ # C++ 客户端 SDK(C++11+) ├── kenaito-conf-cpp-client-demo/ # C++ 客户端示例 ├── kenaito-conf-rust-client/ # Rust 客户端 SDK(Rust 2021+) -└── kenaito-conf-rust-client-demo/ # Rust 客户端示例 +├── kenaito-conf-rust-client-demo/ # Rust 客户端示例 +├── kenaito-conf-ruby-client/ # Ruby 客户端 SDK(Ruby 2.5+) +└── kenaito-conf-ruby-client-demo/ # Ruby 客户端示例 ``` ## 快速开始 @@ -423,6 +425,59 @@ $client->syncOnce(); $client->stop(); ``` +### Ruby 客户端 + +适用于 Ruby 2.5+ 项目。 + +**1. 安装** + +```bash +cd kenaito-conf-ruby-client +gem build kenaito-conf.gemspec +gem install kenaito-conf-1.0.3.gem +``` + +或在项目 `Gemfile` 中引入: + +```ruby +gem 'kenaito-conf', path: '/path/to/kenaito-conf-ruby-client' +``` + +**2. 使用** + +```ruby +require 'kenaito_conf' + +config = Kenaito::Conf::Config.new( + base_url: 'http://localhost:8080', + app_name: 'my-app', + env: 'daily', + sync_interval: 10, +) + +client = Kenaito::Conf::Client.new(config) + +# 监听配置变更 +client.on_change do |changed| + changed.each do |key, value| + puts "config changed: #{key} = #{value}" + end +end + +# 启动客户端(拉取配置) +client.start + +# 获取配置 +value = client.get('database.host') +all_configs = client.get_all + +# 手动同步一次 +client.sync_once + +# 停止 +client.stop_client +``` + ## API 概览 ### 管理端 API(需 JWT 认证) diff --git a/README_DOCKERFILE.md b/README_DOCKERFILE.md index 66505f8..2d90fe6 100644 --- a/README_DOCKERFILE.md +++ b/README_DOCKERFILE.md @@ -192,11 +192,27 @@ docker run -it --rm \ -e KENAITO_APP_NAME=kenaito-springboot-demo \ -e KENAITO_ENV=daily \ kenaito-conf-rust-demo - + # windows docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-rust-demo ``` +## Ruby 客户端 + +```bash +docker build -f Dockerfile.ruby -t kenaito-conf-ruby-demo . + +# linux or mac +docker run -it --rm \ + -e KENAITO_BASE_URL=http://host.docker.internal:8080 \ + -e KENAITO_APP_NAME=kenaito-springboot-demo \ + -e KENAITO_ENV=daily \ + kenaito-conf-ruby-demo + +# windows +docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-ruby-demo +``` + ## 环境变量说明 | 变量名 | 说明 | 默认值 | diff --git a/README_EN.md b/README_EN.md index b36aa70..ba5cb32 100644 --- a/README_EN.md +++ b/README_EN.md @@ -6,7 +6,7 @@ A centralized configuration management platform rebuilt with Go, supporting mult - **Backend**: Go + Gin + xorm + MySQL - **Frontend**: Vue 2 + Vite 5 + Element UI 2 -- **Clients**: Go / Java (Spring Boot 2.x) / Python / Node.js / Perl / Lua / PHP / .NET / C / C++ / Rust / Ruby +- **Clients**: Go / Java (Spring Boot 2.x) / Python / Node.js / Perl / Lua / PHP / .NET / C++ / Rust / Ruby ## Project Structure @@ -33,7 +33,9 @@ kenaito-conf/ ├── kenaito-conf-cpp-client/ # C++ client SDK (C++11+) ├── kenaito-conf-cpp-client-demo/ # C++ client demo ├── kenaito-conf-rust-client/ # Rust client SDK (Rust 2021+) -└── kenaito-conf-rust-client-demo/ # Rust client demo +├── kenaito-conf-rust-client-demo/ # Rust client demo +├── kenaito-conf-ruby-client/ # Ruby client SDK (Ruby 2.5+) +└── kenaito-conf-ruby-client-demo/ # Ruby client demo ``` ## Quick Start @@ -417,6 +419,59 @@ $client->syncOnce(); $client->stop(); ``` +### Ruby Client + +For Ruby 2.5+ projects. + +**1. Install** + +```bash +cd kenaito-conf-ruby-client +gem build kenaito-conf.gemspec +gem install kenaito-conf-1.0.3.gem +``` + +Or add to your project `Gemfile`: + +```ruby +gem 'kenaito-conf', path: '/path/to/kenaito-conf-ruby-client' +``` + +**2. Usage** + +```ruby +require 'kenaito_conf' + +config = Kenaito::Conf::Config.new( + base_url: 'http://localhost:8080', + app_name: 'my-app', + env: 'daily', + sync_interval: 10, +) + +client = Kenaito::Conf::Client.new(config) + +# Listen for config changes +client.on_change do |changed| + changed.each do |key, value| + puts "config changed: #{key} = #{value}" + end +end + +# Start client (fetch configs) +client.start + +# Get config +value = client.get('database.host') +all_configs = client.get_all + +# Manual sync once +client.sync_once + +# Stop +client.stop_client +``` + ## API Overview ### Admin API (JWT Authentication Required) diff --git a/kenaito-conf-ruby-client-demo/app.rb b/kenaito-conf-ruby-client-demo/app.rb new file mode 100644 index 0000000..f07195c --- /dev/null +++ b/kenaito-conf-ruby-client-demo/app.rb @@ -0,0 +1,74 @@ +require_relative '../kenaito-conf-ruby-client/lib/kenaito_conf' + +def get_env_var(key, default) + value = ENV[key] + (value && !value.empty?) ? value : default +end + +# 配置值(可被远程配置覆盖) +config_values = { + 'database.host' => get_env_var('DATABASE_HOST', '192.168.1.1'), + 'database.port' => get_env_var('DATABASE_PORT', '8888'), + 'database.name' => get_env_var('DATABASE_NAME', 'xxxx'), + 'database.user' => get_env_var('DATABASE_USER', 'xxxx'), + 'database.password' => get_env_var('DATABASE_PASSWORD', 'xxxxxxxx') +} + +config = Kenaito::Conf::Config.new( + base_url: get_env_var('KENAITO_BASE_URL', 'http://localhost:8080'), + app_name: get_env_var('KENAITO_APP_NAME', 'kenaito-ruby-demo'), + env: get_env_var('KENAITO_ENV', 'daily'), + version: get_env_var('KENAITO_VERSION', 'latest'), + token: get_env_var('KENAITO_TOKEN', ''), + sync_interval: get_env_var('KENAITO_SYNC_INTERVAL', '10').to_i, + cache_dir: get_env_var('KENAITO_CACHE_DIR', '.') +) + +client = Kenaito::Conf::Client.new(config) + +# 监听配置变更 +client.on_change do |changed| + puts "Config changed:" + changed.each do |key, value| + puts " #{key} = #{value}" + if config_values.key?(key) + config_values[key] = value + puts " [updated] #{key} = #{value}" + end + end +end + +# 启动客户端 +begin + client.start + remote = client.get_all + config_values.each do |key, _| + if remote.key?(key) + config_values[key] = remote[key] + end + end + puts "loaded #{remote.size} configs from remote\n\n" +rescue => e + puts "Failed to init config client, using local defaults: #{e.message}\n\n" +end + +# 显示当前配置 +puts "Current configs:" +config_values.keys.sort.each do |key| + puts " #{key} = #{config_values[key]}" +end + +puts "\nListening for config changes... (press Ctrl+C to stop)\n" + +# 信号处理 +trap('INT') do + puts "\nShutting down..." + client.stop_client + exit(0) +end + +# 同步循环 +until client.stop + client.sync_once + sleep(config.sync_interval) +end diff --git a/kenaito-conf-ruby-client/README.md b/kenaito-conf-ruby-client/README.md new file mode 100644 index 0000000..92fc6ea --- /dev/null +++ b/kenaito-conf-ruby-client/README.md @@ -0,0 +1,91 @@ +# Kenaito Config Center Ruby Client + +[English](README_EN.md) | 中文 + +Kenaito 配置中心 Ruby 客户端 SDK,适用于 Ruby 2.5+ 项目。 + +## 安装 + +### 方式一:本地安装 + +```bash +cd kenaito-conf-ruby-client +gem build kenaito-conf.gemspec +gem install kenaito-conf-1.0.3.gem +``` + +### 方式二:Bundler + +在你的项目 `Gemfile` 中添加: + +```ruby +gem 'kenaito-conf', path: '/path/to/kenaito-conf-ruby-client' +``` + +然后执行: + +```bash +bundle install +``` + +### 方式三:直接引入 + +```ruby +require_relative '/path/to/kenaito-conf-ruby-client/lib/kenaito_conf' +``` + +## 使用示例 + +```ruby +require 'kenaito_conf' + +config = Kenaito::Conf::Config.new( + base_url: 'http://localhost:8080', + app_name: 'my-app', + env: 'daily', + sync_interval: 10 +) + +client = Kenaito::Conf::Client.new(config) + +# 监听配置变更 +client.on_change do |changed| + changed.each do |key, value| + puts "config changed: #{key} = #{value}" + end +end + +# 启动客户端(拉取配置) +client.start + +# 获取配置 +value = client.get('database.host') +all_configs = client.get_all + +# 手动同步一次 +client.sync_once + +# 停止 +client.stop_client +``` + +## 配置项 + +| 配置项 | 说明 | 默认值 | +|--------|------|--------| +| base_url | 配置中心后端地址 | http://localhost:8080 | +| app_name | 应用名称 | 空 | +| env | 环境(dev/daily/prod) | dev | +| version | 配置版本 | latest | +| token | 应用 Token | 空 | +| sync_interval | 同步间隔(秒) | 10 | +| ignore_global | 是否忽略全局配置 | false | +| cache_dir | 缓存目录 | . | + +## 功能特性 + +- ✅ 全量拉取配置 +- ✅ 增量同步变更 +- ✅ 本地文件缓存容灾 +- ✅ 配置变更监听回调 +- ✅ 删除配置感知(deletedKeys) diff --git a/kenaito-conf-ruby-client/README_EN.md b/kenaito-conf-ruby-client/README_EN.md new file mode 100644 index 0000000..238c024 --- /dev/null +++ b/kenaito-conf-ruby-client/README_EN.md @@ -0,0 +1,91 @@ +# Kenaito Config Center Ruby Client + +[English](README_EN.md) | 中文 + +Kenaito Config Center Ruby client SDK, compatible with Ruby 2.5+. + +## Installation + +### Option 1: Local Install + +```bash +cd kenaito-conf-ruby-client +gem build kenaito-conf.gemspec +gem install kenaito-conf-1.0.3.gem +``` + +### Option 2: Bundler + +Add to your project `Gemfile`: + +```ruby +gem 'kenaito-conf', path: '/path/to/kenaito-conf-ruby-client' +``` + +Then run: + +```bash +bundle install +``` + +### Option 3: Direct Require + +```ruby +require_relative '/path/to/kenaito-conf-ruby-client/lib/kenaito_conf' +``` + +## Usage Example + +```ruby +require 'kenaito_conf' + +config = Kenaito::Conf::Config.new( + base_url: 'http://localhost:8080', + app_name: 'my-app', + env: 'daily', + sync_interval: 10 +) + +client = Kenaito::Conf::Client.new(config) + +# Listen for config changes +client.on_change do |changed| + changed.each do |key, value| + puts "config changed: #{key} = #{value}" + end +end + +# Start client (fetch configs) +client.start + +# Get config +value = client.get('database.host') +all_configs = client.get_all + +# Manual sync once +client.sync_once + +# Stop +client.stop_client +``` + +## Configuration Options + +| Option | Description | Default | +|--------|-------------|---------| +| base_url | Config center backend URL | http://localhost:8080 | +| app_name | Application name | empty | +| env | Environment (dev/daily/prod) | dev | +| version | Config version | latest | +| token | Application token | empty | +| sync_interval | Sync interval (seconds) | 10 | +| ignore_global | Ignore global configs | false | +| cache_dir | Cache directory | . | + +## Features + +- ✅ Full config fetch +- ✅ Incremental sync +- ✅ Local file cache for disaster recovery +- ✅ Config change listener callback +- ✅ Deleted config awareness (deletedKeys) diff --git a/kenaito-conf-ruby-client/kenaito-conf.gemspec b/kenaito-conf-ruby-client/kenaito-conf.gemspec new file mode 100644 index 0000000..c5484ba --- /dev/null +++ b/kenaito-conf-ruby-client/kenaito-conf.gemspec @@ -0,0 +1,17 @@ +Gem::Specification.new do |spec| + spec.name = 'kenaito-conf' + spec.version = '1.0.3' + spec.authors = ['odboy'] + spec.email = ['odboy@outlook.com'] + + spec.summary = 'Kenaito Config Center Ruby Client' + spec.description = 'Ruby client SDK for Kenaito Config Center' + spec.homepage = 'https://github.com/odboy/kenaito-conf' + spec.license = 'Apache-2.0' + spec.required_ruby_version = '>= 2.5.0' + + spec.files = Dir['lib/**/*.rb'] + spec.require_paths = ['lib'] + + spec.add_development_dependency 'rake', '~> 13.0' +end diff --git a/kenaito-conf-ruby-client/lib/kenaito_conf.rb b/kenaito-conf-ruby-client/lib/kenaito_conf.rb new file mode 100644 index 0000000..0d482da --- /dev/null +++ b/kenaito-conf-ruby-client/lib/kenaito_conf.rb @@ -0,0 +1,10 @@ +require_relative 'kenaito_conf/config' +require_relative 'kenaito_conf/cache' +require_relative 'kenaito_conf/listener' +require_relative 'kenaito_conf/client' + +module Kenaito + module Conf + VERSION = '1.0.3' + end +end diff --git a/kenaito-conf-ruby-client/lib/kenaito_conf/cache.rb b/kenaito-conf-ruby-client/lib/kenaito_conf/cache.rb new file mode 100644 index 0000000..4043453 --- /dev/null +++ b/kenaito-conf-ruby-client/lib/kenaito_conf/cache.rb @@ -0,0 +1,33 @@ +require 'json' +require 'fileutils' + +module Kenaito + module Conf + class Cache + def initialize(cache_dir) + @cache_file = File.join(cache_dir, 'config-cache.json') + end + + def read + return nil unless File.exist?(@cache_file) + + begin + content = File.read(@cache_file) + JSON.parse(content) + rescue => e + $stderr.puts "[ConfigClient] cache read error: #{e.message}" + nil + end + end + + def write(data) + begin + FileUtils.mkdir_p(File.dirname(@cache_file)) + File.write(@cache_file, JSON.pretty_generate(data)) + rescue => e + $stderr.puts "[ConfigClient] cache write error: #{e.message}" + end + end + end + end +end diff --git a/kenaito-conf-ruby-client/lib/kenaito_conf/client.rb b/kenaito-conf-ruby-client/lib/kenaito_conf/client.rb new file mode 100644 index 0000000..ba73039 --- /dev/null +++ b/kenaito-conf-ruby-client/lib/kenaito_conf/client.rb @@ -0,0 +1,171 @@ +require 'net/http' +require 'uri' +require 'json' + +module Kenaito + module Conf + class Client + attr_reader :configs + attr_accessor :stop + + def initialize(config) + @config = config + @cache = Cache.new(config.cache_dir) + @listener = Listener.new + @configs = {} + @last_time = 0 + @started = false + @stop = false + end + + def on_change(&block) + @listener.on_change(&block) + end + + def get(key) + @configs[key] + end + + def get_all + @configs + end + + def start + return if @started + @started = true + + begin + fetch_all + rescue => e + $stderr.puts "[ConfigClient] fetch all configs error: #{e.message}, trying cache" + cached = @cache.read + if cached + apply_configs(cached) + $stderr.puts "[ConfigClient] loaded #{@configs.size} configs from cache" + else + raise "fetch configs failed and no cache: #{e.message}" + end + end + end + + def stop_client + @stop = true + end + + def sync_once + begin + fetch_changed + rescue => e + $stderr.puts "[ConfigClient] sync error: #{e.message}" + end + end + + private + + def http_post(url, payload) + uri = URI.parse(url) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = (uri.scheme == 'https') + http.open_timeout = 10 + http.read_timeout = 10 + + request = Net::HTTP::Post.new(uri.request_uri, 'Content-Type' => 'application/json') + request.body = payload.to_json + + response = http.request(request) + + if response.code.to_i != 200 + raise "HTTP #{response.code}" + end + + begin + JSON.parse(response.body) + rescue => e + raise "JSON decode error: #{e.message}" + end + end + + def fetch_all + url = "#{@config.base_url}/api/fetchAllConfigs" + payload = { + appName: @config.app_name, + env: @config.env, + version: @config.version, + ignoreGlobal: @config.ignore_global, + token: @config.token + } + + data = http_post(url, payload) + + if data['code'] && data['code'] != 0 + raise "server error: #{data['msg'] || 'unknown'}" + end + + if data['latest.fetch.timestamp'] + @last_time = data['latest.fetch.timestamp'].to_i + end + + @cache.write(data) + apply_configs(data) + + $stderr.puts "[ConfigClient] fetched #{@configs.size} configs" + end + + def fetch_changed + url = "#{@config.base_url}/api/fetchChangedConfigs" + payload = { + appName: @config.app_name, + env: @config.env, + version: @config.version, + lastTime: @last_time, + ignoreGlobal: @config.ignore_global, + token: @config.token + } + + result = http_post(url, payload) + + if result['code'] && result['code'] != 0 + raise "server error code: #{result['code']}" + end + + changed_data = result['data'] || {} + + if changed_data['latest.fetch.timestamp'] + @last_time = changed_data['latest.fetch.timestamp'].to_i + changed_data.delete('latest.fetch.timestamp') + end + + # 处理删除的配置 + if changed_data['deletedKeys'] && !changed_data['deletedKeys'].empty? + deleted_keys = changed_data['deletedKeys'].split(',') + deleted_keys.each { |key| @configs.delete(key) } + $stderr.puts "[ConfigClient] #{deleted_keys.size} configs deleted: #{deleted_keys.join(', ')}" + changed_data.delete('deletedKeys') + end + + # 处理变更的配置 + changed = {} + changed_data.each do |key, value| + if value.is_a?(String) + @configs[key] = value + changed[key] = value + end + end + + unless changed.empty? + @listener.notify(changed) + $stderr.puts "[ConfigClient] #{changed.size} configs changed: #{changed.keys.join(', ')}" + end + end + + def apply_configs(data) + data.each do |key, value| + next if key.start_with?('kenaito.') || key == 'latest.fetch.timestamp' + if value.is_a?(String) + @configs[key] = value + end + end + end + end + end +end diff --git a/kenaito-conf-ruby-client/lib/kenaito_conf/config.rb b/kenaito-conf-ruby-client/lib/kenaito_conf/config.rb new file mode 100644 index 0000000..5a4b468 --- /dev/null +++ b/kenaito-conf-ruby-client/lib/kenaito_conf/config.rb @@ -0,0 +1,18 @@ +module Kenaito + module Conf + class Config + attr_accessor :base_url, :app_name, :env, :version, :token, :sync_interval, :ignore_global, :cache_dir + + def initialize(options = {}) + @base_url = options[:base_url] || 'http://localhost:8080' + @app_name = options[:app_name] || '' + @env = options[:env] || 'dev' + @version = options[:version] || 'latest' + @token = options[:token] || '' + @sync_interval = options[:sync_interval] || 10 + @ignore_global = options[:ignore_global] || false + @cache_dir = options[:cache_dir] || '.' + end + end + end +end diff --git a/kenaito-conf-ruby-client/lib/kenaito_conf/listener.rb b/kenaito-conf-ruby-client/lib/kenaito_conf/listener.rb new file mode 100644 index 0000000..a9e6102 --- /dev/null +++ b/kenaito-conf-ruby-client/lib/kenaito_conf/listener.rb @@ -0,0 +1,23 @@ +module Kenaito + module Conf + class Listener + def initialize + @handlers = [] + end + + def on_change(&block) + @handlers << block + end + + def notify(changed) + @handlers.each do |handler| + begin + handler.call(changed) + rescue => e + $stderr.puts "[ConfigClient] change handler error: #{e.message}" + end + end + end + end + end +end