feat: centralize scattered configs and add dws config list

This commit is contained in:
fantiu
2026-04-12 14:23:11 +08:00
parent 1f0a75f836
commit b78dd19cf9
16 changed files with 861 additions and 0 deletions
+11
View File
@@ -17,9 +17,20 @@ import (
"os"
"path/filepath"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
func init() {
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_CONFIG_DIR",
Category: configmeta.CategoryCore,
Description: "覆盖默认配置目录 (~/.dws)",
DefaultValue: "~/.dws",
Example: "/opt/dws/config",
})
}
// Build-time variables injected via ldflags when available.
var (
buildTime = "unknown"
+165
View File
@@ -0,0 +1,165 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package app
import (
"fmt"
"text/tabwriter"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
"github.com/spf13/cobra"
)
func newConfigCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "配置管理",
Long: "管理 DWS CLI 的配置项。查看所有支持的环境变量及其当前值。",
Args: cobra.NoArgs,
TraverseChildren: true,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
cmd.AddCommand(newConfigListCommand())
return cmd
}
func newConfigListCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "列出所有可用配置项",
Long: "显示 DWS CLI 支持的全部环境变量配置项,包括名称、分类、描述和默认值。",
RunE: runConfigList,
}
cmd.Flags().String("category", "", "按分类过滤 (core|auth|network|security|runtime|debug|external)")
cmd.Flags().Bool("show-values", false, "显示配置项的当前实际值 (敏感信息会脱敏)")
cmd.Flags().Bool("show-hidden", false, "包含隐藏的内部调试配置项")
cmd.Flags().Bool("json", false, "以 JSON 格式输出")
return cmd
}
func runConfigList(cmd *cobra.Command, _ []string) error {
category, _ := cmd.Flags().GetString("category")
showValues, _ := cmd.Flags().GetBool("show-values")
showHidden, _ := cmd.Flags().GetBool("show-hidden")
jsonOut, _ := cmd.Flags().GetBool("json")
var items []configmeta.ConfigItem
if category != "" {
items = configmeta.ByCategory(configmeta.Category(category))
} else {
items = configmeta.All()
}
if !showHidden {
items = filterVisible(items)
}
if jsonOut {
return writeConfigJSON(cmd, items, showValues)
}
return writeConfigTable(cmd, items, showValues)
}
func filterVisible(items []configmeta.ConfigItem) []configmeta.ConfigItem {
out := make([]configmeta.ConfigItem, 0, len(items))
for _, item := range items {
if !item.Hidden {
out = append(out, item)
}
}
return out
}
func writeConfigJSON(cmd *cobra.Command, items []configmeta.ConfigItem, showValues bool) error {
type jsonItem struct {
Name string `json:"name"`
Category string `json:"category"`
Description string `json:"description"`
DefaultValue string `json:"default_value,omitempty"`
Example string `json:"example,omitempty"`
Sensitive bool `json:"sensitive,omitempty"`
CurrentValue string `json:"current_value,omitempty"`
IsSet bool `json:"is_set"`
}
result := make([]jsonItem, 0, len(items))
for _, item := range items {
ji := jsonItem{
Name: item.Name,
Category: string(item.Category),
Description: item.Description,
DefaultValue: item.DefaultValue,
Example: item.Example,
Sensitive: item.Sensitive,
}
val, ok := configmeta.Resolve(item.Name)
ji.IsSet = ok
if showValues && ok {
ji.CurrentValue = val
}
result = append(result, ji)
}
return output.WriteJSON(cmd.OutOrStdout(), map[string]any{
"kind": "config_list",
"count": len(result),
"configs": result,
})
}
func writeConfigTable(cmd *cobra.Command, items []configmeta.ConfigItem, showValues bool) error {
w := cmd.OutOrStdout()
if len(items) == 0 {
_, _ = fmt.Fprintln(w, "没有找到匹配的配置项。")
return nil
}
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
if showValues {
_, _ = fmt.Fprintln(tw, "分类\t配置项\t描述\t默认值\t当前值")
_, _ = fmt.Fprintln(tw, "────\t──────\t────\t──────\t──────")
} else {
_, _ = fmt.Fprintln(tw, "分类\t配置项\t描述\t默认值")
_, _ = fmt.Fprintln(tw, "────\t──────\t────\t──────")
}
for _, item := range items {
def := item.DefaultValue
if def == "" {
def = "(空)"
}
if showValues {
val, ok := configmeta.Resolve(item.Name)
display := "(未设置)"
if ok {
display = val
}
_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n",
item.Category, item.Name, item.Description, def, display)
} else {
_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n",
item.Category, item.Name, item.Description, def)
}
}
_ = tw.Flush()
_, _ = fmt.Fprintf(w, "\n共 %d 个配置项。使用 --show-values 查看当前值,--show-hidden 显示隐藏项。\n", len(items))
return nil
}
+177
View File
@@ -0,0 +1,177 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package app
import (
"bytes"
"encoding/json"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
)
func seedTestConfig(t *testing.T) {
t.Helper()
configmeta.Reset()
t.Cleanup(configmeta.Reset)
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_CONFIG_DIR", Category: configmeta.CategoryCore,
Description: "覆盖默认配置目录", DefaultValue: "~/.dws",
})
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_CLIENT_SECRET", Category: configmeta.CategoryAuth,
Description: "OAuth AppSecret", Sensitive: true,
})
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_CATALOG_FIXTURE", Category: configmeta.CategoryDebug,
Description: "目录 Fixture 路径", Hidden: true,
})
}
func TestConfigListTable(t *testing.T) {
seedTestConfig(t)
cmd := newConfigListCommand()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetArgs([]string{})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
out := buf.String()
if !strings.Contains(out, "DWS_CONFIG_DIR") {
t.Error("expected DWS_CONFIG_DIR in output")
}
if !strings.Contains(out, "DWS_CLIENT_SECRET") {
t.Error("expected DWS_CLIENT_SECRET in output")
}
// Hidden items should be excluded by default
if strings.Contains(out, "DWS_CATALOG_FIXTURE") {
t.Error("expected DWS_CATALOG_FIXTURE to be hidden")
}
}
func TestConfigListShowHidden(t *testing.T) {
seedTestConfig(t)
cmd := newConfigListCommand()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetArgs([]string{"--show-hidden"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
out := buf.String()
if !strings.Contains(out, "DWS_CATALOG_FIXTURE") {
t.Error("expected DWS_CATALOG_FIXTURE with --show-hidden")
}
}
func TestConfigListCategory(t *testing.T) {
seedTestConfig(t)
cmd := newConfigListCommand()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetArgs([]string{"--category", "auth"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
out := buf.String()
if !strings.Contains(out, "DWS_CLIENT_SECRET") {
t.Error("expected DWS_CLIENT_SECRET for auth category")
}
if strings.Contains(out, "DWS_CONFIG_DIR") {
t.Error("DWS_CONFIG_DIR should not appear for auth category")
}
}
func TestConfigListJSON(t *testing.T) {
seedTestConfig(t)
cmd := newConfigListCommand()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetArgs([]string{"--json", "--show-hidden"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
var result map[string]any
if err := json.Unmarshal(buf.Bytes(), &result); err != nil {
t.Fatalf("invalid JSON output: %v", err)
}
if result["kind"] != "config_list" {
t.Errorf("expected kind=config_list, got %v", result["kind"])
}
count, ok := result["count"].(float64)
if !ok || count != 3 {
t.Errorf("expected count=3, got %v", result["count"])
}
}
func TestConfigListShowValues(t *testing.T) {
seedTestConfig(t)
t.Setenv("DWS_CONFIG_DIR", "/custom/dir")
t.Setenv("DWS_CLIENT_SECRET", "supersecret123")
cmd := newConfigListCommand()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetArgs([]string{"--show-values"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
out := buf.String()
if !strings.Contains(out, "/custom/dir") {
t.Error("expected actual value for DWS_CONFIG_DIR")
}
if strings.Contains(out, "supersecret123") {
t.Error("sensitive value should be masked")
}
if !strings.Contains(out, "当前值") {
t.Error("expected '当前值' column header")
}
}
func TestConfigListEmpty(t *testing.T) {
configmeta.Reset()
defer configmeta.Reset()
cmd := newConfigListCommand()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetArgs([]string{})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
out := buf.String()
if !strings.Contains(out, "没有找到") {
t.Error("expected empty message")
}
}
+2
View File
@@ -274,6 +274,7 @@ func NewRootCommandWithEngine(rootCtx context.Context, engine *pipeline.Engine)
newAuthCommand(),
newSkillCommand(),
newCacheCommand(),
newConfigCommand(),
newCompletionCommand(root),
newRecoveryCommand(rootCtx, loader, flags),
newUpgradeCommand(),
@@ -629,6 +630,7 @@ func hideNonDirectRuntimeCommands(root *cobra.Command) {
staticCommands := map[string]bool{
"auth": true,
"cache": true,
"config": true,
"completion": true,
"skill": true,
"version": true,
+42
View File
@@ -33,9 +33,51 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/logging"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/safety"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
func init() {
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_RUNTIME_CONTENT_SCAN",
Category: configmeta.CategoryRuntime,
Description: "启用 MCP 响应内容安全扫描",
Example: "true",
})
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_RUNTIME_CONTENT_SCAN_ENFORCE",
Category: configmeta.CategoryRuntime,
Description: "内容安全扫描发现问题时阻断响应",
Example: "true",
})
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_RUNTIME_CONTENT_SCAN_REPORT",
Category: configmeta.CategoryRuntime,
Description: "在 JSON 输出中包含安全扫描报告",
Example: "true",
})
configmeta.Register(configmeta.ConfigItem{
Name: "DINGTALK_AGENT",
Category: configmeta.CategoryExternal,
Description: "MCP 请求 x-dingtalk-agent 头",
})
configmeta.Register(configmeta.ConfigItem{
Name: "DINGTALK_TRACE_ID",
Category: configmeta.CategoryExternal,
Description: "MCP 请求 x-dingtalk-trace-id 头",
})
configmeta.Register(configmeta.ConfigItem{
Name: "DINGTALK_SESSION_ID",
Category: configmeta.CategoryExternal,
Description: "MCP 请求 x-dingtalk-session-id 头",
})
configmeta.Register(configmeta.ConfigItem{
Name: "DINGTALK_MESSAGE_ID",
Category: configmeta.CategoryExternal,
Description: "MCP 请求 x-dingtalk-message-id 头",
})
}
const (
runtimeContentScanEnv = "DWS_RUNTIME_CONTENT_SCAN"
runtimeContentScanEnforceEnv = "DWS_RUNTIME_CONTENT_SCAN_ENFORCE"
+11
View File
@@ -29,9 +29,20 @@ import (
authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
"github.com/spf13/cobra"
)
func init() {
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_SKILL_API_HOST",
Category: configmeta.CategoryNetwork,
Description: "覆盖 Skill API 地址",
DefaultValue: "https://mcp.dingtalk.com",
Example: "https://custom-mcp.example.com",
})
}
const (
// legacySkillAPIHost is the legacy skill market host used by the old cli.
legacySkillAPIHost = "https://mcp.dingtalk.com"
+11
View File
@@ -21,8 +21,19 @@ import (
"sort"
"sync"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
)
func init() {
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_PERF_DEBUG",
Category: configmeta.CategoryDebug,
Description: "启用性能计时输出到 stderr",
Example: "1",
})
}
// PerfDebugEnv is the environment variable to enable performance timing output.
const PerfDebugEnv = "DWS_PERF_DEBUG"
+18
View File
@@ -19,9 +19,27 @@ import (
"strings"
"sync"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
func init() {
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_CLIENT_ID",
Category: configmeta.CategoryAuth,
Description: "OAuth AppKey (DingTalk 应用凭证)",
DefaultValue: "(内置)",
Sensitive: true,
})
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_CLIENT_SECRET",
Category: configmeta.CategoryAuth,
Description: "OAuth AppSecret (DingTalk 应用凭证)",
DefaultValue: "(内置)",
Sensitive: true,
})
}
const (
// AuthorizeURL is the DingTalk OAuth authorization page.
AuthorizeURL = "https://login.dingtalk.com/oauth2/auth"
+18
View File
@@ -28,9 +28,27 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/market"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/config"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
func init() {
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_CACHE_DIR",
Category: configmeta.CategoryCore,
Description: "覆盖缓存目录",
DefaultValue: "~/.dws/cache",
Example: "/tmp/dws-cache",
})
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_CATALOG_FIXTURE",
Category: configmeta.CategoryDebug,
Description: "使用本地 JSON 文件替代在线目录发现",
Example: "/path/to/catalog.json",
Hidden: true,
})
}
// CatalogDegradedReason identifies why catalog discovery returned empty.
type CatalogDegradedReason string
+16
View File
@@ -27,8 +27,24 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cache"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/market"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
)
func init() {
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_TENANT",
Category: configmeta.CategoryCore,
Description: "缓存分区的租户标识",
DefaultValue: "default",
})
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_AUTH_IDENTITY",
Category: configmeta.CategorySecurity,
Description: "缓存分区的认证身份标识",
DefaultValue: "default",
})
}
const (
tenantEnv = "DWS_TENANT"
authIdentityEnv = "DWS_AUTH_IDENTITY"
+18
View File
@@ -20,9 +20,27 @@ import (
"strings"
registryassets "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/registry"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
"gopkg.in/yaml.v3"
)
func init() {
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_SKILLS_PERSONAS_FILE",
Category: configmeta.CategoryDebug,
Description: "覆盖内置 personas.yaml 的本地文件路径",
Example: "/path/to/personas.yaml",
Hidden: true,
})
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_SKILLS_RECIPES_FILE",
Category: configmeta.CategoryDebug,
Description: "覆盖内置 recipes.yaml 的本地文件路径",
Example: "/path/to/recipes.yaml",
Hidden: true,
})
}
const (
PersonaRegistryPathEnv = "DWS_SKILLS_PERSONAS_FILE"
RecipeRegistryPathEnv = "DWS_SKILLS_RECIPES_FILE"
+11
View File
@@ -37,9 +37,20 @@ import (
"strings"
"sync"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
"golang.org/x/text/language"
)
func init() {
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_LANG",
Category: configmeta.CategoryCore,
Description: "界面语言 (en/zh),回退到 LANG",
DefaultValue: "en",
Example: "zh",
})
}
//go:embed locales/*.json
var localeFS embed.FS
+18
View File
@@ -34,9 +34,27 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/i18n"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/logging"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/config"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/validate"
)
func init() {
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_ALLOW_HTTP_ENDPOINTS",
Category: configmeta.CategorySecurity,
Description: "允许非 HTTPS 的 MCP 端点 (仅限 loopback)",
DefaultValue: "(禁用)",
Example: "1",
})
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_TRUSTED_DOMAINS",
Category: configmeta.CategoryNetwork,
Description: "信任的 HTTPS 域名白名单 (逗号分隔,* 信任所有)",
DefaultValue: "*.dingtalk.com",
Example: "*.dingtalk.com,custom.example.com",
})
}
const (
trustedDomainsEnv = "DWS_TRUSTED_DOMAINS"
defaultTrustedDomains = "*.dingtalk.com"
+24
View File
@@ -14,8 +14,32 @@ import (
"runtime"
"strings"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/configmeta"
)
func init() {
configmeta.Register(configmeta.ConfigItem{
Name: "DWS_UPGRADE_URL",
Category: configmeta.CategoryNetwork,
Description: "覆盖 GitHub API 地址 (镜像/测试)",
DefaultValue: "https://api.github.com",
Example: "https://mirror.example.com/api",
})
configmeta.Register(configmeta.ConfigItem{
Name: "GITHUB_TOKEN",
Category: configmeta.CategoryExternal,
Description: "GitHub API Token (提升 API 限额)",
Sensitive: true,
})
configmeta.Register(configmeta.ConfigItem{
Name: "GH_TOKEN",
Category: configmeta.CategoryExternal,
Description: "GitHub API Token 备选 (GITHUB_TOKEN 为空时使用)",
Sensitive: true,
})
}
const (
gitHubAPIBase = "https://api.github.com"
defaultOwner = "DingTalk-Real-AI"
+155
View File
@@ -0,0 +1,155 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package configmeta provides a central registry of all environment-variable
// based configuration items used by the DWS CLI. Each package registers its
// own items via init(), and the "dws config list" command reads the registry
// to present a unified view to the developer.
package configmeta
import (
"os"
"sort"
"strings"
"sync"
)
// Category groups related configuration items for display purposes.
type Category string
const (
CategoryCore Category = "core"
CategoryAuth Category = "auth"
CategoryNetwork Category = "network"
CategorySecurity Category = "security"
CategoryRuntime Category = "runtime"
CategoryDebug Category = "debug"
CategoryExternal Category = "external"
)
// categoryOrder defines the display order for categories.
var categoryOrder = map[Category]int{
CategoryCore: 0,
CategoryAuth: 1,
CategoryNetwork: 2,
CategorySecurity: 3,
CategoryRuntime: 4,
CategoryDebug: 5,
CategoryExternal: 6,
}
// ConfigItem describes a single environment-variable configuration item.
type ConfigItem struct {
Name string // Environment variable name, e.g. "DWS_CONFIG_DIR"
Category Category // Logical grouping
Description string // Short human-readable description
DefaultValue string // Description of the default value
Example string // Example value for documentation
Sensitive bool // If true, actual value is masked in output
Hidden bool // If true, omitted from default list output
}
var (
mu sync.RWMutex
items []ConfigItem
)
// Register adds a configuration item to the global registry.
// Duplicate names are silently ignored (first registration wins).
func Register(item ConfigItem) {
mu.Lock()
defer mu.Unlock()
for _, existing := range items {
if existing.Name == item.Name {
return
}
}
items = append(items, item)
}
// All returns every registered configuration item sorted by category
// (display order) then by name.
func All() []ConfigItem {
mu.RLock()
defer mu.RUnlock()
out := make([]ConfigItem, len(items))
copy(out, items)
sort.Slice(out, func(i, j int) bool {
ci, cj := categoryOrder[out[i].Category], categoryOrder[out[j].Category]
if ci != cj {
return ci < cj
}
return out[i].Name < out[j].Name
})
return out
}
// ByCategory returns registered items that match the given category.
func ByCategory(cat Category) []ConfigItem {
all := All()
var out []ConfigItem
for _, item := range all {
if item.Category == cat {
out = append(out, item)
}
}
return out
}
// Resolve returns the current value of the named environment variable.
// For sensitive items the value is masked. Returns ("", false) when the
// variable is not set.
func Resolve(name string) (string, bool) {
val, ok := os.LookupEnv(name)
if !ok {
return "", false
}
mu.RLock()
defer mu.RUnlock()
for _, item := range items {
if item.Name == name && item.Sensitive {
return maskValue(val), true
}
}
return val, true
}
// Categories returns all known category values in display order.
func Categories() []Category {
cats := make([]Category, 0, len(categoryOrder))
for c := range categoryOrder {
cats = append(cats, c)
}
sort.Slice(cats, func(i, j int) bool {
return categoryOrder[cats[i]] < categoryOrder[cats[j]]
})
return cats
}
func maskValue(v string) string {
if len(v) == 0 {
return ""
}
if len(v) <= 4 {
return strings.Repeat("*", len(v))
}
return v[:2] + strings.Repeat("*", len(v)-4) + v[len(v)-2:]
}
// Reset clears the registry. Intended for testing only.
func Reset() {
mu.Lock()
defer mu.Unlock()
items = nil
}
+164
View File
@@ -0,0 +1,164 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package configmeta
import (
"os"
"testing"
)
func TestRegisterAndAll(t *testing.T) {
Reset()
defer Reset()
Register(ConfigItem{Name: "ZZZ_LAST", Category: CategoryDebug, Description: "last"})
Register(ConfigItem{Name: "AAA_FIRST", Category: CategoryCore, Description: "first"})
Register(ConfigItem{Name: "MMM_MID", Category: CategoryAuth, Description: "mid"})
all := All()
if len(all) != 3 {
t.Fatalf("expected 3 items, got %d", len(all))
}
// core < auth < debug
if all[0].Name != "AAA_FIRST" {
t.Errorf("expected AAA_FIRST first, got %s", all[0].Name)
}
if all[1].Name != "MMM_MID" {
t.Errorf("expected MMM_MID second, got %s", all[1].Name)
}
if all[2].Name != "ZZZ_LAST" {
t.Errorf("expected ZZZ_LAST third, got %s", all[2].Name)
}
}
func TestRegisterDuplicateIgnored(t *testing.T) {
Reset()
defer Reset()
Register(ConfigItem{Name: "DUP", Category: CategoryCore, Description: "original"})
Register(ConfigItem{Name: "DUP", Category: CategoryCore, Description: "duplicate"})
all := All()
if len(all) != 1 {
t.Fatalf("expected 1 item, got %d", len(all))
}
if all[0].Description != "original" {
t.Errorf("expected original description, got %q", all[0].Description)
}
}
func TestByCategory(t *testing.T) {
Reset()
defer Reset()
Register(ConfigItem{Name: "A", Category: CategoryCore})
Register(ConfigItem{Name: "B", Category: CategoryAuth})
Register(ConfigItem{Name: "C", Category: CategoryCore})
core := ByCategory(CategoryCore)
if len(core) != 2 {
t.Fatalf("expected 2 core items, got %d", len(core))
}
empty := ByCategory(CategoryDebug)
if len(empty) != 0 {
t.Fatalf("expected 0 debug items, got %d", len(empty))
}
}
func TestResolveNonSensitive(t *testing.T) {
Reset()
defer Reset()
Register(ConfigItem{Name: "TEST_VAR_PLAIN", Category: CategoryCore})
t.Setenv("TEST_VAR_PLAIN", "hello")
val, ok := Resolve("TEST_VAR_PLAIN")
if !ok || val != "hello" {
t.Errorf("expected (hello, true), got (%q, %v)", val, ok)
}
}
func TestResolveSensitiveMasked(t *testing.T) {
Reset()
defer Reset()
Register(ConfigItem{Name: "TEST_SECRET", Category: CategoryAuth, Sensitive: true})
t.Setenv("TEST_SECRET", "abcdefgh")
val, ok := Resolve("TEST_SECRET")
if !ok {
t.Fatal("expected ok=true")
}
if val == "abcdefgh" {
t.Error("sensitive value should be masked")
}
// ab****gh
if val != "ab****gh" {
t.Errorf("unexpected masked value: %q", val)
}
}
func TestResolveUnset(t *testing.T) {
Reset()
defer Reset()
Register(ConfigItem{Name: "UNSET_VAR", Category: CategoryCore})
os.Unsetenv("UNSET_VAR")
_, ok := Resolve("UNSET_VAR")
if ok {
t.Error("expected ok=false for unset variable")
}
}
func TestMaskValue(t *testing.T) {
tests := []struct {
in, want string
}{
{"", ""},
{"ab", "**"},
{"abcd", "****"},
{"abcde", "ab*de"},
{"abcdefghij", "ab******ij"},
}
for _, tc := range tests {
got := maskValue(tc.in)
if got != tc.want {
t.Errorf("maskValue(%q) = %q, want %q", tc.in, got, tc.want)
}
}
}
func TestCategories(t *testing.T) {
cats := Categories()
if len(cats) != 7 {
t.Fatalf("expected 7 categories, got %d", len(cats))
}
if cats[0] != CategoryCore {
t.Errorf("expected core first, got %s", cats[0])
}
if cats[len(cats)-1] != CategoryExternal {
t.Errorf("expected external last, got %s", cats[len(cats)-1])
}
}
func TestReset(t *testing.T) {
Reset()
Register(ConfigItem{Name: "X", Category: CategoryCore})
Reset()
if len(All()) != 0 {
t.Error("expected empty registry after Reset")
}
}