Merge Go frontend (apiserver + xcecli) into meson build

This commit is contained in:
Rongsong Shen 2026-04-14 11:20:17 +08:00
parent 43dc12a36c
commit c9dc8c4676
205 changed files with 30793 additions and 328 deletions

View file

@ -0,0 +1,210 @@
package config
import (
"encoding/json"
"fmt"
"os"
"os/user"
"path"
"sort"
"time"
)
type APIServerInfo struct {
// Name string `json:"name"`
URL string `json:"url"`
Token string `json:"token"`
Path string `json:"path"`
CreateTime int64 `json:"createtime"`
CACert string `json:"cacert,omitempty"`
TokenExpireTime int64 `json:"token_expire_time"`
//Version string `json:"version,omitempty"`
}
// APIServerInfoArray is used to sort the APIServerInfo by CreateTime
type APIServerInfoArray []APIServerInfo
func (a APIServerInfoArray) Len() int { return len(a) }
func (a APIServerInfoArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a APIServerInfoArray) Less(i, j int) bool { return a[i].CreateTime > a[j].CreateTime }
type Config struct {
APIServerInfo APIServerInfoArray `json:"servers"`
}
// ConfigManager is used to manage the config
type ConfigManager struct {
config *Config
configPath string
}
// NewConfigManager is used to create the config manager
func NewConfigManager() (*ConfigManager, error) {
usr, err := user.Current()
if err != nil {
return nil, fmt.Errorf("failed to get the user info: [%v]", err)
}
configDir := path.Join(usr.HomeDir, ".xce")
configPath := path.Join(configDir, "config.json")
// create the config directory if it does not exist
if err := os.MkdirAll(configDir, 0700); err != nil {
return nil, fmt.Errorf("failed to create the config directory: [%v]", err)
}
cm := &ConfigManager{
configPath: configPath,
}
return cm, nil
}
// GetConfig is used to get the config
func (cm *ConfigManager) GetConfig() (*Config, error) {
if cm == nil {
return nil, fmt.Errorf("the config manager is not initialized")
}
if cm.config != nil {
return cm.config, nil
}
config, err := cm.loadConfig()
if err != nil {
return nil, err
}
cm.config = config
return config, nil
}
// SaveConfig is used to save the config
func (cm *ConfigManager) SaveConfig(config *Config) error {
// sort the APIServerInfoArray by CreateTime
sort.Sort(config.APIServerInfo)
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
dir := path.Dir(cm.configPath)
if err := os.MkdirAll(dir, 0700); err != nil {
return err
}
if err := os.WriteFile(cm.configPath, data, 0600); err != nil {
return err
}
cm.config = config
return nil
}
// loadConfig is used to load the config from the file
func (cm *ConfigManager) loadConfig() (*Config, error) {
// if the config file does not exist, create the default config
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
defaultConfig := &Config{
APIServerInfo: make([]APIServerInfo, 0),
}
// save the default config
if err := cm.SaveConfig(defaultConfig); err != nil {
return nil, fmt.Errorf("failed to create the default config: [%v]", err)
}
return defaultConfig, nil
}
// read the config file
data, err := os.ReadFile(cm.configPath)
if err != nil {
return nil, fmt.Errorf("failed to read the config file: [%v]", err)
}
// parse the config file
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse the config file: [%s,%v]", cm.configPath, err)
}
return &config, nil
}
// Save is used to save the config
func (c *Config) Save() error {
usr, err := user.Current()
if err != nil {
return err
}
// get the config directory
configDir := path.Join(usr.HomeDir, ".xce")
configPath := path.Join(configDir, "config.json")
// create the config directory if it does not exist
if err := os.MkdirAll(configDir, 0700); err != nil {
return err
}
// marshal the config
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
// write the config to the file
return os.WriteFile(configPath, data, 0600)
}
// IsAuthenticated check if the user has been authenticated by the API server
// if not, return an error
func (cm *ConfigManager) IsAuthenticated() (*APIServerInfo, error) {
cfg, err := cm.GetConfig()
if err != nil {
return nil, fmt.Errorf("failed to get config: [%v]", err)
}
if len(cfg.APIServerInfo) == 0 {
return nil, fmt.Errorf("you are not logged in, please log in first")
}
serverInfo := &cfg.APIServerInfo[0]
if serverInfo.URL == "" {
return nil, fmt.Errorf("no default API Server is set, please set a default server")
}
if serverInfo.Token == "" {
return nil, fmt.Errorf("you are not logged in, please log in first")
}
// Use the token expire time from server configuration
expireTime := serverInfo.TokenExpireTime
if expireTime == 0 {
expireTime = 86400 // Default to 24 hours if not set
}
if time.Now().Unix()-serverInfo.CreateTime > expireTime {
return nil, fmt.Errorf("your login has expired, please log in again")
}
return serverInfo, nil
}
// GetServerInfo
func (cm *ConfigManager) GetServerInfo() (*APIServerInfo, error) {
cfg, err := cm.GetConfig()
if err != nil {
return nil, fmt.Errorf("failed to get config: [%v]", err)
}
if len(cfg.APIServerInfo) == 0 || cfg.APIServerInfo[0].URL == "" {
return nil, fmt.Errorf("not found the API Server information")
}
return &cfg.APIServerInfo[0], nil
}
// NewConfigManagerWithPath creates a ConfigManager using a specific config file path.
// Used in tests.
func NewConfigManagerWithPath(path string) *ConfigManager {
return &ConfigManager{configPath: path}
}

View file

@ -0,0 +1,189 @@
package config
import (
"fmt"
"os"
"os/user"
"path/filepath"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
var (
Log *zap.Logger
)
const (
defaultLoggerConfigFile = "logger.yaml"
defaultLoggerOutputFile = "xcecli_out.log"
)
type LoggerConfig struct {
Logging struct {
Level string `yaml:"level"`
Encoding string `yaml:"encoding"`
OutputPath string `yaml:"outputPath"`
File struct {
MaxSize int `yaml:"maxSize"`
MaxBackups int `yaml:"maxBackups"`
MaxAge int `yaml:"maxAge"`
Compress bool `yaml:"compress"`
} `yaml:"file"`
EncoderConfig struct {
MessageKey string `yaml:"messageKey"`
LevelKey string `yaml:"levelKey"`
TimeKey string `yaml:"timeKey"`
NameKey string `yaml:"nameKey"`
CallerKey string `yaml:"callerKey"`
FunctionKey string `yaml:"functionKey"`
StacktraceKey string `yaml:"stacktraceKey"`
EncodeTime string `yaml:"timeEncoder"`
EncodeLevel string `yaml:"levelEncoder"`
} `yaml:"encoderConfig"`
} `yaml:"logging"`
}
func InitLogger() error {
var err error
err = loadLoggerConfig()
return err
}
func loadLoggerConfig() error {
usr, err := user.Current()
if err != nil {
return fmt.Errorf("failed to get the user info: [%v]", err)
}
// read logger configuration file
configPath := filepath.Join(usr.HomeDir, ".xce")
var loggerConfig LoggerConfig
v := viper.New()
v.SetDefault("logging.level", "info") // debug, info, warn, error, fatal
v.SetDefault("logging.encoding", "console") // console, json
v.SetDefault("logging.outputPath", defaultLoggerOutputFile) // xcecli_out.log
v.SetDefault("logging.file.maxSize", 100) // 100MB
v.SetDefault("logging.file.maxBackups", 10) // 10
v.SetDefault("logging.file.maxAge", 14) // 14 days
v.SetDefault("logging.file.compress", true) // true
v.SetDefault("logging.encoderConfig.messageKey", "message") // message
v.SetDefault("logging.encoderConfig.levelKey", "level") // level
v.SetDefault("logging.encoderConfig.timeKey", "timestamp") // timestamp
v.SetDefault("logging.encoderConfig.nameKey", "logger") // logger
v.SetDefault("logging.encoderConfig.callerKey", "caller") // caller
v.SetDefault("logging.encoderConfig.functionKey", "func") // func
v.SetDefault("logging.encoderConfig.stacktraceKey", "stacktrace") // stacktrace
v.SetDefault("logging.encoderConfig.timeEncoder", "ISO8601") // ISO8601
v.SetDefault("logging.encoderConfig.levelEncoder", "capital") // capital
v.SetConfigName(defaultLoggerConfigFile)
v.SetConfigType("yaml")
v.AddConfigPath(configPath)
// yamlContent := `# log level: debug, info, warn, error, fatal
// # debug: detailed log
// # info: normal log
// # warn: warning log
// # error: error log
// # fatal: fatal log` + v.GetString("logging.level")
err = v.ReadInConfig()
if err != nil {
os.MkdirAll(configPath, 0755)
if err = v.WriteConfigAs(filepath.Join(configPath, "logger.yaml")); err != nil {
fmt.Printf("failed to write default logger configuration file: [%v]", err)
}
}
err = v.Unmarshal(&loggerConfig)
if err != nil {
return fmt.Errorf("unable to decode into struct: [%v]", err)
}
// Handle both formats for OutputPath
if !filepath.IsAbs(loggerConfig.Logging.OutputPath) {
if loggerConfig.Logging.OutputPath == "" {
loggerConfig.Logging.OutputPath = defaultLoggerOutputFile
}
// If OutputPath is relative, prepend the logs directory
loggerConfig.Logging.OutputPath = filepath.Join(configPath, "logs", loggerConfig.Logging.OutputPath)
}
logDir := filepath.Dir(loggerConfig.Logging.OutputPath) // Extract the directory
// Create the directory if it does not exist
if err = os.MkdirAll(logDir, 0755); err != nil {
return fmt.Errorf("failed to create client logs directory: %w", err)
}
Log, err = createLogger(loggerConfig)
if err != nil {
return fmt.Errorf("failed to create client logger: %w", err)
}
return nil
}
func createLogger(loggerConfig LoggerConfig) (*zap.Logger, error) {
configLevel, err := zapcore.ParseLevel(loggerConfig.Logging.Level)
if err != nil {
return nil, err
}
// create log file and set the permission
file, err := os.OpenFile(loggerConfig.Logging.OutputPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, err
}
file.Close()
// configure lumberjack for log rotation
logWriter := &lumberjack.Logger{
Filename: loggerConfig.Logging.OutputPath,
MaxSize: loggerConfig.Logging.File.MaxSize,
MaxBackups: loggerConfig.Logging.File.MaxBackups,
MaxAge: loggerConfig.Logging.File.MaxAge,
Compress: loggerConfig.Logging.File.Compress,
}
// Create cores for different log levels
core := zapcore.NewCore(
zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
MessageKey: loggerConfig.Logging.EncoderConfig.MessageKey,
LevelKey: loggerConfig.Logging.EncoderConfig.LevelKey,
TimeKey: loggerConfig.Logging.EncoderConfig.TimeKey,
NameKey: loggerConfig.Logging.EncoderConfig.NameKey,
CallerKey: loggerConfig.Logging.EncoderConfig.CallerKey,
StacktraceKey: "", // disable stacktrace
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
ConsoleSeparator: " ",
}),
zapcore.AddSync(logWriter),
configLevel,
)
logger := zap.New(core,
zap.AddCaller(), // caller info
zap.AddStacktrace(zap.ErrorLevel), // show error stacktrace
zap.AddCallerSkip(1), // skip one layer of caller
)
return logger, nil
}
func Info(msg string, fields ...zap.Field) {
Log.WithOptions(zap.WithCaller(false)).Info(msg, fields...)
}
func Error(msg string, fields ...zap.Field) {
Log.WithOptions(zap.WithCaller(false)).Error(msg, fields...)
}
func Warn(msg string, fields ...zap.Field) {
Log.WithOptions(zap.WithCaller(false)).Warn(msg, fields...)
}
func Debug(msg string, fields ...zap.Field) {
Log.WithOptions(zap.WithCaller(true)).Debug(msg, fields...)
}
func Fatal(msg string, fields ...zap.Field) {
Log.WithOptions(zap.WithCaller(false)).Fatal(msg, fields...)
}

View file

@ -0,0 +1,26 @@
logging:
# Log level: debug, info, warn, error, fatal
level: "info"
# Log encoding: json or console
encoding: console
# standard output
outputPath: "xcecli_out.log"
# File rotation settings
file:
maxSize: 100 # megabytes before rotation
maxBackups: 10 # number of old files to keep
maxAge: 14 # days to keep old files
compress: true # compress rotated files
# Encoder configuration
encoderConfig:
messageKey: "message"
levelKey: "level"
timeKey: "timestamp"
nameKey: "logger"
callerKey: "caller"
functionKey: "func"
stacktraceKey: "stacktrace"
timeEncoder: "ISO8601"
levelEncoder: "capital"

View file

@ -0,0 +1,92 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/xeipuuv/gojsonschema"
"gopkg.in/yaml.v2"
)
// validate the specified yaml file format whether is correct
// return the content of the yaml file if the format is correct
// return error if the format is incorrect
func ValidateYamlFormat(yamlFilePath string) ([]byte, error) {
// find the file path
yamlFilePath, err := filepath.Abs(yamlFilePath)
if err != nil {
return nil, err
}
// get scheme file path, suppose schema file is in the same directory as yaml file
yamlFileRootPath := filepath.Dir(yamlFilePath)
realFileName := filepath.Base(yamlFilePath)
preRealFileName := realFileName[:len(realFileName)-5] + "_schema.json"
schemaFilePath := filepath.Join(yamlFileRootPath, "schema", preRealFileName)
// checking if the schema file exists
if _, err := os.Stat(schemaFilePath); os.IsNotExist(err) {
err = fmt.Errorf("schema file %s does not exist ", schemaFilePath)
return nil, err
}
data, err := ValidateYAML(yamlFilePath, schemaFilePath)
if err != nil {
return nil, err
}
return data, nil
}
// ValidateYAML validates the YAML content against the provided JSON schema
func ValidateYAML(yamlPath, schemaPath string) ([]byte, error) {
// schemaLoader is a absolute file path
schemaLoader := gojsonschema.NewReferenceLoader("file:///" + filepath.ToSlash(schemaPath))
// get yaml file content by reading the file
data, err := os.ReadFile(yamlPath)
if err != nil {
return nil, err
}
// get the yaml file into object
var raw interface{}
err = yaml.Unmarshal(data, &raw)
if err != nil {
return nil, err
}
// transform into map[string]interface{}
yamlContent := convertYAML(raw)
// create documentLoader
documentLoader := gojsonschema.NewGoLoader(yamlContent)
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
return nil, err
}
if !result.Valid() {
var errorMessages string
for _, desc := range result.Errors() {
errorMessages += fmt.Sprintf("- %s\n", desc)
}
err = errors.New(errorMessages)
return nil, err
}
// return the yaml content
return data, nil
}
// convertYAML recursively converts map[interface{}]interface{} to map[string]interface{}
func convertYAML(i interface{}) interface{} {
switch x := i.(type) {
case map[interface{}]interface{}:
m2 := make(map[string]interface{})
for k, v := range x {
m2[fmt.Sprintf("%v", k)] = convertYAML(v)
}
return m2
case []interface{}:
for idx, v := range x {
x[idx] = convertYAML(v)
}
}
return i
}

View file

@ -0,0 +1,88 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Logger Configuration Schema",
"type": "object",
"properties": {
"logging": {
"type": "object",
"properties": {
"level": {
"type": "string",
"enum": ["debug", "info", "warn", "error", "fatal"]
},
"encoding": {
"type": "string",
"enum": ["json", "console"]
},
"outputPath": {
"type": "string"
},
"file": {
"type": "object",
"properties": {
"maxSize": {
"type": "number",
"description": "megabytes before rotation"
},
"maxBackups": {
"type": "number",
"description": "number of old files to keep"
},
"maxAge": {
"type": "number",
"description": "days to keep old files"
},
"compress": {
"type": "boolean",
"description": "compress rotated files"
}
},
"required": ["maxSize", "maxBackups", "maxAge", "compress"]
},
"encoderConfig": {
"type": "object",
"properties": {
"messageKey": {
"type": "string"
},
"levelKey": {
"type": "string"
},
"timeKey": {
"type": "string"
},
"nameKey": {
"type": "string"
},
"callerKey": {
"type": "string"
},
"functionKey": {
"type": "string"
},
"stacktraceKey": {
"type": "string"
},
"timeEncoder": {
"type": "string"
},
"levelEncoder": {
"type": "string"
}
},
"required": [
"messageKey",
"levelKey",
"timeKey",
"nameKey",
"callerKey",
"functionKey",
"stacktraceKey"
]
}
},
"required": ["level", "encoding", "outputPath", "file", "encoderConfig"]
}
},
"required": ["logging"]
}