mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-29 07:21:23 +08:00
✨ feat(jvm): 增加连接测试与能力探测 API
- 新增 JVM provider 工厂与 JMX、Endpoint 骨架实现 - 暴露 TestJVMConnection 和 JVMProbeCapabilities 并统一 QueryResult 返回 - 刷新 Wails 绑定与 JVM 连接模型,补齐前后端方法签名 - 补充 App 编排测试与 provider 契约测试,避免假成功和静默成功 - 更新需求追踪,记录 Task 2 审查结论与验证证据
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
- [x] 阶段 2(影响分析):完成
|
||||
- [x] 阶段 3(方案设计):完成(已形成正式设计文档)
|
||||
- [x] 阶段 4(实施计划):完成(已形成正式实施计划)
|
||||
- [ ] 阶段 5(实现与自检):进行中(Task 1 已完成并通过回归)
|
||||
- [ ] 阶段 5(实现与自检):进行中(Task 1、Task 2 已完成并通过回归)
|
||||
- [ ] 阶段 6(评审与交付):
|
||||
- [ ] 阶段 7(发布与观察):
|
||||
|
||||
@@ -40,11 +40,12 @@
|
||||
- 用户明确目标 Java 服务大概率不允许 `-javaagent` 或运行时动态 attach
|
||||
- 已形成 JVM 缓存可视化编辑正式设计文档
|
||||
- 已形成 JVM Connector MVP 正式实施计划文档
|
||||
- - 已完成 Task 1:JVM 共享契约与配置归一化
|
||||
- 已完成 Task 1:JVM 共享契约与配置归一化
|
||||
- 已完成 Task 2:Provider 注册、连接测试与能力探测 API
|
||||
- 进行中:
|
||||
- Task 2:建立后端 Provider 注册与连接探测 API
|
||||
- Task 3:接入 JVM 连接表单与图标
|
||||
- 待处理:
|
||||
- Task 3+:Guard/Audit/App/UI/AI 结构化计划等后续任务
|
||||
- Task 4+:只读资源浏览、Guard/Audit、AI 结构化计划等后续任务
|
||||
|
||||
## 5. 风险与阻塞
|
||||
- 风险:
|
||||
@@ -73,12 +74,15 @@
|
||||
- JVM Connector 正式设计文档自检
|
||||
- JVM Connector 实施计划文档自检
|
||||
- Task 1:JVM 共享契约与配置归一化
|
||||
- Task 2:Provider 注册、连接测试与能力探测 API
|
||||
- 结果:
|
||||
- 已确认存在可复用的连接桥接与编辑器基础设施
|
||||
- 已完成正式设计文档落盘与自检,未发现占位词和明显范围冲突
|
||||
- 已完成正式实施计划落盘与自检,已补齐共享 DTO、provider factory 和审计落盘等关键实现细节
|
||||
- 已完成 JVM 连接共享契约、默认只读/默认 JMX 归一化、前端配置收敛与补测
|
||||
- Task 1 已完成规格审查与代码质量审查,结论均通过
|
||||
- 已完成 JVM Provider 工厂、JMX/Endpoint provider 骨架、App 层连接测试与能力探测 API
|
||||
- Task 2 已完成规格审查与代码质量审查,结论均通过
|
||||
- 证据(日志/截图/链接):
|
||||
- `cmd/optional-driver-agent/main.go`
|
||||
- `internal/db/database.go`
|
||||
@@ -99,7 +103,19 @@
|
||||
- `cd frontend && npm test -- src/utils/jvmConnectionConfig.test.ts`
|
||||
- `cd frontend && npm test -- --run`
|
||||
- `cd frontend && npm run build`
|
||||
- `internal/jvm/provider.go`
|
||||
- `internal/jvm/jmx_provider.go`
|
||||
- `internal/jvm/http_provider.go`
|
||||
- `internal/jvm/provider_contract_test.go`
|
||||
- `internal/app/methods_jvm.go`
|
||||
- `internal/app/methods_jvm_test.go`
|
||||
- `frontend/wailsjs/go/app/App.d.ts`
|
||||
- `frontend/wailsjs/go/app/App.js`
|
||||
- `frontend/wailsjs/go/models.ts`
|
||||
- `go test ./internal/app -run 'Test(TestJVMConnection|JVMProbeCapabilities)' -count=1`
|
||||
- `go test ./internal/jvm ./internal/app -count=1`
|
||||
- `wails build -clean`
|
||||
|
||||
## 8. 下一步
|
||||
- 下一步行动:进入 Task 2,建立 JVM Provider 注册、连接测试与能力探测 API,并在完成后生成/校验 Wails 绑定代码
|
||||
- 下一步行动:进入 Task 3,接入 JVM 连接表单、图标与展示文案,并在前端完成最小交互闭环
|
||||
- 负责人:Codex
|
||||
|
||||
12
frontend/wailsjs/go/app/App.d.ts
vendored
12
frontend/wailsjs/go/app/App.d.ts
vendored
@@ -127,6 +127,10 @@ export function InstallLocalDriverPackage(arg1:string,arg2:string,arg3:string,ar
|
||||
|
||||
export function InstallUpdateAndRestart():Promise<connection.QueryResult>;
|
||||
|
||||
export function JVMProbeCapabilities(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
|
||||
|
||||
export function ListSQLDirectory(arg1:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function LogWindowDiagnostic(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function MongoDiscoverMembers(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
|
||||
@@ -149,8 +153,6 @@ export function OpenDriverDownloadDirectory(arg1:string):Promise<connection.Quer
|
||||
|
||||
export function OpenSQLFile():Promise<connection.QueryResult>;
|
||||
|
||||
export function ListSQLDirectory(arg1:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function PreviewImportFile(arg1:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function ReadSQLFile(arg1:string):Promise<connection.QueryResult>;
|
||||
@@ -223,8 +225,6 @@ export function RetrySecurityUpdateCurrentRound(arg1:app.RetrySecurityUpdateRequ
|
||||
|
||||
export function SaveConnection(arg1:connection.SavedConnectionInput):Promise<connection.SavedConnectionView>;
|
||||
|
||||
export function SelectSQLDirectory(arg1:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function SaveGlobalProxy(arg1:connection.SaveGlobalProxyInput):Promise<connection.GlobalProxyView>;
|
||||
|
||||
export function SelectDataRootDirectory(arg1:string):Promise<connection.QueryResult>;
|
||||
@@ -237,6 +237,8 @@ export function SelectDriverPackageDirectory(arg1:string):Promise<connection.Que
|
||||
|
||||
export function SelectDriverPackageFile(arg1:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function SelectSQLDirectory(arg1:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function SelectSSHKeyFile(arg1:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function SetMacNativeWindowControls(arg1:boolean):Promise<void>;
|
||||
@@ -247,4 +249,6 @@ export function StartSecurityUpdate(arg1:app.StartSecurityUpdateRequest):Promise
|
||||
|
||||
export function TestConnection(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
|
||||
|
||||
export function TestJVMConnection(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
|
||||
|
||||
export function TruncateTables(arg1:connection.ConnectionConfig,arg2:string,arg3:Array<string>):Promise<connection.QueryResult>;
|
||||
|
||||
@@ -246,6 +246,14 @@ export function InstallUpdateAndRestart() {
|
||||
return window['go']['app']['App']['InstallUpdateAndRestart']();
|
||||
}
|
||||
|
||||
export function JVMProbeCapabilities(arg1) {
|
||||
return window['go']['app']['App']['JVMProbeCapabilities'](arg1);
|
||||
}
|
||||
|
||||
export function ListSQLDirectory(arg1) {
|
||||
return window['go']['app']['App']['ListSQLDirectory'](arg1);
|
||||
}
|
||||
|
||||
export function LogWindowDiagnostic(arg1, arg2) {
|
||||
return window['go']['app']['App']['LogWindowDiagnostic'](arg1, arg2);
|
||||
}
|
||||
@@ -290,10 +298,6 @@ export function OpenSQLFile() {
|
||||
return window['go']['app']['App']['OpenSQLFile']();
|
||||
}
|
||||
|
||||
export function ListSQLDirectory(arg1) {
|
||||
return window['go']['app']['App']['ListSQLDirectory'](arg1);
|
||||
}
|
||||
|
||||
export function PreviewImportFile(arg1) {
|
||||
return window['go']['app']['App']['PreviewImportFile'](arg1);
|
||||
}
|
||||
@@ -438,10 +442,6 @@ export function SaveConnection(arg1) {
|
||||
return window['go']['app']['App']['SaveConnection'](arg1);
|
||||
}
|
||||
|
||||
export function SelectSQLDirectory(arg1) {
|
||||
return window['go']['app']['App']['SelectSQLDirectory'](arg1);
|
||||
}
|
||||
|
||||
export function SaveGlobalProxy(arg1) {
|
||||
return window['go']['app']['App']['SaveGlobalProxy'](arg1);
|
||||
}
|
||||
@@ -466,6 +466,10 @@ export function SelectDriverPackageFile(arg1) {
|
||||
return window['go']['app']['App']['SelectDriverPackageFile'](arg1);
|
||||
}
|
||||
|
||||
export function SelectSQLDirectory(arg1) {
|
||||
return window['go']['app']['App']['SelectSQLDirectory'](arg1);
|
||||
}
|
||||
|
||||
export function SelectSSHKeyFile(arg1) {
|
||||
return window['go']['app']['App']['SelectSSHKeyFile'](arg1);
|
||||
}
|
||||
@@ -486,6 +490,10 @@ export function TestConnection(arg1) {
|
||||
return window['go']['app']['App']['TestConnection'](arg1);
|
||||
}
|
||||
|
||||
export function TestJVMConnection(arg1) {
|
||||
return window['go']['app']['App']['TestJVMConnection'](arg1);
|
||||
}
|
||||
|
||||
export function TruncateTables(arg1, arg2, arg3) {
|
||||
return window['go']['app']['App']['TruncateTables'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
@@ -456,6 +456,86 @@ export namespace connection {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class JVMEndpointConfig {
|
||||
enabled?: boolean;
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
timeoutSeconds?: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new JVMEndpointConfig(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.enabled = source["enabled"];
|
||||
this.baseUrl = source["baseUrl"];
|
||||
this.apiKey = source["apiKey"];
|
||||
this.timeoutSeconds = source["timeoutSeconds"];
|
||||
}
|
||||
}
|
||||
export class JVMJMXConfig {
|
||||
enabled?: boolean;
|
||||
host?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
domainAllowlist?: string[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new JVMJMXConfig(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.enabled = source["enabled"];
|
||||
this.host = source["host"];
|
||||
this.port = source["port"];
|
||||
this.username = source["username"];
|
||||
this.password = source["password"];
|
||||
this.domainAllowlist = source["domainAllowlist"];
|
||||
}
|
||||
}
|
||||
export class JVMConfig {
|
||||
environment?: string;
|
||||
readOnly?: boolean;
|
||||
allowedModes?: string[];
|
||||
preferredMode?: string;
|
||||
jmx?: JVMJMXConfig;
|
||||
endpoint?: JVMEndpointConfig;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new JVMConfig(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.environment = source["environment"];
|
||||
this.readOnly = source["readOnly"];
|
||||
this.allowedModes = source["allowedModes"];
|
||||
this.preferredMode = source["preferredMode"];
|
||||
this.jmx = this.convertValues(source["jmx"], JVMJMXConfig);
|
||||
this.endpoint = this.convertValues(source["endpoint"], JVMEndpointConfig);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class HTTPTunnelConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
@@ -549,6 +629,7 @@ export namespace connection {
|
||||
mongoAuthMechanism?: string;
|
||||
mongoReplicaUser?: string;
|
||||
mongoReplicaPassword?: string;
|
||||
jvm?: JVMConfig;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ConnectionConfig(source);
|
||||
@@ -590,6 +671,7 @@ export namespace connection {
|
||||
this.mongoAuthMechanism = source["mongoAuthMechanism"];
|
||||
this.mongoReplicaUser = source["mongoReplicaUser"];
|
||||
this.mongoReplicaPassword = source["mongoReplicaPassword"];
|
||||
this.jvm = this.convertValues(source["jvm"], JVMConfig);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@@ -638,6 +720,9 @@ export namespace connection {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export class QueryResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
|
||||
60
internal/app/methods_jvm.go
Normal file
60
internal/app/methods_jvm.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/jvm"
|
||||
)
|
||||
|
||||
var newJVMProvider = jvm.NewProvider
|
||||
|
||||
func (a *App) TestJVMConnection(cfg connection.ConnectionConfig) connection.QueryResult {
|
||||
normalized, err := jvm.NormalizeConnectionConfig(cfg)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
provider, err := newJVMProvider(normalized.JVM.PreferredMode)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
if err := provider.TestConnection(a.ctx, normalized); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
return connection.QueryResult{Success: true, Message: "JVM 连接成功"}
|
||||
}
|
||||
|
||||
func (a *App) JVMProbeCapabilities(cfg connection.ConnectionConfig) connection.QueryResult {
|
||||
normalized, err := jvm.NormalizeConnectionConfig(cfg)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
items := make([]jvm.Capability, 0, len(normalized.JVM.AllowedModes))
|
||||
for _, mode := range normalized.JVM.AllowedModes {
|
||||
provider, providerErr := newJVMProvider(mode)
|
||||
if providerErr != nil {
|
||||
items = append(items, jvm.Capability{
|
||||
Mode: mode,
|
||||
DisplayLabel: jvm.ModeDisplayLabel(mode),
|
||||
Reason: providerErr.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
caps, probeErr := provider.ProbeCapabilities(a.ctx, normalized)
|
||||
if probeErr != nil {
|
||||
items = append(items, jvm.Capability{
|
||||
Mode: mode,
|
||||
DisplayLabel: jvm.ModeDisplayLabel(mode),
|
||||
Reason: probeErr.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
items = append(items, caps...)
|
||||
}
|
||||
|
||||
return connection.QueryResult{Success: true, Data: items}
|
||||
}
|
||||
240
internal/app/methods_jvm_test.go
Normal file
240
internal/app/methods_jvm_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/jvm"
|
||||
)
|
||||
|
||||
type fakeJVMProvider struct {
|
||||
testErr error
|
||||
probe []jvm.Capability
|
||||
probeErr error
|
||||
list []jvm.ResourceSummary
|
||||
value jvm.ValueSnapshot
|
||||
apply jvm.ApplyResult
|
||||
}
|
||||
|
||||
func (f fakeJVMProvider) Mode() string { return jvm.ModeJMX }
|
||||
func (f fakeJVMProvider) TestConnection(context.Context, connection.ConnectionConfig) error {
|
||||
return f.testErr
|
||||
}
|
||||
func (f fakeJVMProvider) ProbeCapabilities(context.Context, connection.ConnectionConfig) ([]jvm.Capability, error) {
|
||||
return f.probe, f.probeErr
|
||||
}
|
||||
func (f fakeJVMProvider) ListResources(context.Context, connection.ConnectionConfig, string) ([]jvm.ResourceSummary, error) {
|
||||
return f.list, nil
|
||||
}
|
||||
func (f fakeJVMProvider) GetValue(context.Context, connection.ConnectionConfig, string) (jvm.ValueSnapshot, error) {
|
||||
return f.value, nil
|
||||
}
|
||||
func (f fakeJVMProvider) PreviewChange(context.Context, connection.ConnectionConfig, jvm.ChangeRequest) (jvm.ChangePreview, error) {
|
||||
return jvm.ChangePreview{Allowed: true, Summary: "preview"}, nil
|
||||
}
|
||||
func (f fakeJVMProvider) ApplyChange(context.Context, connection.ConnectionConfig, jvm.ChangeRequest) (jvm.ApplyResult, error) {
|
||||
return f.apply, nil
|
||||
}
|
||||
|
||||
func swapJVMProviderFactory(factory func(mode string) (jvm.Provider, error)) func() {
|
||||
prev := newJVMProvider
|
||||
newJVMProvider = factory
|
||||
return func() { newJVMProvider = prev }
|
||||
}
|
||||
|
||||
func TestTestJVMConnectionUsesPreferredProvider(t *testing.T) {
|
||||
app := NewAppWithSecretStore(nil)
|
||||
var gotMode string
|
||||
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
|
||||
gotMode = mode
|
||||
return fakeJVMProvider{}, nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
res := app.TestJVMConnection(connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
Host: "orders.internal",
|
||||
JVM: connection.JVMConfig{
|
||||
PreferredMode: "endpoint",
|
||||
AllowedModes: []string{"jmx", "endpoint"},
|
||||
},
|
||||
})
|
||||
|
||||
if !res.Success {
|
||||
t.Fatalf("expected success, got %+v", res)
|
||||
}
|
||||
if gotMode != "endpoint" {
|
||||
t.Fatalf("expected provider mode endpoint, got %q", gotMode)
|
||||
}
|
||||
if res.Message != "JVM 连接成功" {
|
||||
t.Fatalf("expected success message %q, got %q", "JVM 连接成功", res.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestJVMConnectionReturnsProviderError(t *testing.T) {
|
||||
app := NewAppWithSecretStore(nil)
|
||||
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
|
||||
return fakeJVMProvider{testErr: errors.New("dial failed")}, nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
res := app.TestJVMConnection(connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
Host: "orders.internal",
|
||||
JVM: connection.JVMConfig{
|
||||
PreferredMode: "jmx",
|
||||
AllowedModes: []string{"jmx"},
|
||||
},
|
||||
})
|
||||
|
||||
if res.Success {
|
||||
t.Fatalf("expected failure, got %+v", res)
|
||||
}
|
||||
if res.Message != "dial failed" {
|
||||
t.Fatalf("expected message %q, got %q", "dial failed", res.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestJVMConnectionReturnsProviderFactoryError(t *testing.T) {
|
||||
app := NewAppWithSecretStore(nil)
|
||||
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
|
||||
return nil, errors.New("factory unavailable")
|
||||
})
|
||||
defer restore()
|
||||
|
||||
res := app.TestJVMConnection(connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
Host: "orders.internal",
|
||||
JVM: connection.JVMConfig{
|
||||
PreferredMode: "endpoint",
|
||||
AllowedModes: []string{"endpoint"},
|
||||
},
|
||||
})
|
||||
|
||||
if res.Success {
|
||||
t.Fatalf("expected failure, got %+v", res)
|
||||
}
|
||||
if res.Message != "factory unavailable" {
|
||||
t.Fatalf("expected message %q, got %q", "factory unavailable", res.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJVMProbeCapabilitiesReturnsCapabilityArray(t *testing.T) {
|
||||
app := NewAppWithSecretStore(nil)
|
||||
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
|
||||
return fakeJVMProvider{
|
||||
probe: []jvm.Capability{{Mode: jvm.ModeJMX, CanBrowse: true, CanWrite: false, CanPreview: false, DisplayLabel: "JMX"}},
|
||||
}, nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
res := app.JVMProbeCapabilities(connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
Host: "orders.internal",
|
||||
JVM: connection.JVMConfig{
|
||||
PreferredMode: "jmx",
|
||||
AllowedModes: []string{"jmx"},
|
||||
},
|
||||
})
|
||||
|
||||
if !res.Success {
|
||||
t.Fatalf("expected success, got %+v", res)
|
||||
}
|
||||
items, ok := res.Data.([]jvm.Capability)
|
||||
if !ok || len(items) != 1 {
|
||||
t.Fatalf("expected one capability, got %#v", res.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJVMProbeCapabilitiesIncludesReasonWhenProbeFails(t *testing.T) {
|
||||
app := NewAppWithSecretStore(nil)
|
||||
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
|
||||
return fakeJVMProvider{
|
||||
probeErr: errors.New("probe failed"),
|
||||
}, nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
res := app.JVMProbeCapabilities(connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
Host: "orders.internal",
|
||||
JVM: connection.JVMConfig{
|
||||
PreferredMode: "jmx",
|
||||
AllowedModes: []string{"jmx"},
|
||||
},
|
||||
})
|
||||
|
||||
if !res.Success {
|
||||
t.Fatalf("expected success, got %+v", res)
|
||||
}
|
||||
items, ok := res.Data.([]jvm.Capability)
|
||||
if !ok || len(items) != 1 {
|
||||
t.Fatalf("expected one capability, got %#v", res.Data)
|
||||
}
|
||||
if items[0].Reason != "probe failed" {
|
||||
t.Fatalf("expected reason %q, got %#v", "probe failed", items[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestJVMProbeCapabilitiesIncludesReasonWhenProviderFactoryFails(t *testing.T) {
|
||||
app := NewAppWithSecretStore(nil)
|
||||
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
|
||||
return nil, errors.New("provider disabled")
|
||||
})
|
||||
defer restore()
|
||||
|
||||
res := app.JVMProbeCapabilities(connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
Host: "orders.internal",
|
||||
JVM: connection.JVMConfig{
|
||||
PreferredMode: "endpoint",
|
||||
AllowedModes: []string{"endpoint"},
|
||||
},
|
||||
})
|
||||
|
||||
if !res.Success {
|
||||
t.Fatalf("expected success, got %+v", res)
|
||||
}
|
||||
items, ok := res.Data.([]jvm.Capability)
|
||||
if !ok || len(items) != 1 {
|
||||
t.Fatalf("expected one capability, got %#v", res.Data)
|
||||
}
|
||||
if items[0].Reason != "provider disabled" {
|
||||
t.Fatalf("expected reason %q, got %#v", "provider disabled", items[0])
|
||||
}
|
||||
if items[0].DisplayLabel != "Endpoint" {
|
||||
t.Fatalf("expected display label %q, got %#v", "Endpoint", items[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestJVMProbeCapabilitiesUsesReadableLabelForUnsupportedMode(t *testing.T) {
|
||||
app := NewAppWithSecretStore(nil)
|
||||
restore := swapJVMProviderFactory(jvm.NewProvider)
|
||||
defer restore()
|
||||
|
||||
res := app.JVMProbeCapabilities(connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
Host: "orders.internal",
|
||||
JVM: connection.JVMConfig{
|
||||
PreferredMode: "agent",
|
||||
AllowedModes: []string{"agent"},
|
||||
},
|
||||
})
|
||||
|
||||
if !res.Success {
|
||||
t.Fatalf("expected success, got %+v", res)
|
||||
}
|
||||
items, ok := res.Data.([]jvm.Capability)
|
||||
if !ok || len(items) != 1 {
|
||||
t.Fatalf("expected one capability, got %#v", res.Data)
|
||||
}
|
||||
if items[0].DisplayLabel != "Agent" {
|
||||
t.Fatalf("expected display label %q, got %#v", "Agent", items[0])
|
||||
}
|
||||
if !strings.Contains(items[0].Reason, "unsupported jvm provider mode") {
|
||||
t.Fatalf("expected unsupported mode error, got %#v", items[0])
|
||||
}
|
||||
}
|
||||
93
internal/jvm/http_provider.go
Normal file
93
internal/jvm/http_provider.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package jvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
type HTTPProvider struct{}
|
||||
|
||||
func NewHTTPProvider() Provider { return &HTTPProvider{} }
|
||||
|
||||
func (p *HTTPProvider) Mode() string { return ModeEndpoint }
|
||||
|
||||
func (p *HTTPProvider) TestConnection(ctx context.Context, cfg connection.ConnectionConfig) error {
|
||||
baseURL := strings.TrimSpace(cfg.JVM.Endpoint.BaseURL)
|
||||
if baseURL == "" {
|
||||
return fmt.Errorf("endpoint baseURL is required")
|
||||
}
|
||||
parsed, err := url.Parse(baseURL)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return fmt.Errorf("endpoint baseURL is invalid: %s", baseURL)
|
||||
}
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return fmt.Errorf("endpoint scheme is unsupported: %s", parsed.Scheme)
|
||||
}
|
||||
|
||||
timeout := time.Duration(cfg.JVM.Endpoint.TimeoutSeconds) * time.Second
|
||||
if timeout <= 0 {
|
||||
timeout = time.Duration(cfg.Timeout) * time.Second
|
||||
}
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
client := &http.Client{Timeout: timeout}
|
||||
resp, err := doEndpointProbe(ctx, client, baseURL, http.MethodHead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotImplemented {
|
||||
_ = resp.Body.Close()
|
||||
resp, err = doEndpointProbe(ctx, client, baseURL, http.MethodGet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if isReachableStatus(resp.StatusCode) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("endpoint returned unexpected status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
func (p *HTTPProvider) ProbeCapabilities(ctx context.Context, cfg connection.ConnectionConfig) ([]Capability, error) {
|
||||
return []Capability{{Mode: ModeEndpoint, CanBrowse: true, CanWrite: true, CanPreview: true, DisplayLabel: "Endpoint"}}, nil
|
||||
}
|
||||
|
||||
func (p *HTTPProvider) ListResources(ctx context.Context, cfg connection.ConnectionConfig, parentPath string) ([]ResourceSummary, error) {
|
||||
return nil, errProviderNotImplemented(p.Mode(), "list resources")
|
||||
}
|
||||
|
||||
func (p *HTTPProvider) GetValue(ctx context.Context, cfg connection.ConnectionConfig, resourcePath string) (ValueSnapshot, error) {
|
||||
return ValueSnapshot{}, errProviderNotImplemented(p.Mode(), "get value")
|
||||
}
|
||||
|
||||
func (p *HTTPProvider) PreviewChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ChangePreview, error) {
|
||||
return ChangePreview{}, errProviderNotImplemented(p.Mode(), "preview change")
|
||||
}
|
||||
|
||||
func (p *HTTPProvider) ApplyChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ApplyResult, error) {
|
||||
return ApplyResult{}, errProviderNotImplemented(p.Mode(), "apply change")
|
||||
}
|
||||
|
||||
func doEndpointProbe(ctx context.Context, client *http.Client, baseURL string, method string) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, baseURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("endpoint request build failed: %w", err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("endpoint request failed: %w", err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func isReachableStatus(statusCode int) bool {
|
||||
return (statusCode >= 200 && statusCode < 400) || statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden
|
||||
}
|
||||
64
internal/jvm/jmx_provider.go
Normal file
64
internal/jvm/jmx_provider.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package jvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
type JMXProvider struct{}
|
||||
|
||||
func NewJMXProvider() Provider { return &JMXProvider{} }
|
||||
|
||||
func (p *JMXProvider) Mode() string { return ModeJMX }
|
||||
|
||||
func (p *JMXProvider) TestConnection(ctx context.Context, cfg connection.ConnectionConfig) error {
|
||||
host := strings.TrimSpace(cfg.JVM.JMX.Host)
|
||||
if host == "" {
|
||||
host = strings.TrimSpace(cfg.Host)
|
||||
}
|
||||
if host == "" {
|
||||
return fmt.Errorf("jmx host is required")
|
||||
}
|
||||
port := cfg.JVM.JMX.Port
|
||||
if port <= 0 {
|
||||
return fmt.Errorf("jmx port is invalid: %d", port)
|
||||
}
|
||||
|
||||
timeout := time.Duration(cfg.Timeout) * time.Second
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
dialer := net.Dialer{Timeout: timeout}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("jmx tcp connect failed: %w", err)
|
||||
}
|
||||
_ = conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *JMXProvider) ProbeCapabilities(ctx context.Context, cfg connection.ConnectionConfig) ([]Capability, error) {
|
||||
return []Capability{{Mode: ModeJMX, CanBrowse: true, CanWrite: false, CanPreview: false, DisplayLabel: "JMX"}}, nil
|
||||
}
|
||||
|
||||
func (p *JMXProvider) ListResources(ctx context.Context, cfg connection.ConnectionConfig, parentPath string) ([]ResourceSummary, error) {
|
||||
return nil, errProviderNotImplemented(p.Mode(), "list resources")
|
||||
}
|
||||
|
||||
func (p *JMXProvider) GetValue(ctx context.Context, cfg connection.ConnectionConfig, resourcePath string) (ValueSnapshot, error) {
|
||||
return ValueSnapshot{}, errProviderNotImplemented(p.Mode(), "get value")
|
||||
}
|
||||
|
||||
func (p *JMXProvider) PreviewChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ChangePreview, error) {
|
||||
return ChangePreview{}, errProviderNotImplemented(p.Mode(), "preview change")
|
||||
}
|
||||
|
||||
func (p *JMXProvider) ApplyChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ApplyResult, error) {
|
||||
return ApplyResult{}, errProviderNotImplemented(p.Mode(), "apply change")
|
||||
}
|
||||
57
internal/jvm/provider.go
Normal file
57
internal/jvm/provider.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package jvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
Mode() string
|
||||
TestConnection(ctx context.Context, cfg connection.ConnectionConfig) error
|
||||
ProbeCapabilities(ctx context.Context, cfg connection.ConnectionConfig) ([]Capability, error)
|
||||
ListResources(ctx context.Context, cfg connection.ConnectionConfig, parentPath string) ([]ResourceSummary, error)
|
||||
GetValue(ctx context.Context, cfg connection.ConnectionConfig, resourcePath string) (ValueSnapshot, error)
|
||||
PreviewChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ChangePreview, error)
|
||||
ApplyChange(ctx context.Context, cfg connection.ConnectionConfig, req ChangeRequest) (ApplyResult, error)
|
||||
}
|
||||
|
||||
var providerFactories = map[string]func() Provider{
|
||||
ModeJMX: func() Provider { return NewJMXProvider() },
|
||||
ModeEndpoint: func() Provider { return NewHTTPProvider() },
|
||||
}
|
||||
|
||||
func NewProvider(mode string) (Provider, error) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(mode))
|
||||
factory, ok := providerFactories[normalized]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported jvm provider mode: %s", mode)
|
||||
}
|
||||
return factory(), nil
|
||||
}
|
||||
|
||||
func ModeDisplayLabel(mode string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||||
case ModeJMX:
|
||||
return "JMX"
|
||||
case ModeEndpoint:
|
||||
return "Endpoint"
|
||||
case ModeAgent:
|
||||
return "Agent"
|
||||
default:
|
||||
normalized := strings.TrimSpace(mode)
|
||||
if normalized == "" {
|
||||
return "Unknown"
|
||||
}
|
||||
if len(normalized) == 1 {
|
||||
return strings.ToUpper(normalized)
|
||||
}
|
||||
return strings.ToUpper(normalized[:1]) + strings.ToLower(normalized[1:])
|
||||
}
|
||||
}
|
||||
|
||||
func errProviderNotImplemented(mode string, action string) error {
|
||||
return fmt.Errorf("%s provider does not implement %s yet", ModeDisplayLabel(mode), action)
|
||||
}
|
||||
102
internal/jvm/provider_contract_test.go
Normal file
102
internal/jvm/provider_contract_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package jvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
func TestJMXProviderTestConnectionReturnsErrorWhenHostMissing(t *testing.T) {
|
||||
provider := NewJMXProvider()
|
||||
|
||||
err := provider.TestConnection(context.Background(), connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
JVM: connection.JVMConfig{
|
||||
JMX: connection.JVMJMXConfig{
|
||||
Port: 9010,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error when jmx host is missing")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "jmx host is required") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJMXProviderTestConnectionReturnsErrorWhenPortInvalid(t *testing.T) {
|
||||
provider := NewJMXProvider()
|
||||
|
||||
err := provider.TestConnection(context.Background(), connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
Host: "orders.internal",
|
||||
JVM: connection.JVMConfig{
|
||||
JMX: connection.JVMJMXConfig{
|
||||
Port: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error when jmx port is invalid")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "jmx port is invalid") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPProviderTestConnectionReturnsErrorWhenBaseURLMissing(t *testing.T) {
|
||||
provider := NewHTTPProvider()
|
||||
|
||||
err := provider.TestConnection(context.Background(), connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
JVM: connection.JVMConfig{
|
||||
Endpoint: connection.JVMEndpointConfig{
|
||||
BaseURL: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error when endpoint baseURL is missing")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "endpoint baseURL is required") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPProviderTestConnectionReturnsErrorWhenBaseURLInvalid(t *testing.T) {
|
||||
provider := NewHTTPProvider()
|
||||
|
||||
err := provider.TestConnection(context.Background(), connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
JVM: connection.JVMConfig{
|
||||
Endpoint: connection.JVMEndpointConfig{
|
||||
BaseURL: "://bad-url",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error when endpoint baseURL is invalid")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "endpoint baseURL is invalid") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJMXProviderListResourcesReturnsNotImplementedError(t *testing.T) {
|
||||
provider := NewJMXProvider()
|
||||
|
||||
_, err := provider.ListResources(context.Background(), connection.ConnectionConfig{}, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected not implemented error")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "does not implement") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user