Files
aiops/backend/scripts/mock.go
T

555 lines
22 KiB
Go

package main
import (
"fmt"
"math/rand"
"time"
"ezops/internal/config"
"ezops/internal/models"
"ezops/pkg/database"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
func main() {
cfg := config.Load()
db, err := database.New(cfg.Database)
if err != nil {
fmt.Printf("Failed to connect to database: %v\n", err)
return
}
if err := generateMockData(db); err != nil {
fmt.Printf("Failed to generate mock data: %v\n", err)
return
}
fmt.Println("Mock data generated successfully!")
}
func generateMockData(db *gorm.DB) error {
if err := generateUsers(db); err != nil {
return err
}
if err := generateServers(db); err != nil {
return err
}
if err := generateCloudAccounts(db); err != nil {
return err
}
if err := generateDomains(db); err != nil {
return err
}
if err := generateK8sClusters(db); err != nil {
return err
}
if err := generateAlertRules(db); err != nil {
return err
}
if err := generatePipelines(db); err != nil {
return err
}
if err := generateAppReleases(db); err != nil {
return err
}
if err := generateCronJobs(db); err != nil {
return err
}
if err := generateScripts(db); err != nil {
return err
}
if err := generateApprovalTemplates(db); err != nil {
return err
}
if err := generateNavigations(db); err != nil {
return err
}
return nil
}
func generateUsers(db *gorm.DB) error {
users := []models.User{
{Username: "admin", Email: "admin@ezops.local", RealName: "系统管理员", Phone: "13800138000", Status: 1, IsAdmin: true},
{Username: "ops01", Email: "ops01@ezops.local", RealName: "运维工程师1", Phone: "13800138001", Status: 1, IsAdmin: false},
{Username: "ops02", Email: "ops02@ezops.local", RealName: "运维工程师2", Phone: "13800138002", Status: 1, IsAdmin: false},
{Username: "dev01", Email: "dev01@ezops.local", RealName: "开发工程师1", Phone: "13800138003", Status: 1, IsAdmin: false},
{Username: "dev02", Email: "dev02@ezops.local", RealName: "开发工程师2", Phone: "13800138004", Status: 0, IsAdmin: false},
}
password, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
for i := range users {
users[i].Password = string(password)
if err := db.Create(&users[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated users:", len(users))
return nil
}
func generateServers(db *gorm.DB) error {
osTypes := []string{"CentOS 7.9", "CentOS 8.5", "Ubuntu 20.04", "Ubuntu 22.04", "Debian 11", "Rocky Linux 8.6"}
statuses := []string{"running", "stopped", "maintenance"}
servers := make([]models.Server, 0, 50)
for i := 0; i < 50; i++ {
server := models.Server{
Hostname: fmt.Sprintf("server-%03d", i+1),
IP: fmt.Sprintf("10.0.%d.%d", rand.Intn(256), rand.Intn(256)),
PublicIP: fmt.Sprintf("47.%d.%d.%d", rand.Intn(256), rand.Intn(256), rand.Intn(256)),
OS: osTypes[rand.Intn(len(osTypes))],
CPU: []int{2, 4, 8, 16, 32}[rand.Intn(5)],
Memory: []int64{4096, 8192, 16384, 32768, 65536, 131072}[rand.Intn(6)],
Disk: []int64{100, 200, 500, 1000}[rand.Intn(4)],
Status: statuses[rand.Intn(len(statuses))],
CPUUsage: rand.Float64() * 80,
MemoryUsage: rand.Float64() * 80,
DiskUsage: rand.Float64() * 70,
Tags: fmt.Sprintf("{\"project\":\"project%d\",\"team\":\"team%d\"}", rand.Intn(10), rand.Intn(5)),
Description: fmt.Sprintf("服务器 %d 描述", i+1),
}
servers = append(servers, server)
}
if err := db.Create(&servers).Error; err != nil {
return err
}
fmt.Println("Generated servers:", len(servers))
return nil
}
func generateCloudAccounts(db *gorm.DB) error {
accounts := []models.CloudAccount{
{Name: "阿里云主账号", Provider: "aliyun", Status: "active", MonthlyCost: 28500},
{Name: "腾讯云主账号", Provider: "tencent", Status: "active", MonthlyCost: 15800},
{Name: "AWS 账号", Provider: "aws", Status: "active", MonthlyCost: 42000},
{Name: "华为云账号", Provider: "huawei", Status: "inactive", MonthlyCost: 8900},
}
for i := range accounts {
accounts[i].AccessKey = "LTAI" + randomString(16)
accounts[i].SecretKey = randomString(32)
if err := db.Create(&accounts[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated cloud accounts:", len(accounts))
return nil
}
func generateDomains(db *gorm.DB) error {
domains := []models.Domain{
{Name: "ezops.local", Registrar: "aliyun", Status: "active", DNSProvider: "alidns"},
{Name: "api.ezops.local", Registrar: "aliyun", Status: "active", DNSProvider: "alidns"},
{Name: "app.ezops.local", Registrar: "tencent", Status: "active", DNSProvider: "dnspod"},
{Name: "monitor.ezops.local", Registrar: "aliyun", Status: "active", DNSProvider: "alidns", SSLEnabled: true},
}
for i := range domains {
expireDays := rand.Intn(365) + 30
expireDate := time.Now().AddDate(0, 0, expireDays)
domains[i].ExpireDate = &expireDate
domains[i].SSLExpireDate = &expireDate
if err := db.Create(&domains[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated domains:", len(domains))
return nil
}
func generateK8sClusters(db *gorm.DB) error {
clusters := []models.K8sCluster{
{Name: "prod-cluster", Code: "prod", Version: "v1.28.0", Status: "healthy", NodeCount: 10, PodCount: 150, ServiceCount: 30, DeploymentCount: 15, APIEndpoint: "https://prod-api.ezops.local:6443", CPUUsage: 65.5, MemoryUsage: 72.3, Description: "生产环境 Kubernetes 集群"},
{Name: "staging-cluster", Code: "staging", Version: "v1.28.0", Status: "healthy", NodeCount: 5, PodCount: 80, ServiceCount: 15, DeploymentCount: 10, APIEndpoint: "https://staging-api.ezops.local:6443", CPUUsage: 45.2, MemoryUsage: 55.0, Description: "预发环境 Kubernetes 集群"},
{Name: "dev-cluster", Code: "dev", Version: "v1.27.0", Status: "warning", NodeCount: 3, PodCount: 45, ServiceCount: 8, DeploymentCount: 8, APIEndpoint: "https://dev-api.ezops.local:6443", CPUUsage: 80.1, MemoryUsage: 85.0, Description: "开发环境 Kubernetes 集群"},
{Name: "test-cluster", Code: "test", Version: "v1.27.0", Status: "error", NodeCount: 2, PodCount: 20, ServiceCount: 5, DeploymentCount: 5, APIEndpoint: "https://test-api.ezops.local:6443", CPUUsage: 30.0, MemoryUsage: 40.0, Description: "测试环境 Kubernetes 集群"},
}
for i := range clusters {
if err := db.Create(&clusters[i]).Error; err != nil {
return err
}
}
for _, cluster := range clusters {
// 生成节点
for j := 0; j < cluster.NodeCount; j++ {
node := models.K8sNode{
ClusterID: cluster.ID,
Name: fmt.Sprintf("%s-node-%d", cluster.Code, j+1),
IP: fmt.Sprintf("192.168.%d.%d", rand.Intn(256), rand.Intn(256)),
Status: []string{"Ready", "Ready", "Ready", "NotReady"}[rand.Intn(4)],
Role: []string{"master", "worker", "worker"}[rand.Intn(3)],
CPU: []int{4, 8, 16}[rand.Intn(3)],
Memory: []int64{16384, 32768, 65536}[rand.Intn(3)],
Disk: []int64{100, 200, 500}[rand.Intn(3)],
CPUUsage: rand.Float64() * 80,
MemoryUsage: rand.Float64() * 80,
DiskUsage: rand.Float64() * 70,
PodCount: rand.Intn(30) + 5,
MaxPods: 110,
OS: "Ubuntu 22.04.3 LTS",
KubeletVer: cluster.Version,
}
if err := db.Create(&node).Error; err != nil {
return err
}
}
// 生成 Deployments
depImages := []string{
"nginx:1.25.3", "redis:7.2-alpine", "mysql:8.0.35",
"kafka:3.6.0", "elasticsearch:8.11.0", "grafana/grafana:10.2.0",
"prom/prometheus:v2.48.0", "busybox:1.36", "easzlab/kubeasz:3.6.1",
}
depNames := []string{
"nginx-deploy", "redis-cluster", "mysql-master", "mysql-slave",
"kafka-broker", "es-cluster", "grafana", "prometheus",
"filebeat", "logstash", "jaeger", "zipkin",
"frontend", "backend-api", "gateway",
}
namespaces := []string{"default", "kube-system", "monitoring", "ingress-nginx", "production"}
numDeps := cluster.DeploymentCount
if numDeps < 5 {
numDeps = 5 + rand.Intn(10)
}
for d := 0; d < numDeps && d < len(depNames); d++ {
replicas := []int{1, 2, 3, 5}[rand.Intn(4)]
available := replicas
if rand.Intn(4) == 0 {
available = replicas - 1
}
if available < 0 {
available = 0
}
dep := models.K8sDeployment{
ClusterID: cluster.ID,
Namespace: namespaces[rand.Intn(len(namespaces))],
Name: depNames[d],
Replicas: replicas,
AvailableReplicas: available,
ReadyReplicas: available,
Image: depImages[rand.Intn(len(depImages))],
Strategy: "RollingUpdate",
}
if err := db.Create(&dep).Error; err != nil {
return err
}
}
// 生成 Pods
podStatuses := []string{"Running", "Running", "Running", "Running", "Pending", "CrashLoopBackOff"}
numPods := cluster.PodCount
if numPods > 30 {
numPods = 30
}
if numPods < 10 {
numPods = 10 + rand.Intn(15)
}
for p := 0; p < numPods; p++ {
status := podStatuses[rand.Intn(len(podStatuses))]
pod := models.K8sPod{
ClusterID: cluster.ID,
Namespace: namespaces[rand.Intn(len(namespaces))],
Name: fmt.Sprintf("%s-pod-%d", cluster.Code, p+1),
Status: status,
NodeName: fmt.Sprintf("%s-node-%d", cluster.Code, rand.Intn(cluster.NodeCount)+1),
IP: fmt.Sprintf("10.%d.%d.%d", rand.Intn(256), rand.Intn(256), rand.Intn(256)),
RestartCount: rand.Intn(10),
Image: depImages[rand.Intn(len(depImages))],
CPUUsage: rand.Float64() * 500,
MemoryUsage: rand.Float64() * 2048,
}
if err := db.Create(&pod).Error; err != nil {
return err
}
}
// 生成 Services
svcTypes := []string{"ClusterIP", "NodePort", "LoadBalancer"}
svcNames := []string{
"nginx-svc", "redis-svc", "mysql-svc", "kafka-svc",
"es-svc", "grafana-svc", "prometheus-svc",
"frontend-svc", "backend-svc", "gateway-svc",
}
numSvcs := cluster.ServiceCount
if numSvcs > 10 {
numSvcs = 10
}
if numSvcs < 5 {
numSvcs = 5 + rand.Intn(5)
}
for s := 0; s < numSvcs && s < len(svcNames); s++ {
svcType := svcTypes[rand.Intn(len(svcTypes))]
ports := "[]"
switch rand.Intn(4) {
case 0:
ports = `[{"port":80,"protocol":"TCP","targetPort":80}]`
case 1:
ports = `[{"port":443,"protocol":"TCP","targetPort":8443}]`
case 2:
ports = `[{"port":8080,"protocol":"TCP","targetPort":8080},{"port":9090,"protocol":"TCP","targetPort":9090}]`
default:
ports = `[{"port":3306,"protocol":"TCP","targetPort":3306}]`
}
svc := models.K8sService{
ClusterID: cluster.ID,
Namespace: namespaces[rand.Intn(len(namespaces))],
Name: svcNames[s],
Type: svcType,
ClusterIP: fmt.Sprintf("10.96.%d.%d", rand.Intn(256), rand.Intn(256)),
ExternalIP: "",
Ports: ports,
Selector: fmt.Sprintf(`{"app":"%s"}`, svcNames[s]),
}
if svcType == "LoadBalancer" {
svc.ExternalIP = fmt.Sprintf("203.0.113.%d", rand.Intn(256))
}
if svcType == "NodePort" {
svc.ExternalIP = fmt.Sprintf("%d-%d", 30000+rand.Intn(1000), 32767)
}
if err := db.Create(&svc).Error; err != nil {
return err
}
}
// 生成 Ingresses
ingNames := []string{"main-ingress", "api-ingress", "grafana-ingress", "canary-ingress"}
numIngs := 2 + rand.Intn(3)
for ig := 0; ig < numIngs && ig < len(ingNames); ig++ {
hosts := fmt.Sprintf(`["%s.ezops.local"]`, ingNames[ig])
ing := models.K8sIngress{
ClusterID: cluster.ID,
Namespace: namespaces[rand.Intn(len(namespaces))],
Name: ingNames[ig],
Hosts: hosts,
Backend: fmt.Sprintf("%s-svc:80", ingNames[ig]),
TLS: rand.Intn(2) == 0,
}
if err := db.Create(&ing).Error; err != nil {
return err
}
}
}
fmt.Println("Generated K8s clusters:", len(clusters))
return nil
}
func generateAlertRules(db *gorm.DB) error {
rules := []models.AlertRule{
{Name: "CPU 使用率告警", Type: "metric", Source: "prometheus", Expression: "cpu_usage > 80", Severity: "warning", Duration: 300, Enabled: true, Description: "CPU 使用率超过 80%"},
{Name: "CPU 使用率严重告警", Type: "metric", Source: "prometheus", Expression: "cpu_usage > 95", Severity: "critical", Duration: 120, Enabled: true, Description: "CPU 使用率超过 95%"},
{Name: "内存使用率告警", Type: "metric", Source: "prometheus", Expression: "memory_usage > 80", Severity: "warning", Duration: 300, Enabled: true, Description: "内存使用率超过 80%"},
{Name: "磁盘使用率告警", Type: "metric", Source: "prometheus", Expression: "disk_usage > 80", Severity: "warning", Duration: 600, Enabled: true, Description: "磁盘使用率超过 80%"},
{Name: "服务不可用告警", Type: "event", Source: "custom", Expression: "service_up == 0", Severity: "critical", Duration: 60, Enabled: true, Description: "服务不可用"},
{Name: "Pod 重启告警", Type: "event", Source: "kubernetes", Expression: "pod_restart > 5", Severity: "warning", Duration: 600, Enabled: true, Description: "Pod 重启次数超过 5 次"},
}
for i := range rules {
if err := db.Create(&rules[i]).Error; err != nil {
return err
}
}
for i := 0; i < 20; i++ {
rule := rules[rand.Intn(len(rules))]
event := models.AlertEvent{
RuleID: rule.ID,
RuleName: rule.Name,
Severity: rule.Severity,
Status: []string{"firing", "resolved"}[rand.Intn(2)],
Target: fmt.Sprintf("server-%03d", rand.Intn(50)+1),
Message: fmt.Sprintf("告警事件 %d", i+1),
Value: rand.Float64() * 100,
StartedAt: time.Now().Add(-time.Duration(rand.Intn(72)) * time.Hour),
}
if err := db.Create(&event).Error; err != nil {
return err
}
}
fmt.Println("Generated alert rules:", len(rules))
return nil
}
func generatePipelines(db *gorm.DB) error {
pipelines := []models.Pipeline{
{Name: "前端构建", Code: "frontend-build", Type: "build", Source: "https://gitlab.com/ezops/frontend", Branch: "main", Enabled: true, Description: "前端项目构建流水线"},
{Name: "后端构建", Code: "backend-build", Type: "build", Source: "https://gitlab.com/ezops/backend", Branch: "main", Enabled: true, Description: "后端项目构建流水线"},
{Name: "部署预发", Code: "deploy-staging", Type: "deploy", Source: "https://gitlab.com/ezops/deploy", Branch: "staging", Enabled: true, Description: "部署到预发环境"},
{Name: "部署生产", Code: "deploy-production", Type: "deploy", Source: "https://gitlab.com/ezops/deploy", Branch: "main", Enabled: true, Description: "部署到生产环境"},
{Name: "集成测试", Code: "integration-test", Type: "test", Source: "https://gitlab.com/ezops/tests", Branch: "main", Enabled: true, Description: "集成测试流水线"},
}
for i := range pipelines {
if err := db.Create(&pipelines[i]).Error; err != nil {
return err
}
}
for _, pipeline := range pipelines {
for j := 0; j < 5; j++ {
run := models.PipelineRun{
PipelineID: pipeline.ID,
BuildNumber: j + 1,
Status: []string{"success", "success", "success", "failed", "running"}[rand.Intn(5)],
Branch: pipeline.Branch,
CommitID: randomString(8),
CommitMsg: "Update " + randomString(6),
Author: []string{"admin", "ops01", "dev01"}[rand.Intn(3)],
TriggeredBy: []string{"admin", "ops01", "dev01"}[rand.Intn(3)],
}
if err := db.Create(&run).Error; err != nil {
return err
}
}
}
fmt.Println("Generated pipelines:", len(pipelines))
return nil
}
func generateAppReleases(db *gorm.DB) error {
apps := []string{"EZOps 前端", "EZOps 后端", "用户中心", "支付网关", "数据分析平台"}
environments := []string{"dev", "test", "staging", "prod"}
statuses := []string{"pending", "building", "deploying", "success", "success", "success", "failed", "rolled_back"}
authors := []string{"admin", "ops01", "dev01"}
releases := make([]models.AppRelease, 0, 10)
for i := 0; i < 10; i++ {
app := apps[i%len(apps)]
env := environments[rand.Intn(len(environments))]
status := statuses[rand.Intn(len(statuses))]
minor := rand.Intn(20)
patch := rand.Intn(10)
release := models.AppRelease{
AppName: app,
Version: fmt.Sprintf("v1.%d.%d", minor, patch),
Environment: env,
Status: status,
CommitID: randomString(8),
CommitMsg: fmt.Sprintf("feat: 更新 %s 功能", app),
Author: authors[rand.Intn(len(authors))],
Duration: rand.Intn(600) + 60,
Description: fmt.Sprintf("%s %s 环境发布", app, env),
}
// Set timestamps and approval fields based on status
now := time.Now()
hoursAgo := time.Duration(rand.Intn(168)+1) * time.Hour
if status == "success" || status == "failed" {
approver := authors[rand.Intn(len(authors))]
release.Approver = approver
approvedAt := now.Add(-hoursAgo + 30*time.Minute)
release.ApprovedAt = &approvedAt
deployedAt := now.Add(-hoursAgo + 2*time.Hour)
release.DeployedAt = &deployedAt
} else if status == "rolled_back" {
approver := authors[rand.Intn(len(authors))]
release.Approver = approver
approvedAt := now.Add(-hoursAgo + 30*time.Minute)
release.ApprovedAt = &approvedAt
deployedAt := now.Add(-hoursAgo + 2*time.Hour)
release.DeployedAt = &deployedAt
rollbackAt := now.Add(-hoursAgo + 4*time.Hour)
release.RollbackAt = &rollbackAt
}
releases = append(releases, release)
}
for i := range releases {
if err := db.Create(&releases[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated app releases:", len(releases))
return nil
}
func generateCronJobs(db *gorm.DB) error {
cronJobs := []models.CronJob{
{Name: "数据库备份", CronExpr: "0 2 * * *", Type: "script", Content: "/scripts/backup-db.sh", Enabled: true, Description: "每日凌晨 2 点备份数据库"},
{Name: "清理日志", CronExpr: "0 3 * * 0", Type: "command", Content: "find /var/log -mtime +30 -delete", Enabled: true, Description: "每周日凌晨 3 点清理日志"},
{Name: "数据同步", CronExpr: "*/30 * * * *", Type: "script", Content: "/scripts/sync-data.sh", Enabled: true, Description: "每 30 分钟同步数据"},
{Name: "健康检查", CronExpr: "*/5 * * * *", Type: "command", Content: "curl -sf http://localhost:8080/health", Enabled: true, Description: "每 5 分钟健康检查"},
{Name: "生成报表", CronExpr: "0 9 * * 1", Type: "script", Content: "/scripts/generate-report.sh", Enabled: false, Description: "每周一上午 9 点生成报表"},
}
for i := range cronJobs {
if err := db.Create(&cronJobs[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated cron jobs:", len(cronJobs))
return nil
}
func generateScripts(db *gorm.DB) error {
scripts := []models.Script{
{Name: "重启服务", Code: "restart-service", Type: "shell", Content: "#!/bin/bash\nsystemctl restart $1", Description: "重启服务脚本"},
{Name: "检查磁盘", Code: "check-disk", Type: "shell", Content: "#!/bin/bash\ndf -h", Description: "检查磁盘空间"},
{Name: "检查内存", Code: "check-memory", Type: "shell", Content: "#!/bin/bash\nfree -h", Description: "检查内存使用"},
{Name: "部署应用", Code: "deploy-app", Type: "python", Content: "#!/usr/bin/env python3\nimport sys\nprint(f'Deploying {sys.argv[1]}')", Description: "部署应用脚本"},
{Name: "备份配置", Code: "backup-config", Type: "shell", Content: "#!/bin/bash\ntar -czf /backup/config-$(date +%Y%m%d).tar.gz /etc/ezops", Description: "备份配置文件"},
}
for i := range scripts {
if err := db.Create(&scripts[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated scripts:", len(scripts))
return nil
}
func generateApprovalTemplates(db *gorm.DB) error {
templates := []models.ApprovalTemplate{
{Name: "服务器申请审批", Code: "server-request", Type: "server", Enabled: true, Description: "服务器资源申请审批流程"},
{Name: "发布审批", Code: "release-approval", Type: "release", Enabled: true, Description: "应用发布审批流程"},
{Name: "权限申请审批", Code: "permission-request", Type: "permission", Enabled: true, Description: "系统权限申请审批流程"},
}
for i := range templates {
if err := db.Create(&templates[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated approval templates:", len(templates))
return nil
}
func generateNavigations(db *gorm.DB) error {
groups := []models.NavigationGroup{
{Name: "常用工具", Code: "tools", Icon: "Tool", Sort: 1, Enabled: true},
{Name: "监控平台", Code: "monitor", Icon: "Activity", Sort: 2, Enabled: true},
{Name: "开发工具", Code: "devtools", Icon: "Code", Sort: 3, Enabled: true},
{Name: "文档中心", Code: "docs", Icon: "FileText", Sort: 4, Enabled: true},
}
for i := range groups {
if err := db.Create(&groups[i]).Error; err != nil {
return err
}
}
links := []models.NavigationLink{
{GroupID: groups[0].ID, Name: "GitLab", URL: "https://gitlab.com", Icon: "GitBranch", Description: "代码仓库", Sort: 1},
{GroupID: groups[0].ID, Name: "Jenkins", URL: "https://jenkins.io", Icon: "Settings", Description: "CI/CD 工具", Sort: 2},
{GroupID: groups[0].ID, Name: "Nexus", URL: "https://www.sonatype.com/nexus-repository-oss", Icon: "Package", Description: "制品库", Sort: 3},
{GroupID: groups[1].ID, Name: "Prometheus", URL: "https://prometheus.io", Icon: "Activity", Description: "监控系统", Sort: 1},
{GroupID: groups[1].ID, Name: "Grafana", URL: "https://grafana.com", Icon: "BarChart", Description: "可视化平台", Sort: 2},
{GroupID: groups[1].ID, Name: "ELK", URL: "https://www.elastic.co/what-is/elk-stack", Icon: "Search", Description: "日志分析", Sort: 3},
{GroupID: groups[2].ID, Name: "Swagger", URL: "https://swagger.io", Icon: "FileCode", Description: "API 文档", Sort: 1},
{GroupID: groups[2].ID, Name: "Postman", URL: "https://www.postman.com", Icon: "Send", Description: "API 测试", Sort: 2},
{GroupID: groups[3].ID, Name: "Confluence", URL: "https://www.atlassian.com/software/confluence", Icon: "BookOpen", Description: "知识库", Sort: 1},
}
for i := range links {
if err := db.Create(&links[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated navigation groups:", len(groups))
fmt.Println("Generated navigation links:", len(links))
return nil
}
func randomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}