mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 10:08:02 +08:00
203 lines
5.0 KiB
Go
203 lines
5.0 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package apps
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/larksuite/cli/internal/validate"
|
|
)
|
|
|
|
const (
|
|
defaultAppsPageSize = 50
|
|
maxAppsPageSize = 100
|
|
appsEnvironmentFlag = "environment"
|
|
|
|
// The CLI exposes the user-facing online environment, while the
|
|
// observability backend stores online app runtime telemetry under runtime.
|
|
appsObservabilityBackendEnv = "runtime"
|
|
)
|
|
|
|
func appScopedPath(appID, suffix string) string {
|
|
base := apiBasePath + "/apps/" + validate.EncodePathSegment(strings.TrimSpace(appID))
|
|
suffix = strings.TrimLeft(strings.TrimSpace(suffix), "/")
|
|
if suffix == "" {
|
|
return base
|
|
}
|
|
return base + "/" + suffix
|
|
}
|
|
|
|
func validateObservabilityEnv(env string) error {
|
|
switch strings.TrimSpace(env) {
|
|
case "", "online":
|
|
return nil
|
|
default:
|
|
return appsValidationParamError("--environment", "observability commands only support online (got %q)", env).
|
|
WithHint("only online is supported; omit --environment to use the default online environment")
|
|
}
|
|
}
|
|
|
|
func validateEnvVarEnv(env string) error {
|
|
switch strings.TrimSpace(env) {
|
|
case "dev", "online":
|
|
return nil
|
|
default:
|
|
return appsValidationParamError("--environment", "env var commands only support --environment dev or --environment online (got %q)", env)
|
|
}
|
|
}
|
|
|
|
func validateAppsPageSize(n int) error {
|
|
if n < 1 || n > maxAppsPageSize {
|
|
return appsValidationParamError("--page-size", "--page-size must be between 1 and %d", maxAppsPageSize)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cleanRepeatedStrings(values []string) []string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
seen := make(map[string]struct{}, len(values))
|
|
out := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[value]; ok {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
out = append(out, value)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func normalizeObservabilityAttributes(item map[string]interface{}) {
|
|
kv := observabilityKVList(item["attributes"])
|
|
if len(kv) > 0 {
|
|
item["attributes"] = kv
|
|
}
|
|
}
|
|
|
|
func parseAppsTimeRange(sinceName, sinceRaw, untilName, untilRaw string) (time.Time, time.Time, bool, bool, error) {
|
|
var since, until time.Time
|
|
var hasSince, hasUntil bool
|
|
now := time.Now()
|
|
if strings.TrimSpace(sinceRaw) != "" {
|
|
parsed, err := parseAppsTimeFlag(sinceName, sinceRaw, now)
|
|
if err != nil {
|
|
return time.Time{}, time.Time{}, false, false, err
|
|
}
|
|
since = parsed
|
|
hasSince = true
|
|
}
|
|
if strings.TrimSpace(untilRaw) != "" {
|
|
parsed, err := parseAppsTimeFlag(untilName, untilRaw, now)
|
|
if err != nil {
|
|
return since, time.Time{}, hasSince, false, err
|
|
}
|
|
until = parsed
|
|
hasUntil = true
|
|
}
|
|
if hasSince && hasUntil && since.After(until) {
|
|
return since, until, true, true, appsValidationParamError(untilName, "%s must be greater than or equal to %s", untilName, sinceName)
|
|
}
|
|
return since, until, hasSince, hasUntil, nil
|
|
}
|
|
|
|
func parseAppsTimeFlag(param, raw string, now time.Time) (time.Time, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return time.Time{}, appsValidationParamError(param, "%s is required", param)
|
|
}
|
|
if d, ok := parseAppsRelativeDuration(raw); ok {
|
|
return now.Add(-d), nil
|
|
}
|
|
if t, err := time.Parse(time.RFC3339Nano, raw); err == nil {
|
|
return t, nil
|
|
}
|
|
for _, layout := range []string{
|
|
"2006-01-02",
|
|
"2006-01-02T15:04:05",
|
|
"2006-01-02T15:04:05.000",
|
|
} {
|
|
if t, err := time.ParseInLocation(layout, raw, time.Local); err == nil {
|
|
return t, nil
|
|
}
|
|
}
|
|
return time.Time{}, appsValidationParamError(param, "invalid %s %q: expected relative duration (30s, 5m, 0.5h, 2h, 3d, 1w), YYYY-MM-DD, local YYYY-MM-DDTHH:mm:ss(.SSS), or RFC3339", param, raw)
|
|
}
|
|
|
|
func parseAppsRelativeDuration(s string) (time.Duration, bool) {
|
|
s = strings.TrimSpace(s)
|
|
if len(s) < 2 {
|
|
return 0, false
|
|
}
|
|
unit := s[len(s)-1]
|
|
number := s[:len(s)-1]
|
|
if number == "" {
|
|
return 0, false
|
|
}
|
|
seenDot := false
|
|
seenFractionDigit := false
|
|
for i := 0; i < len(number); i++ {
|
|
ch := number[i]
|
|
if ch == '.' {
|
|
if seenDot || i == 0 {
|
|
return 0, false
|
|
}
|
|
seenDot = true
|
|
continue
|
|
}
|
|
if ch < '0' || ch > '9' {
|
|
return 0, false
|
|
}
|
|
if seenDot {
|
|
seenFractionDigit = true
|
|
}
|
|
}
|
|
if seenDot && !seenFractionDigit {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.ParseFloat(number, 64)
|
|
if err != nil || n <= 0 {
|
|
return 0, false
|
|
}
|
|
var unitDuration time.Duration
|
|
switch unit {
|
|
case 's':
|
|
unitDuration = time.Second
|
|
case 'm':
|
|
unitDuration = time.Minute
|
|
case 'h':
|
|
unitDuration = time.Hour
|
|
case 'd':
|
|
unitDuration = 24 * time.Hour
|
|
case 'w':
|
|
unitDuration = 7 * 24 * time.Hour
|
|
default:
|
|
return 0, false
|
|
}
|
|
const maxDuration = time.Duration(1<<63 - 1)
|
|
if n > float64(maxDuration)/float64(unitDuration) {
|
|
return 0, false
|
|
}
|
|
duration := time.Duration(n * float64(unitDuration))
|
|
if duration <= 0 {
|
|
return 0, false
|
|
}
|
|
return duration, true
|
|
}
|
|
|
|
func nsNumber(t time.Time) string {
|
|
return strconv.FormatInt(t.UnixNano(), 10)
|
|
}
|
|
|
|
func secNumber(t time.Time) string {
|
|
return strconv.FormatInt(t.Unix(), 10)
|
|
}
|