myxce/src/frontend/apiserver/utils/utils.go

646 lines
17 KiB
Go

package utils
import (
"apiserver/models"
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
)
var (
Global_TopDir string
PER_PAGE_LIMIT = 50
PER_PAGE_LIMIT_MAX = 1000
FileChunkSize = 1024 * 1024 // 1 MB
)
type FilterCondition struct {
Field string
Operator string
Value string
}
// to store filter condition for array job. 101[1-3, 6-8]
// mainFiltCond: job_id=101
// SubFiltConds: [ array_idx:btw:(1,3), array_idx:btw:(6,8) ]
type FilterConditionArray struct {
MainFiltCond FilterCondition
SubFiltConds []FilterCondition
}
type CompFilterCondition map[string][]FilterConditionArray
// check the starting top directory whether correctly
func PromptForTopDir(topDir *string) string {
if *topDir == "" {
fmt.Fprintln(os.Stderr, "Error: -d parameter is required")
flag.Usage()
os.Exit(1)
}
// convert to absolute path if the directory is relative
absPath, err := filepath.Abs(*topDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to convert to absolute path: %v\n", err)
os.Exit(1)
}
*topDir = absPath
// check if the directory exists
if _, err := os.Stat(*topDir); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Error: directory %s does not exist, fails to run the command\n", *topDir)
os.Exit(1)
}
// check if the config directory exists
configDir := filepath.Join(*topDir, "config")
if _, err := os.Stat(configDir); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Error: config directory %s does not exist, fails to run the command\n", configDir)
os.Exit(1)
}
return *topDir
}
// bindJSONStrict strict check the json format, if the json format is not correct, return error.
func BindJSONStrict(ctx *gin.Context, obj interface{}) error {
// First read the raw body to check for duplicate fields
body, err := io.ReadAll(ctx.Request.Body)
if err != nil {
return fmt.Errorf("error reading request body: %v", err)
}
// Reset the request body so it can be read again
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))
// Check for duplicate fields using json.Decoder
dec := json.NewDecoder(bytes.NewBuffer(body))
seenFields := make(map[string]bool)
// Read the opening brace
t, err := dec.Token()
if err != nil {
return fmt.Errorf("error reading JSON: %v", err)
}
if delim, ok := t.(json.Delim); !ok || delim != '{' {
return fmt.Errorf("expected JSON object")
}
// Read key-value pairs
for dec.More() {
// Read key
t, err := dec.Token()
if err != nil {
return fmt.Errorf("error reading JSON key: %v", err)
}
key, ok := t.(string)
if !ok {
return fmt.Errorf("expected string key")
}
// Check for duplicate key
if seenFields[key] {
return fmt.Errorf("duplicate field found in JSON: %s", key)
}
seenFields[key] = true
// Skip the value
var value interface{}
if err := dec.Decode(&value); err != nil {
return fmt.Errorf("error reading JSON value: %v", err)
}
}
// Read the closing brace
t, err = dec.Token()
if err != nil {
return fmt.Errorf("error reading JSON: %v", err)
}
if delim, ok := t.(json.Delim); !ok || delim != '}' {
return fmt.Errorf("expected closing brace")
}
// Now decode into the target object
dec = json.NewDecoder(bytes.NewBuffer(body))
dec.DisallowUnknownFields()
if err := dec.Decode(obj); err != nil {
var syntaxError *json.SyntaxError
var unmarshalTypeError *json.UnmarshalTypeError
var invalidUnmarshalError *json.InvalidUnmarshalError
switch {
case errors.As(err, &syntaxError):
return fmt.Errorf("request body contains badly-formed JSON (at position %d)", syntaxError.Offset)
case errors.Is(err, io.ErrUnexpectedEOF):
return errors.New("request body contains badly-formed JSON")
case errors.As(err, &unmarshalTypeError):
if unmarshalTypeError.Field != "" {
return fmt.Errorf("request body contains incorrect JSON type for field %q", unmarshalTypeError.Field)
}
return fmt.Errorf("request body contains incorrect JSON type (at position %d)", unmarshalTypeError.Offset)
case errors.Is(err, io.EOF):
return errors.New("request body must not be empty")
case strings.HasPrefix(err.Error(), "json: unknown field "):
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
return fmt.Errorf("request body contains unknown field %s", fieldName)
case errors.As(err, &invalidUnmarshalError):
return fmt.Errorf("error unmarshalling json: %v", err)
default:
return errors.New(err.Error())
}
}
val := reflect.ValueOf(obj)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
for i := 0; i < val.NumField(); i++ {
field := val.Type().Field(i)
if field.Tag.Get("binding") == "required" {
if val.Field(i).Kind() == reflect.String {
trimmedValue := strings.TrimSpace(val.Field(i).String())
val.Field(i).SetString(trimmedValue)
}
if isZero(val.Field(i)) {
// get json tag name
jsonTag := field.Tag.Get("json")
if jsonTag == "" {
jsonTag = field.Name // if no tag name, using field name
}
return fmt.Errorf("json key of %s is required", jsonTag)
}
}
}
return nil
}
func isZero(v reflect.Value) bool {
return v.IsZero()
}
// ParseFilters parses filter string into FilterConditions
func ParseFilters(filterStr string) []FilterCondition {
Debug("Enter ParseFilters function", zap.String("filterStr", filterStr))
// Remove brackets
//filterStr = strings.TrimPrefix(filterStr, "[")
//filterStr = strings.TrimSuffix(filterStr, "]") // since job_name will add index
// remove double quotes
filterStr = strings.ReplaceAll(filterStr, "\"", "")
filterStr = strings.ReplaceAll(filterStr, "}, {", "},{")
if filterStr == "" {
Debug("filterStr is empty, return nil")
return nil
}
conditions := strings.Split(filterStr, "},{")
filters := make([]FilterCondition, 0)
for _, condition := range conditions {
condition = strings.TrimPrefix(condition, "{")
condition = strings.TrimSuffix(condition, "}")
// need handle submit_time:eq:2025-03-31 14:34:07}
parts := strings.SplitN(condition, ":", 3)
if len(parts) != 3 {
continue
}
// check if there are same filter key and operator
exists := false
for _, filter := range filters {
if filter.Field == parts[0] && filter.Operator == parts[1] {
exists = true
break
}
}
if exists {
Error("There are duplicated filter key and operator, please check the input.")
return nil // return error for filter
}
filters = append(filters, FilterCondition{
Field: parts[0],
Operator: parts[1],
Value: strings.TrimSpace(parts[2]),
})
Debug("In function of ParseFilters, Field, Operator, Value are:", zap.Any("Field", parts[0]),
zap.Any("Op", parts[1]), zap.Any("Value", strings.TrimSpace(parts[2])))
}
if len(filters) > 0 {
Debug("Finish calling ParseFilters function, filter results", zap.Any("filters", filters))
return filters
} else {
Debug("The format of filter is not correct!")
return nil
}
}
// parseFields will parse the fields string
// eg: field=col1,col2,col3
func ParseFields(fieldsStr string) []string {
// Parse fields
var fields []string
if fieldsStr == "" {
return nil
}
fields = strings.Split(fieldsStr, ",")
// Trim spaces
result := make([]string, 0, len(fields))
for _, field := range fields {
field = strings.TrimSpace(field)
if field != "" {
result = append(result, field)
}
}
return result
}
func ParsePageToken(pagingStr string) (string, error) {
if pagingStr != "" {
pagingStr = strings.TrimPrefix(pagingStr, "\"")
pagingStr = strings.TrimSuffix(pagingStr, "\"")
pagingStr = strings.TrimSpace(pagingStr)
return pagingStr, nil
}
return "", nil
}
// Parse sort string in query parameters
// sort=job_id:asc,submit_time:desc
func ParseSortParam(s string, supportSortList []string) ([]SortOrder, error) {
if s == "" {
return nil, fmt.Errorf("the sort params is empty")
}
sortStr := strings.TrimSpace(s)
sorts := strings.SplitN(sortStr, ",", -1)
sortList := make([]SortOrder, 0)
// Convert supportSortList to map for quick lookup
sortMap := make(map[string]bool)
for _, item := range supportSortList {
sortMap[item] = true
}
if len(sorts) > 0 {
for _, sort := range sorts {
var col, order string
if strings.Contains(sort, ":") {
col = strings.SplitN(sort, ":", -1)[0]
order = strings.SplitN(sort, ":", -1)[1]
if order != "asc" && order != "desc" {
return nil, fmt.Errorf("the order string %s not in \"asc\" or \"desc\" ", order)
}
} else {
col = sort
order = "asc" // by default
}
if _, ok := sortMap[col]; !ok {
return nil, fmt.Errorf("unsupported sort column: %s", col)
}
sortList = append(sortList, SortOrder{
Column: col,
Ascending: order == "asc",
})
}
} else {
return nil, fmt.Errorf("the sort params is empty")
}
return sortList, nil
}
func CheckAllMapKeyInList(sorts []SortOrder, fields []string) error {
// check the sort column is among the query fields
for _, col := range sorts {
found := ContainsField(fields, col.Column)
if !found {
err := fmt.Errorf("key \"%s\" is not in fields:%v ", col.Column, fields)
return err
}
}
return nil
}
// judge if a field exist in supported fields list
func ContainsField(fields []string, field string) bool {
for _, v := range fields {
if v == field {
return true
}
}
return false
}
// judge if a tagName exist in structType json tag ;
func JsonTagExists(structType interface{}, tagName string) bool {
val := reflect.ValueOf(structType)
for i := 0; i < val.NumField(); i++ {
field := val.Type().Field(i)
if field.Tag.Get("json") == tagName {
return true
}
}
return false
}
// in a map, get value's corresponding key
func GetKeyFromValue(m map[string]int, value int) (string, error) {
for k, v := range m {
if v == value {
return k, nil
}
}
return "", errors.New("value is not recognized.")
}
// GetMappedField returns the mapped field if it exists, otherwise returns the original field.
func GetMappedField(jsonTag string) string {
if mappedField, exists := models.JobFieldMappings[jsonTag]; exists {
return mappedField
}
return jsonTag
}
// GetSupportedFields returns the supported fields for a given model, applying any field mappings.
func GetSupportedFields(model interface{}) []string {
var fields []string
t := reflect.TypeOf(model)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag != "" {
// Only take the first part before any comma
jsonTag = strings.Split(jsonTag, ",")[0]
// Apply field mapping
fields = append(fields, jsonTag)
}
}
return fields
}
// IntOrInt64 converts an interface{} value that is either int or int64 to int.
// Returns (0, false) for any other type.
func IntOrInt64(v interface{}) (int, bool) {
switch t := v.(type) {
case int:
return t, true
case int64:
return int(t), true
default:
return 0, false
}
}
// ValidateYamlFormat reads and validates a YAML file, returning its raw bytes.
func ValidateYamlFormat(filePath string) ([]byte, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read YAML file %s: %v", filePath, err)
}
var out interface{}
if err := yaml.Unmarshal(data, &out); err != nil {
return nil, fmt.Errorf("invalid YAML format in %s: %v", filePath, err)
}
return data, nil
}
func ParseLimit(limitStr string) (int, error) {
if limitStr == "" {
return PER_PAGE_LIMIT, nil
}
limit, err := strconv.Atoi(limitStr)
if err != nil {
return 0, err
}
if limit > PER_PAGE_LIMIT_MAX {
return 0, fmt.Errorf("limit=%d exceed MAX=%d", limit, PER_PAGE_LIMIT_MAX)
}
return limit, nil
}
func ParseType(typeStr string, availableTypes []string) (string, error) {
if typeStr == "" {
return "", nil
}
typeStr = strings.TrimSpace(typeStr)
for _, t := range availableTypes {
if typeStr == t {
return typeStr, nil
}
}
return "", fmt.Errorf("type=%s is not supported, available types: %v", typeStr, availableTypes)
}
func ParseUnique(uniqueStr string) bool {
if uniqueStr == "" {
return false
}
// Convert to lowercase and check if it's exactly "true"
return strings.ToLower(strings.TrimSpace(uniqueStr)) == "true"
}
func ConvertStringToJSON(str []string) (string, error) {
// eg:"VAR1=value1","VAR2=value2"
jsonMap := make(map[string]string)
for _, e := range str {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
jsonMap[parts[0]] = parts[1]
}
}
strIntoJson, err := json.Marshal(jsonMap)
if err != nil {
return "", err
}
return string(strIntoJson), nil
}
func ConvertStringToJsonb(str []string) (models.Jsonb, error) {
// eg:"VAR1=value1","VAR2=value2"
jsonMap := make(map[string]string)
for _, e := range str {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
jsonMap[parts[0]] = parts[1]
}
}
strIntoJson, err := json.Marshal(jsonMap)
if err != nil {
return models.Jsonb{}, err
}
jsonb := models.Jsonb{
String: string(strIntoJson),
}
return jsonb, nil
}
func AddFieldsToSupportedList(target *[]string, fields interface{}) {
t := reflect.TypeOf(fields)
for i := 0; i < t.NumField(); i++ {
jsonTag := t.Field(i).Tag.Get("json")
if jsonTag != "" && jsonTag != "-" {
*target = append(*target, jsonTag)
}
}
}
func GetServerHostPort(ctx *gin.Context) string {
scheme := "http"
if ctx.Request.TLS != nil {
scheme = "https"
}
return fmt.Sprintf("%s://%s", scheme, ctx.Request.Host)
}
// convert timestamp(s) to local time string with TZ.
func convertTimestampToLocalTime(timestamp int64) string {
utcTime := time.Unix(timestamp, 0).UTC()
localTime := utcTime.Local()
return localTime.Format("2006-01-02 15:04:05 MST")
}
// convert time string with TZ to timestamp
func convertTimestrToTimestamp(timeStr string) (int64, error) {
// Define the layout for parsing the time string.
// "2006-01-02 15:04:05 MST" is a reference time in Go.
layout := "2006-01-02 15:04:05 MST"
// Parse the time string into a time.Time object.
parsedTime, err := time.Parse(layout, timeStr)
if err != nil {
return 0, fmt.Errorf("failed to parse time: %w", err)
}
// Get the timestamp in seconds.
timestamp := parsedTime.Unix()
return timestamp, nil
}
func IsNumeric(s string) bool {
_, err := strconv.ParseFloat(s, 64)
return err == nil
}
func ValidateControlAction(controlAction string, supportedActions []string) error {
controlAction = strings.ToLower(strings.TrimSpace(controlAction))
for _, action := range supportedActions {
if controlAction == action {
return nil
}
}
return errors.New("unsupported control action: " + controlAction)
}
func IsControlActionAllowed(actionType int, status int64, actionControlMap map[int][]int) bool {
Debug("Enter IsActionAllowed function",
zap.Any("actionType", actionType),
zap.Any("status", status))
allowedStatuses, exists := actionControlMap[actionType]
if !exists {
Error("Unsupported action type", zap.Any("actionType", actionType))
return false
}
for _, allowedStatus := range allowedStatuses {
if allowedStatus == int(status) {
Debug("Action is allowed",
zap.Any("status", status),
zap.Any("allowedStatus", allowedStatus))
return true
}
}
Debug("Action is not allowed", zap.Any("status", status))
return false
}
// RemoveDuplicates removes duplicate elements from any slice.
func RemoveDuplicates[T comparable](input []T) []T {
seen := make(map[T]bool)
result := []T{}
for _, value := range input {
if !seen[value] {
seen[value] = true
result = append(result, value)
}
}
return result
}
func TruncateString(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
runes := []rune(s)
if len(runes) > maxLen {
return string(runes[:maxLen])
}
return s
}
func IncludeBuiltinRoles(roleNames []string) (builtinIncluded []string) {
for _, roleName := range roleNames {
if ContainsField(models.BUILTIN_ROLE_NAMES, roleName) {
builtinIncluded = append(builtinIncluded, roleName)
}
}
return builtinIncluded
}
func IsValidUserName(userName string) error {
// Check if name is empty
if userName == "" {
return errors.New("username cannot be empty")
}
// Check maximum length (32 characters)
if len(userName) > 32 {
return errors.New("username cannot exceed 32 characters")
}
// Check if it begins with a letter
if !((userName[0] >= 'a' && userName[0] <= 'z') || (userName[0] >= 'A' && userName[0] <= 'Z')) {
return errors.New("username must begin with a letter")
}
// Check for valid characters (0-9, a-z, A-Z, -, _, space)
for i := 0; i < len(userName); i++ {
c := userName[i]
if !((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-' ||
c == '_' ||
c == ' ') {
return errors.New("username can only contain letters, numbers, hyphens, underscores, or spaces")
}
}
return nil
}