feat: 添加多语言客户端 Docker 支持

- 新增 .NET 客户端 SDK 和 demo 项目
- 添加 Go/Node.js/Python/.NET/SpringBoot2 的 Dockerfile
- Go demo 支持从环境变量读取配置
- 创建 README_DOCKERFILE.md 文档说明 Docker 使用方法
- 更新根目录 README.md 标记 .NET 客户端为已支持
This commit is contained in:
2026-06-14 21:07:35 +08:00
parent 18b8c4c18b
commit 82523b9236
19 changed files with 820 additions and 11 deletions
+23
View File
@@ -0,0 +1,23 @@
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
# Copy client library
COPY kenaito-conf-dotnet-client/ ./kenaito-conf-dotnet-client/
# Copy demo project
COPY kenaito-conf-dotnet-client-demo/ ./kenaito-conf-dotnet-client-demo/
# Build
WORKDIR /src/kenaito-conf-dotnet-client-demo
RUN dotnet restore
RUN dotnet publish -c Release -o /app
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "KenaitoConfDemo.dll"]
+25
View File
@@ -0,0 +1,25 @@
# Build stage
FROM golang:1.25-alpine AS build
# 设置国内 Go 代理
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /src
# Copy client library
COPY kenaito-conf-go-client/ ./kenaito-conf-go-client/
# Copy demo project
COPY kenaito-conf-go-client-demo/ ./kenaito-conf-go-client-demo/
# Build
WORKDIR /src/kenaito-conf-go-client-demo
RUN go mod tidy && go build -o /app/demo .
# Runtime stage
FROM alpine:3.20
WORKDIR /app
COPY --from=build /app/demo .
ENTRYPOINT ["./demo"]
+27
View File
@@ -0,0 +1,27 @@
# Build stage
FROM node:18-alpine AS build
WORKDIR /src
# Copy client library
COPY kenaito-conf-nodejs-client/ ./kenaito-conf-nodejs-client/
# Copy demo project
COPY kenaito-conf-nodejs-client-demo/ ./kenaito-conf-nodejs-client-demo/
# Install client locally, then demo dependencies
WORKDIR /src/kenaito-conf-nodejs-client
RUN npm link
WORKDIR /src/kenaito-conf-nodejs-client-demo
RUN npm link kenaito-conf && npm install
# Runtime stage
FROM node:18-alpine
WORKDIR /app
COPY --from=build /src/kenaito-conf-nodejs-client-demo .
EXPOSE 8083
CMD ["node", "app.js"]
+29
View File
@@ -0,0 +1,29 @@
# Build stage
FROM python:3.12-slim AS build
WORKDIR /src
# Copy client library
COPY kenaito-conf-python-client/ ./kenaito-conf-python-client/
# Copy demo project
COPY kenaito-conf-python-client-demo/ ./kenaito-conf-python-client-demo/
# Install client library
WORKDIR /src/kenaito-conf-python-client
RUN pip install --no-cache-dir .
# Install demo dependencies
WORKDIR /src/kenaito-conf-python-client-demo
RUN pip install --no-cache-dir -r requirements.txt
# Runtime stage
FROM python:3.12-slim
WORKDIR /app
COPY --from=build /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=build /src/kenaito-conf-python-client-demo .
EXPOSE 8082
CMD ["python", "app.py"]
+24
View File
@@ -0,0 +1,24 @@
# Build stage
FROM maven:3.9-eclipse-temurin-8 AS build
WORKDIR /src
# Copy client library and install to local Maven repo
COPY kenaito-conf-springboot2-client/ ./kenaito-conf-springboot2-client/
WORKDIR /src/kenaito-conf-springboot2-client
RUN mvn clean install -DskipTests
# Copy demo project and build
COPY kenaito-conf-springboot2-client-demo/ ./kenaito-conf-springboot2-client-demo/
WORKDIR /src/kenaito-conf-springboot2-client-demo
RUN mvn clean package -DskipTests
# Runtime stage
FROM eclipse-temurin:8-jre
WORKDIR /app
COPY --from=build /src/kenaito-conf-springboot2-client-demo/target/*.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]
+2 -2
View File
@@ -24,8 +24,8 @@ kenaito-conf/
├── kenaito-conf-nodejs-client-demo/ # Node.js 客户端示例(端口 8083) ├── kenaito-conf-nodejs-client-demo/ # Node.js 客户端示例(端口 8083)
├── kenaito-conf-php-client/ # (未支持)PHP 客户端 SDK(PHP 8.0+) ├── kenaito-conf-php-client/ # (未支持)PHP 客户端 SDK(PHP 8.0+)
├── kenaito-conf-php-client-demo/ # (未支持)PHP 客户端示例 ├── kenaito-conf-php-client-demo/ # (未支持)PHP 客户端示例
├── kenaito-conf-dotnet-client/ # (未支持).NET 客户端 SDK(.NET 6.0+) ├── kenaito-conf-dotnet-client/ # .NET 客户端 SDK(.NET 6.0+)
├── kenaito-conf-dotnet-client-demo/ # (未支持).NET 客户端示例 ├── kenaito-conf-dotnet-client-demo/ # .NET 客户端示例
├── kenaito-conf-c-client/ # (未支持)C 客户端 SDK(libcurl + OpenSSL) ├── kenaito-conf-c-client/ # (未支持)C 客户端 SDK(libcurl + OpenSSL)
├── kenaito-conf-c-client-demo/ # (未支持)C 客户端示例 ├── kenaito-conf-c-client-demo/ # (未支持)C 客户端示例
├── kenaito-conf-cpp-client/ # (未支持)C++ 客户端 SDK(libcurl + OpenSSL) ├── kenaito-conf-cpp-client/ # (未支持)C++ 客户端 SDK(libcurl + OpenSSL)
+152
View File
@@ -0,0 +1,152 @@
# Kenaito Config Center 客户端 Docker 部署指南
本文档说明如何使用 Dockerfile 构建和测试各语言客户端 demo。
## 前置条件
- Docker 已安装
- 配置中心服务端已启动(默认 `http://localhost:8080`)
- 已在管理后台创建对应应用和环境
## 快速构建
在项目根目录执行:
```bash
# Go 客户端
docker build -f Dockerfile.go -t kenaito-conf-go-demo .
# Node.js 客户端
docker build -f Dockerfile.nodejs -t kenaito-conf-nodejs-demo .
# Python 客户端
docker build -f Dockerfile.python -t kenaito-conf-python-demo .
# .NET 客户端
docker build -f Dockerfile.dotnet -t kenaito-conf-dotnet-demo .
# Spring Boot 2 客户端
docker build -f Dockerfile.springboot2 -t kenaito-conf-springboot2-demo .
```
## 运行测试
### Go 客户端
```bash
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-go-demo
```
### Node.js 客户端
```bash
docker run -d --rm --name nodejs-demo \
-p 8083:8083 \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
-e KENAITO_APP_NAME=kenaito-springboot-demo \
-e KENAITO_ENV=daily \
kenaito-conf-nodejs-demo
# 查看日志
docker logs -f nodejs-demo
# 测试接口
curl http://localhost:8083/
curl http://localhost:8083/health
# 停止
docker stop nodejs-demo
```
### Python 客户端
```bash
docker run -d --rm --name python-demo \
-p 8082:8082 \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
-e KENAITO_APP_NAME=kenaito-springboot-demo \
-e KENAITO_ENV=daily \
kenaito-conf-python-demo
# 查看日志
docker logs -f python-demo
# 测试接口
curl http://localhost:8082/
curl http://localhost:8082/health
# 停止
docker stop python-demo
```
### .NET 客户端
```bash
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-dotnet-demo
```
### Spring Boot 2 客户端
```bash
docker run -d --rm --name springboot2-demo \
-p 8081:8081 \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
-e KENAITO_APP_NAME=kenaito-springboot-demo \
-e KENAITO_ENV=daily \
kenaito-conf-springboot2-demo
# 查看日志
docker logs -f springboot2-demo
# 测试接口
curl http://localhost:8081/
curl http://localhost:8081/health
# 停止
docker stop springboot2-demo
```
## 环境变量说明
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `KENAITO_BASE_URL` | 配置中心地址 | `http://localhost:8080` |
| `KENAITO_APP_NAME` | 应用名称 | `kenaito-springboot-demo` |
| `KENAITO_ENV` | 环境标识 | `daily` |
| `KENAITO_VERSION` | 配置版本 | `latest` |
| `KENAITO_TOKEN` | 应用 Token | 空 |
| `KENAITO_SYNC_INTERVAL` | 同步间隔(秒) | `10` |
| `KENAITO_CACHE_DIR` | 缓存目录 | `.` |
## 注意事项
1. **host.docker.internal**:容器内访问宿主机服务时使用 `host.docker.internal:8080`,Linux 系统可能需要添加 `--add-host host.docker.internal:host-gateway`
2. **网络模式**:如果配置中心也运行在 Docker 中,建议使用自定义网络:
```bash
docker network create kenaito-net
docker run --network kenaito-net --name kenaito-server ...
docker run --network kenaito-net -e KENAITO_BASE_URL=http://kenaito-server:8080 ...
```
3. **配置缓存**:各客户端会在运行目录生成 `config-cache.json` 文件,容器重启后可从缓存加载配置
4. **日志查看**:
- Go/.NET:直接输出到 stdout
- Node.js/Python/Spring Boot:使用各自日志框架
## 测试配置变更
1. 启动客户端容器
2. 在管理后台修改对应应用的配置
3. 等待同步间隔(默认 10 秒)
4. 观察客户端日志是否输出配置变更
5. 访问 HTTP 接口验证配置是否更新(Node.js/Python/Spring Boot)
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../kenaito-conf-dotnet-client/KenaitoConfClient.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,52 @@
using Kenaito.Conf.Client;
var config = new Config
{
BaseUrl = "http://localhost:8080",
AppName = "kenaito-springboot-demo",
Env = "daily",
Version = "latest",
SyncInterval = 10,
CacheDir = "."
};
var client = new Client(config);
client.OnChange(changed =>
{
Console.WriteLine("Config changed:");
foreach (var kv in changed)
{
Console.WriteLine($" {kv.Key} = {kv.Value}");
}
});
await client.StartAsync();
Console.WriteLine("Current configs:");
foreach (var kv in client.GetAll())
{
Console.WriteLine($" {kv.Key} = {kv.Value}");
}
var dbHost = client.Get("database.host");
Console.WriteLine($"\ndatabase.host = {dbHost}");
Console.WriteLine("\nListening for config changes... (press Ctrl+C to stop)");
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) =>
{
e.Cancel = true;
cts.Cancel();
};
try
{
await Task.Delay(Timeout.Infinite, cts.Token);
}
catch (TaskCanceledException)
{
}
Console.WriteLine("\nShutting down...");
client.Stop();
+41
View File
@@ -0,0 +1,41 @@
# Kenaito Config Center .NET Demo
演示如何使用 kenaito-conf-dotnet-client 客户端库。
## 本地运行
```bash
cd kenaito-conf-dotnet-client-demo
dotnet run
```
## Docker 运行
```bash
# 在项目根目录执行
docker build -f Dockerfile.dotnet -t kenaito-conf-dotnet-demo .
docker run -it --rm \
-e CONFIG_CENTER_BASE_URL=http://host.docker.internal:8080 \
kenaito-conf-dotnet-demo
```
## 配置项
修改 `Program.cs` 中的配置:
```csharp
var config = new Config
{
BaseUrl = "http://localhost:8080", // 配置中心地址
AppName = "kenaito-dotnet-demo", // 应用名称
Env = "daily", // 环境
SyncInterval = 10 // 同步间隔(秒)
};
```
## 功能演示
- 启动时拉取全量配置并打印
- 监听配置变更,实时输出变更内容
- 按 Ctrl+C 优雅退出
+41
View File
@@ -0,0 +1,41 @@
using Newtonsoft.Json;
namespace Kenaito.Conf.Client;
internal class Cache
{
private readonly string _cacheFile;
public Cache(string cacheDir)
{
_cacheFile = Path.Combine(cacheDir, "config-cache.json");
}
public Dictionary<string, object>? Read()
{
try
{
if (!File.Exists(_cacheFile))
return null;
var json = File.ReadAllText(_cacheFile);
return JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
}
catch
{
return null;
}
}
public void Write(Dictionary<string, object> data)
{
try
{
var json = JsonConvert.SerializeObject(data, Formatting.Indented);
File.WriteAllText(_cacheFile, json);
}
catch
{
// ignore cache write errors
}
}
}
+222
View File
@@ -0,0 +1,222 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Kenaito.Conf.Client;
public class Client
{
private readonly Config _config;
private readonly Cache _cache;
private readonly Listener _listener;
private readonly HttpClient _httpClient;
private readonly Dictionary<string, string> _configs;
private readonly object _lock = new();
private long _lastTime;
private bool _started;
private CancellationTokenSource? _cts;
public Client(Config config)
{
_config = config;
if (string.IsNullOrEmpty(_config.Version))
_config.Version = "latest";
if (_config.SyncInterval <= 0)
_config.SyncInterval = 10;
if (string.IsNullOrEmpty(_config.CacheDir))
_config.CacheDir = ".";
_cache = new Cache(_config.CacheDir);
_listener = new Listener();
_configs = new Dictionary<string, string>();
_httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
}
public void OnChange(ChangeHandler handler)
{
_listener.OnChange(handler);
}
public string? Get(string key)
{
lock (_lock)
{
return _configs.TryGetValue(key, out var value) ? value : null;
}
}
public Dictionary<string, string> GetAll()
{
lock (_lock)
{
return new Dictionary<string, string>(_configs);
}
}
public async Task StartAsync()
{
if (_started)
return;
_started = true;
try
{
await FetchAllAsync();
}
catch (Exception ex)
{
Console.WriteLine($"[ConfigClient] fetch all configs error: {ex.Message}, trying cache");
var cached = _cache.Read();
if (cached != null)
{
ApplyConfigs(cached);
Console.WriteLine($"[ConfigClient] loaded {CountConfigs(cached)} configs from cache");
}
else
{
throw new Exception("fetch configs failed and no cache", ex);
}
}
_cts = new CancellationTokenSource();
_ = SyncLoopAsync(_cts.Token);
}
public void Stop()
{
_cts?.Cancel();
}
private async Task FetchAllAsync()
{
var url = $"{_config.BaseUrl.TrimEnd('/')}/api/fetchAllConfigs";
var payload = new
{
appName = _config.AppName,
env = _config.Env,
version = _config.Version,
ignoreGlobal = _config.IgnoreGlobal,
token = _config.Token
};
var content = new StringContent(JsonConvert.SerializeObject(payload), System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Dictionary<string, object>>(body);
if (result == null)
throw new Exception("empty response");
if (result.TryGetValue("code", out var codeObj))
{
var code = Convert.ToInt64(codeObj);
if (code != 0)
{
var msg = result.TryGetValue("msg", out var msgObj) ? msgObj?.ToString() : "unknown error";
throw new Exception($"server error: {msg}");
}
}
if (result.TryGetValue("latest.fetch.timestamp", out var tsObj))
{
long.TryParse(tsObj?.ToString(), out _lastTime);
}
_cache.Write(result);
ApplyConfigs(result);
Console.WriteLine($"[ConfigClient] fetched {CountConfigs(result)} configs");
}
private async Task FetchChangedAsync()
{
var url = $"{_config.BaseUrl.TrimEnd('/')}/api/fetchChangedConfigs";
var payload = new
{
appName = _config.AppName,
env = _config.Env,
version = _config.Version,
lastTime = _lastTime,
ignoreGlobal = _config.IgnoreGlobal
};
var content = new StringContent(JsonConvert.SerializeObject(payload), System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
var wrapper = JsonConvert.DeserializeObject<FetchChangedResponse>(body);
if (wrapper == null)
return;
if (wrapper.Code != 0)
throw new Exception($"server error code: {wrapper.Code}");
if (wrapper.Data == null)
return;
if (wrapper.Data.TryGetValue("latest.fetch.timestamp", out var ts))
{
long.TryParse(ts, out _lastTime);
wrapper.Data.Remove("latest.fetch.timestamp");
}
if (wrapper.Data.Count > 0)
{
lock (_lock)
{
foreach (var kv in wrapper.Data)
{
_configs[kv.Key] = kv.Value;
}
}
_listener.Notify(wrapper.Data);
Console.WriteLine($"[ConfigClient] {wrapper.Data.Count} configs changed");
}
}
private void ApplyConfigs(Dictionary<string, object> data)
{
lock (_lock)
{
foreach (var kv in data)
{
if (kv.Key.StartsWith("kenaito.") || kv.Key == "latest.fetch.timestamp")
continue;
if (kv.Value is string strVal)
{
_configs[kv.Key] = strVal;
}
}
}
}
private int CountConfigs(Dictionary<string, object> data)
{
return data.Count(kv => !kv.Key.StartsWith("kenaito.") && kv.Key != "latest.fetch.timestamp" && kv.Value is string);
}
private async Task SyncLoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
await Task.Delay(_config.SyncInterval * 1000, ct);
await FetchChangedAsync();
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Console.WriteLine($"[ConfigClient] sync error: {ex.Message}");
}
}
}
private class FetchChangedResponse
{
public int Code { get; set; }
public Dictionary<string, string>? Data { get; set; }
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace Kenaito.Conf.Client;
public class Config
{
public string BaseUrl { get; set; } = "";
public string AppName { get; set; } = "";
public string Env { get; set; } = "dev";
public string Version { get; set; } = "latest";
public string? Token { get; set; }
public int SyncInterval { get; set; } = 10;
public string CacheDir { get; set; } = ".";
public bool IgnoreGlobal { get; set; } = false;
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>Kenaito.Conf.Client</PackageId>
<Version>1.0.0</Version>
<Authors>odboy</Authors>
<Company>www.odboy.cn</Company>
<Description>Kenaito Config Center .NET Client</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
+28
View File
@@ -0,0 +1,28 @@
namespace Kenaito.Conf.Client;
public delegate void ChangeHandler(Dictionary<string, string> changed);
internal class Listener
{
private readonly List<ChangeHandler> _handlers = new();
public void OnChange(ChangeHandler handler)
{
_handlers.Add(handler);
}
public void Notify(Dictionary<string, string> changed)
{
foreach (var handler in _handlers)
{
try
{
handler(changed);
}
catch
{
// ignore handler errors
}
}
}
}
+75
View File
@@ -0,0 +1,75 @@
# Kenaito Config Center .NET Client
kenaito-conf 配置中心的 .NET 客户端 SDK。
## 安装
### 本地编译
```bash
cd kenaito-conf-dotnet-client
dotnet build
```
### 作为本地依赖
在 demo 项目的 csproj 中引用:
```xml
<ItemGroup>
<ProjectReference Include="../kenaito-conf-dotnet-client/KenaitoConfClient.csproj" />
</ItemGroup>
```
## 快速开始
```csharp
using Kenaito.Conf.Client;
var config = new Config
{
BaseUrl = "http://localhost:8080",
AppName = "kenaito-dotnet-demo",
Env = "daily",
SyncInterval = 10
};
var client = new Client(config);
client.OnChange(changed =>
{
Console.WriteLine("Config changed:");
foreach (var kv in changed)
{
Console.WriteLine($" {kv.Key} = {kv.Value}");
}
});
await client.StartAsync();
var dbHost = client.Get("database.host");
Console.WriteLine($"database.host = {dbHost}");
Console.ReadLine();
client.Stop();
```
## 配置项
| 配置项 | 说明 | 默认值 | 必填 |
|--------|------|--------|------|
| BaseUrl | 配置中心地址 | - | 是 |
| AppName | 应用名称 | - | 是 |
| Env | 环境标识 | dev | 否 |
| Version | 配置版本 | latest | 否 |
| Token | 认证令牌 | null | 否 |
| SyncInterval | 同步间隔(秒) | 10 | 否 |
| CacheDir | 缓存目录 | . | 否 |
| IgnoreGlobal | 是否忽略全局配置 | false | 否 |
## 功能特性
- 启动时拉取全量配置
- 定时轮询增量配置变更
- 本地文件缓存,服务不可用时从缓存加载
- 配置变更回调通知
+4 -1
View File
@@ -2,6 +2,9 @@ module kenaito-conf-go-client-demo
go 1.25.0 go 1.25.0
require kenaito-conf/go-client v0.0.0 require (
kenaito-conf/go-client v0.0.0
github.com/joho/godotenv v1.5.1
)
replace kenaito-conf/go-client => ../kenaito-conf-go-client replace kenaito-conf/go-client => ../kenaito-conf-go-client
+1
View File
@@ -0,0 +1 @@
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+29 -8
View File
@@ -6,19 +6,40 @@ import (
"log" "log"
"os" "os"
"os/signal" "os/signal"
"strconv"
"syscall" "syscall"
"github.com/joho/godotenv"
) )
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if i, err := strconv.Atoi(value); err == nil {
return i
}
}
return defaultValue
}
func main() { func main() {
// create config _ = godotenv.Load()
// create config from environment variables
cfg := &client.Config{ cfg := &client.Config{
BaseURL: "http://localhost:8080", BaseURL: getEnv("KENAITO_BASE_URL", "http://localhost:8080"),
AppName: "kenaito-springboot-demo", AppName: getEnv("KENAITO_APP_NAME", "kenaito-springboot-demo"),
Env: "daily", Env: getEnv("KENAITO_ENV", "daily"),
Version: "latest", Version: getEnv("KENAITO_VERSION", "latest"),
//Token: "6f6966fe196572954b176c0e2a97bc399b050c6be31454b8d6b0f7f622b5d6f1", Token: getEnv("KENAITO_TOKEN", ""),
SyncInterval: 10, // sync every 10 seconds SyncInterval: getEnvInt("KENAITO_SYNC_INTERVAL", 10),
CacheDir: ".", CacheDir: getEnv("KENAITO_CACHE_DIR", "."),
} }
// create client // create client