+
+
+
+
+
+
+ Hi {{.UserName}},
+
+
+ {{.AlertMessage}}
+
+ {{if .WarnContent}}
+
+ {{.WarnContent}}
+
+ {{else}}{{if .Content}}
+
+ {{.Content}}
+
+ {{end}}
+ {{end}}
+
+ {{if .PlanDetails}}
+
+
+
+
+
+
+ {{range .PlanDetails.Features}}
+
+
+ |
+ ✓
+ |
+
+ {{.}}
+ |
+
+
+ {{end}}
+
+ |
+
+
+ {{end}}
+
+ {{if .Recommendation}}
+
+ Upgrade Plan
+
+
+ {{end}}
+
+
+
+ `
+
+// EventConfig defines the configuration for each event type
+type EventConfig struct {
+ TitleTemplate string
+ AlertTemplate string
+ WarnAlertTemplate string
+ Content string
+ BorderColor string
+ Features []string
+ Recommendation string
+ DatesFormat string
+}
+
+// eventConfigs maps event types to their configurations
+var eventConfigs = map[EventType]EventConfig{
+ EventTypeTrafficUsageAlert: {
+ TitleTemplate: "%s Region %s Workspace Resource %s",
+ AlertTemplate: "This is a heads-up that your %s resource usage in the %s region for the %s workspace has exceeded %s.",
+ Content: "To avoid any potential service disruption, please review your current usage and consider upgrading your plan.",
+ BorderColor: "#ffa500",
+ Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"},
+ Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s",
+ DatesFormat: "Until %s",
+ },
+ EventTypeWorkspaceSubscriptionCreatedSuccess: {
+ TitleTemplate: "%s Region %s Space Subscription Created Successfully",
+ AlertTemplate: `Welcome to Sealos!
+You have successfully subscribed to the %s plan for the %s space in the %s region.`,
+ Content: "Your plan is active and will automatically renew on %s.",
+ BorderColor: "#e5e5e5",
+ Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"},
+ DatesFormat: "Until %s",
+ },
+ EventTypeWorkspaceSubscriptionCreatedFailed: {
+ TitleTemplate: "%s Region %s workspace Subscription Creation Failed",
+ AlertTemplate: `We were unable to process your subscription to the %s plan for the %s workspace in the %s region.`,
+ Content: "Please contact support for assistance.",
+ BorderColor: "#ff0000",
+ Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"},
+ Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s",
+ DatesFormat: "Until %s",
+ },
+ EventTypeWorkspaceSubscriptionRenewedSuccess: {
+ TitleTemplate: "%s Region %s Workspace Subscription Renewed",
+ AlertTemplate: `Your subscription to the %s plan for the %s workspace in the %s region has been successfully renewed.`,
+ Content: "Thank you for your continued trust in Sealos.",
+ BorderColor: "#e5e5e5",
+ Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"},
+ DatesFormat: "Until %s",
+ Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s",
+ },
+ EventTypeWorkspaceSubscriptionRenewedFailed: {
+ TitleTemplate: "%s Region %s Workspace Subscription Renewal Failed",
+ AlertTemplate: `We were unable to renew your %s plan for the %s workspace in the %s region. Your service will be suspended on your plan's expiration date.`,
+ Content: "To prevent service interruption and data loss, please update your payment information as soon as possible. Your resources will be permanently deleted 7 days after the expiration date of %s.",
+ BorderColor: "#ff0000",
+ Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"},
+ Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s",
+ DatesFormat: "Until %s",
+ },
+ // EventTypeWorkspaceSubscriptionRenewedBalanceFallback 订阅支付失败,自动使用余额支付,需要通知用户订阅支付失败,但使用cloud balance成功
+ EventTypeWorkspaceSubscriptionRenewedBalanceFallback: {
+ TitleTemplate: "%s Region %s Workspace Subscription Renewed with Cloud Balance",
+ AlertTemplate: `Your subscription to the %s plan for the %s workspace in the %s region was successfully renewed using your cloud balance.Dear User, Your subscription auto-renewal has failed. However, we have successfully deducted the payment from your account balance, and your subscription has been renewed. Please check your payment method to ensure smooth auto-renewals in the future. If you have any questions, please contact our customer support.`,
+ Content: "Thank you for your continued trust in Sealos. Please ensure your payment information is up to date to avoid future interruptions.",
+ BorderColor: "#e5e5e5",
+ Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"},
+ DatesFormat: "Until %s",
+ Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s",
+ },
+ EventTypeWorkspaceSubscriptionExpired: {
+ TitleTemplate: "%s Region %s Workspace Subscription Expired",
+ AlertTemplate: "Your %s plan for the %s workspace in the %s region has expired, and your service is now suspended.",
+ Content: "Your resources will be permanently deleted in %d days. To restore your service and prevent data loss, please renew your subscription now.",
+ BorderColor: "#ff0000",
+ Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"},
+ Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s",
+ DatesFormat: "Until %s",
+ },
+ EventTypeWorkspaceSubscriptionExpiredDeleteResources: {
+ TitleTemplate: "%s Region %s Workspace Resources Deleted",
+ AlertTemplate: "As your subscription for the %s space in the %s region was not renewed within the 7-day grace period, your associated resources have now been permanently deleted.",
+ Content: "We're sorry to see you go. If you wish to use Sealos services again in the future, you can start a new subscription at any time.",
+ BorderColor: "#ff0000",
+ Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"},
+ Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s",
+ DatesFormat: "Expired on %s",
+ },
+ EventTypeWorkspaceSubscriptionUpgradedSuccess: {
+ TitleTemplate: "%s Region %s Workspace Subscription Upgraded",
+ AlertTemplate: "You have successfully upgraded to the %s plan! Your new plan is effective immediately for the %s workspace in the %s region.",
+ Content: "Enjoy the new features! Your plan will automatically renew on %s.",
+ BorderColor: "#e5e5e5",
+ Features: []string{"8 vCPU", "16GB RAM", "10GB Disk"},
+ DatesFormat: "Until %s",
+ Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s",
+ },
+ EventTypeWorkspaceSubscriptionUpgradedFailed: {
+ TitleTemplate: "%s Region %s Workspace Subscription Upgrade Failed",
+ AlertTemplate: "We were unable to process your upgrade to the %s plan for the %s workspace in the %s region. Your subscription will remain on your current plan.",
+ Content: "Please contact support for assistance.",
+ BorderColor: "#ff0000",
+ Features: []string{"8 vCPU", "16GB RAM", "10GB Disk"},
+ Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s",
+ DatesFormat: "Current Until %s",
+ },
+ EventTypeWorkspaceSubscriptionDowngradedSuccess: {
+ TitleTemplate: "%s Region %s Workspace Subscription Downgraded",
+ AlertTemplate: "You have successfully scheduled a downgrade to the %s plan for the %s workspace in the %s region.",
+ Content: "This change will take effect at the start of your next billing cycle on %s.",
+ BorderColor: "#e5e5e5",
+ Features: []string{"2 vCPU", "2GB RAM", "500MB Disk"},
+ DatesFormat: "Effective from %s",
+ Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s",
+ },
+ EventTypeWorkspaceSubscriptionDowngradedFailed: {
+ TitleTemplate: "%s Region %s Workspace Subscription Downgrade Failed",
+ AlertTemplate: "We were unable to process your downgrade to the %s plan for the %s workspace in the %s region. Your subscription will remain on your current plan.",
+ Content: "Please contact support for assistance.",
+ BorderColor: "#ff0000",
+ Features: []string{"2 vCPU", "2GB RAM", "500MB Disk"},
+ DatesFormat: "Current Until %s",
+ Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s",
+ },
+ EventTypeWorkspaceSubscriptionDebt: {
+ TitleTemplate: "Workspace Subscription Expiration Warning in %s Region %s Workspace",
+ AlertTemplate: "Your %s Plan for the %s workspace in the %s region has expired. Your service will be suspended on your plan's expiration date.",
+ BorderColor: "#ff0000",
+ WarnAlertTemplate: "To prevent service interruption and data loss, please update your payment information as soon as possible. Your resources will be permanently deleted 7 days after the expiration date.",
+ // Content: `To prevent service interruption and data loss, please update your plan as soon as possible. Your resources will be permanently deleted 7 days after the expiration date.`,
+ Content: "Your subscription will delete in %d days. Please renew your subscription to continue enjoying our services.",
+ Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s",
+ },
+}
+
+// generateEmailContent generates email content based on the event type
+func generateEmailContent(event *NotificationEvent) (*EmailData, error) {
+ config, ok := eventConfigs[event.EventType]
+ if !ok {
+ return nil, fmt.Errorf("unsupported event type: %s", event.EventType)
+ }
+
+ var data EmailData
+ data.BorderColor = config.BorderColor
+ data.Content = config.Content
+
+ switch event.EventType {
+ case EventTypeTrafficUsageAlert:
+ var trafficData WorkspaceSubscriptionTrafficEventData
+ dataBytes, err := json.Marshal(event.EventData)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal traffic event data: %w", err)
+ }
+ if err := json.Unmarshal(dataBytes, &trafficData); err != nil {
+ return nil, fmt.Errorf("failed to parse traffic event data: %w", err)
+ }
+ // Determine title suffix based on UsagePercent
+ titleSuffix := "Usage Alert"
+ if trafficData.UsagePercent >= 100 {
+ titleSuffix = "Exhausted"
+ data.Content = "Please upgrade your plan immediately to ensure continued operation."
+ data.BorderColor = "#ff0000"
+ }
+ data.Title = fmt.Sprintf(
+ config.TitleTemplate,
+ trafficData.RegionDomain,
+ trafficData.Workspace,
+ titleSuffix,
+ )
+ data.AlertMessage = fmt.Sprintf(
+ strings.ReplaceAll(
+ config.AlertTemplate,
+ `%s`,
+ `%s`,
+ ),
+ "Traffic",
+ trafficData.RegionDomain,
+ trafficData.Workspace,
+ strconv.Itoa(trafficData.UsagePercent)+"%",
+ )
+ data.Recommendation = fmt.Sprintf(
+ config.Recommendation,
+ trafficData.RegionDomain,
+ trafficData.Workspace,
+ )
+ data.PlanDetails = &PlanDetails{
+ Title: trafficData.PlanName,
+ Dates: fmt.Sprintf(config.DatesFormat, trafficData.ExpirationDate),
+ Location: fmt.Sprintf("%s/%s", trafficData.RegionDomain, trafficData.Workspace),
+ Features: trafficData.Features,
+ }
+ if trafficData.UsagePercent >= 100 {
+ data.Content = "Please upgrade your plan immediately to ensure continued operation."
+ data.BorderColor = "#ff0000"
+ }
+ case EventTypeWorkspaceSubscriptionDebt:
+ var subData WorkspaceSubscriptionDebtEventData
+ dataBytes, err := json.Marshal(event.EventData)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal subscription debt event data: %w", err)
+ }
+ if err := json.Unmarshal(dataBytes, &subData); err != nil {
+ return nil, fmt.Errorf("failed to parse subscription debt event data: %w", err)
+ }
+ data.Title = fmt.Sprintf(config.TitleTemplate, subData.RegionDomain, subData.WorkspaceName)
+ data.AlertMessage = fmt.Sprintf(
+ config.AlertTemplate,
+ subData.PlanName,
+ subData.WorkspaceName,
+ subData.RegionDomain,
+ )
+ data.Content = fmt.Sprintf(
+ config.Content,
+ subData.PlanName,
+ subData.WorkspaceName,
+ subData.RegionDomain,
+ )
+ data.WarnContent = config.WarnAlertTemplate
+ data.Recommendation = fmt.Sprintf(
+ config.Recommendation,
+ subData.RegionDomain,
+ subData.WorkspaceName,
+ )
+ data.AlertMessage = fmt.Sprintf(
+ strings.ReplaceAll(
+ config.AlertTemplate,
+ `%s`,
+ `%s`,
+ ),
+ subData.PlanName,
+ subData.WorkspaceName,
+ subData.RegionDomain,
+ )
+ data.WarnContent = config.WarnAlertTemplate
+ default:
+ var subData WorkspaceSubscriptionEventData
+ dataBytes, err := json.Marshal(event.EventData)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal subscription event data: %w", err)
+ }
+ if err := json.Unmarshal(dataBytes, &subData); err != nil {
+ return nil, fmt.Errorf("failed to parse subscription event data: %w", err)
+ }
+ data.Title = fmt.Sprintf(config.TitleTemplate, subData.RegionDomain, subData.WorkspaceName)
+
+ data.AlertMessage = fmt.Sprintf(
+ strings.ReplaceAll(
+ config.AlertTemplate,
+ `%s`,
+ `%s`,
+ ),
+ subData.NewPlanName,
+ subData.WorkspaceName,
+ subData.RegionDomain,
+ )
+ data.PlanDetails = &PlanDetails{
+ Title: subData.NewPlanName,
+ Dates: fmt.Sprintf(config.DatesFormat, subData.ExpirationDate),
+ Location: fmt.Sprintf("%s/%s", subData.RegionDomain, subData.WorkspaceName),
+ Features: subData.Features,
+ }
+ if config.Recommendation != "" {
+ data.Recommendation = fmt.Sprintf(
+ config.Recommendation,
+ subData.RegionDomain,
+ subData.WorkspaceName,
+ )
+ }
+ if event.EventType == EventTypeWorkspaceSubscriptionExpired {
+ data.Content = fmt.Sprintf(config.Content, subData.DaysRemaining)
+ }
+ switch event.EventType {
+ case EventTypeWorkspaceSubscriptionUpgradedSuccess,
+ EventTypeWorkspaceSubscriptionCreatedSuccess:
+ data.Content = fmt.Sprintf(config.Content, subData.NextPayDate)
+ case EventTypeWorkspaceSubscriptionRenewedFailed:
+ data.Content = fmt.Sprintf(config.Content, subData.NewPlanName)
+ }
+ }
+ data.UserName = event.Recipient.UserName
+
+ fmt.Printf("Generated Email Data: %+v\n", data)
+ return &data, nil
+}
+
+// wrapSimpleContent 将简单文本内容包装为基本HTML
+func (p *EmailProvider) wrapSimpleContent(content string) string {
+ return fmt.Sprintf(`
+
+
+
+
+ Sealos通知
+
+
+
+
+
+
+`, content)
+}
+
+// SMSProvider 阿里短信通知提供者
+type SMSProvider struct {
+ BaseProvider
+}
+
+// NewSMSProvider 创建SMS提供者
+func NewSMSProvider(config ProviderConfig) *SMSProvider {
+ provider := &SMSProvider{
+ BaseProvider: BaseProvider{
+ Name: "sms",
+ Available: false,
+ Config: config,
+ },
+ }
+
+ // 检查配置是否完整
+ if config.SMSAccessKeyID == "" || config.SMSAccessKeySecret == "" ||
+ config.SMSDefaultTemplate == "" {
+ log.Printf("SMS provider configuration incomplete, disabling provider")
+ return provider
+ }
+
+ provider.Available = true
+ log.Printf("SMS provider initialized successfully")
+ return provider
+}
+
+func (p *SMSProvider) Send(
+ ctx context.Context,
+ message *NotificationMessage,
+) (*NotificationResult, error) {
+ if !p.IsAvailable() {
+ return nil, errors.New("SMS provider is not available")
+ }
+ // 准备短信模板参数
+ templateParam := ""
+ if message.EventData != nil {
+ if templateParamBytes, err := json.Marshal(message.EventData); err == nil {
+ templateParam = string(templateParamBytes)
+ }
+ }
+
+ if message.Recipient.PhoneNumber == "" || templateParam == "" {
+ // TODO: skip null phone number or empty template param
+ return nil, nil
+ }
+
+ result := &NotificationResult{
+ UserUID: message.UserUID,
+ EventType: message.EventType,
+ Method: message.Method,
+ SentAt: time.Now(),
+ }
+
+ // 实际发送短信通知
+ log.Printf("Sending SMS notification to %s: %s", message.Recipient.PhoneNumber, message.Title)
+
+ // 创建阿里云SMS客户端
+ config := &openapi.Config{
+ AccessKeyId: tea.String(p.Config.SMSAccessKeyID),
+ AccessKeySecret: tea.String(p.Config.SMSAccessKeySecret),
+ Endpoint: tea.String(p.Config.SMSEndpoint),
+ }
+
+ client, err := dysmsapi20170525.NewClient(config)
+ if err != nil {
+ result.Success = false
+ result.Error = fmt.Sprintf("failed to create SMS client: %v", err)
+ return result, err
+ }
+
+ // 构建发送短信请求
+ sendSmsRequest := &dysmsapi20170525.SendSmsRequest{
+ PhoneNumbers: tea.String(message.Recipient.PhoneNumber),
+ SignName: tea.String(p.Config.SMSSignName),
+ TemplateCode: tea.String(message.TemplateID),
+ TemplateParam: tea.String(templateParam),
+ }
+
+ // 发送短信
+ runtime := &util.RuntimeOptions{}
+ resp, err := client.SendSmsWithOptions(sendSmsRequest, runtime)
+ if err != nil {
+ result.Success = false
+ result.Error = fmt.Sprintf("failed to send SMS: %v", err)
+ return result, err
+ }
+
+ // 检查响应结果
+ if resp.Body.Code == nil || *resp.Body.Code != "OK" {
+ errCode := "UNKNOWN"
+ errMsg := "Unknown error"
+ if resp.Body.Code != nil {
+ errCode = *resp.Body.Code
+ }
+ if resp.Body.Message != nil {
+ errMsg = *resp.Body.Message
+ }
+ result.Success = false
+ result.Error = fmt.Sprintf("SMS send failed with code %s: %s", errCode, errMsg)
+ return result, fmt.Errorf("SMS send failed with code %s: %s", errCode, errMsg)
+ }
+
+ // 构建响应数据
+ responseData := map[string]any{
+ "template_code": message.TemplateID,
+ "phone": message.Recipient.PhoneNumber,
+ "content": message.Content,
+ "template_param": templateParam,
+ "sent_at": result.SentAt,
+ "provider": "sms",
+ "sign_name": p.Config.SMSSignName,
+ "response_code": *resp.Body.Code,
+ "request_id": *resp.Body.RequestId,
+ }
+
+ if resp.Body.BizId != nil {
+ responseData["biz_id"] = *resp.Body.BizId
+ }
+
+ if responseBytes, err := json.Marshal(responseData); err == nil {
+ result.ProviderResponse = string(responseBytes)
+ }
+
+ result.Success = true
+ log.Printf(
+ "SMS notification sent successfully to %s, RequestId: %s",
+ message.Recipient.PhoneNumber,
+ *resp.Body.RequestId,
+ )
+ return result, nil
+}
+
+// ProviderManager 通知提供者管理器
+type ProviderManager struct {
+ providers map[NotificationMethod]NotificationProvider
+ contactProvider UserContactProvider
+ contentGen NotificationContentGenerator
+}
+
+// NewProviderManager 创建提供者管理器
+func NewProviderManager(
+ configs map[NotificationMethod]ProviderConfig,
+ contactProvider UserContactProvider,
+) *ProviderManager {
+ manager := &ProviderManager{
+ providers: make(map[NotificationMethod]NotificationProvider),
+ contactProvider: contactProvider,
+ contentGen: NewDefaultContentGenerator(configs),
+ }
+
+ // 初始化各种提供者
+ if vmsConfig, exists := configs[NotificationMethodVMS]; exists {
+ manager.providers[NotificationMethodVMS] = NewVMSProvider(vmsConfig)
+ }
+
+ if emailConfig, exists := configs[NotificationMethodEmail]; exists {
+ manager.providers[NotificationMethodEmail] = NewEmailProvider(emailConfig)
+ }
+
+ if smsConfig, exists := configs[NotificationMethodSMS]; exists {
+ manager.providers[NotificationMethodSMS] = NewSMSProvider(smsConfig)
+ }
+
+ return manager
+}
+
+// GetProvider 获取指定方法的提供者
+func (m *ProviderManager) GetProvider(method NotificationMethod) (NotificationProvider, bool) {
+ provider, exists := m.providers[method]
+ return provider, exists
+}
+
+// GetAvailableProviders 获取所有可用的提供者
+func (m *ProviderManager) GetAvailableProviders() []NotificationProvider {
+ var available []NotificationProvider
+ for _, provider := range m.providers {
+ if provider.IsAvailable() {
+ available = append(available, provider)
+ }
+ }
+ return available
+}
+
+// SendEvent 发送事件通知
+func (m *ProviderManager) SendEvent(
+ ctx context.Context,
+ event *NotificationEvent,
+) ([]*NotificationResult, error) {
+ results := make([]*NotificationResult, 0)
+ var errors []string
+
+ // 如果事件中没有接收者信息,尝试获取
+ if m.contactProvider != nil {
+ contact, err := m.contactProvider.GetUserContact(ctx, event.UserUID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get user contact: %w", err)
+ } else if contact != nil {
+ // 补充接收者信息
+ event.Recipient = *contact
+ }
+ }
+ logrus.Infof("recipient info: %+v", event.Recipient)
+
+ // 为每种通知方式生成消息并发送
+ for _, method := range event.Methods {
+ provider, exists := m.GetProvider(method)
+ if !exists {
+ // TODO 临时去掉
+ // return nil, fmt.Errorf("failed to get provider for method: %s", method)
+ continue
+ }
+
+ if !provider.IsAvailable() {
+ errors = append(errors, fmt.Sprintf("provider %s is not available", provider.GetName()))
+ continue
+ }
+ switch provider.GetName() {
+ case "email":
+ if event.Recipient.Email == "" && !event.NotIgnoreIfNoContact {
+ // return nil, fmt.Errorf("email required for email notification")
+ continue
+ }
+ case "sms", "vms":
+ if event.Recipient.PhoneNumber == "" && !event.NotIgnoreIfNoContact {
+ continue
+ }
+ }
+
+ // 生成通知内容
+ title, content, templateID, err := m.contentGen.GenerateContent(event, method)
+ if err != nil {
+ errors = append(
+ errors,
+ fmt.Sprintf("failed to generate content for %s: %v", method, err),
+ )
+ continue
+ }
+
+ // 创建通知消息
+ message := &NotificationMessage{
+ UserUID: event.UserUID,
+ EventType: event.EventType,
+ Method: method,
+ Priority: event.Priority,
+ Title: title,
+ Content: content,
+ Recipient: event.Recipient,
+ EventData: event.EventData,
+ TemplateID: templateID,
+ Timestamp: event.Timestamp,
+ }
+
+ // 发送通知
+ result, err := provider.Send(ctx, message)
+ if err != nil {
+ // 创建失败结果
+ result = &NotificationResult{
+ UserUID: event.UserUID,
+ EventType: event.EventType,
+ Method: method,
+ Success: false,
+ Error: err.Error(),
+ SentAt: time.Now(),
+ }
+ errors = append(errors, fmt.Sprintf("failed to send %s notification: %v", method, err))
+ }
+
+ results = append(results, result)
+ }
+
+ var finalError error
+ if len(errors) > 0 {
+ finalError = fmt.Errorf("notification sending errors: %s", strings.Join(errors, "; "))
+ }
+
+ return results, finalError
+}
+
+// SetContactProvider 设置用户联系方式提供者
+func (m *ProviderManager) SetContactProvider(provider UserContactProvider) {
+ m.contactProvider = provider
+}
+
+// SetContentGenerator 设置内容生成器
+func (m *ProviderManager) SetContentGenerator(generator NotificationContentGenerator) {
+ m.contentGen = generator
+}
diff --git a/controllers/pkg/user_notify/service.go b/controllers/pkg/user_notify/service.go
new file mode 100644
index 000000000000..dc1ae415d4fe
--- /dev/null
+++ b/controllers/pkg/user_notify/service.go
@@ -0,0 +1,657 @@
+// Copyright © 2024 sealos.
+//
+// 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 usernotify
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/labring/sealos/controllers/pkg/types"
+)
+
+// EventNotificationService 事件驱动的通知服务
+type EventNotificationService interface {
+ // 发送事件通知
+ SendEventNotification(
+ ctx context.Context,
+ event *NotificationEvent,
+ ) ([]*NotificationResult, error)
+
+ // 债务状态变更事件
+ HandleDebtStatusChange(
+ ctx context.Context,
+ userUID uuid.UUID,
+ lastStatus, currentStatus types.DebtStatusType,
+ debtDays int,
+ methods []NotificationMethod,
+ ) ([]*NotificationResult, error)
+
+ // 订阅相关事件
+ HandleWorkspaceSubscriptionEvent(
+ ctx context.Context,
+ userUID uuid.UUID,
+ eventData EventData,
+ operator types.SubscriptionOperator,
+ methods []NotificationMethod,
+ ) ([]*NotificationResult, error)
+
+ // 流量相关事件
+ HandleTrafficEvent(
+ ctx context.Context,
+ userUID uuid.UUID,
+ status types.WorkspaceTrafficStatus,
+ usagePercent int,
+ workspace string,
+ methods []NotificationMethod,
+ ) ([]*NotificationResult, error)
+
+ // 自定义事件
+ HandleCustomEvent(
+ ctx context.Context,
+ userUID uuid.UUID,
+ title, content string,
+ extraData map[string]any,
+ methods []NotificationMethod,
+ ) ([]*NotificationResult, error)
+}
+
+// eventNotificationServiceImpl 事件通知服务实现
+type eventNotificationServiceImpl struct {
+ providerManager *ProviderManager
+}
+
+// NewEventNotificationService 创建事件通知服务
+func NewEventNotificationService(
+ configs map[NotificationMethod]ProviderConfig,
+ contactProvider UserContactProvider,
+) EventNotificationService {
+ providerManager := NewProviderManager(configs, contactProvider)
+
+ return &eventNotificationServiceImpl{
+ providerManager: providerManager,
+ }
+}
+
+// SendEventNotification 发送事件通知
+func (s *eventNotificationServiceImpl) SendEventNotification(
+ ctx context.Context,
+ event *NotificationEvent,
+) ([]*NotificationResult, error) {
+ // 设置默认值
+ if event.Priority == "" {
+ event.Priority = NotificationPriorityNormal
+ }
+ if event.Timestamp.IsZero() {
+ event.Timestamp = time.Now()
+ }
+
+ // 发送通知
+ results, err := s.providerManager.SendEvent(ctx, event)
+
+ // 记录发送日志
+ for _, result := range results {
+ if result.Success {
+ log.Printf("Event notification sent successfully: UserUID=%s, EventType=%s, Method=%s",
+ result.UserUID, result.EventType, result.Method)
+ } else {
+ log.Printf("Event notification failed: UserUID=%s, EventType=%s, Method=%s, Error=%s",
+ result.UserUID, result.EventType, result.Method, result.Error)
+ }
+ }
+
+ return results, err
+}
+
+// HandleDebtStatusChange 处理债务状态变更事件
+func (s *eventNotificationServiceImpl) HandleDebtStatusChange(
+ ctx context.Context,
+ userUID uuid.UUID,
+ lastStatus, currentStatus types.DebtStatusType,
+ debtDays int,
+ methods []NotificationMethod,
+) ([]*NotificationResult, error) {
+ // 只在状态真正发生变化时发送通知
+ if lastStatus == currentStatus {
+ return nil, nil
+ }
+
+ // 构建事件数据
+ eventData := map[string]any{
+ "last_status": string(lastStatus),
+ "current_status": string(currentStatus),
+ "debt_days": debtDays,
+ }
+
+ // 根据债务状态设置优先级
+ var priority NotificationPriority
+ switch currentStatus {
+ case types.FinalDeletionPeriod:
+ priority = NotificationPriorityCritical
+ case types.DebtDeletionPeriod:
+ priority = NotificationPriorityHigh
+ case types.DebtPeriod:
+ priority = NotificationPriorityHigh
+ default:
+ priority = NotificationPriorityNormal
+ }
+
+ event := &NotificationEvent{
+ UserUID: userUID,
+ EventType: EventTypeDebtStatusChange,
+ EventData: eventData,
+ Methods: methods,
+ Priority: priority,
+ Timestamp: time.Now(),
+ }
+
+ return s.SendEventNotification(ctx, event)
+}
+
+// HandleSubscriptionEvent 处理订阅相关事件
+func (s *eventNotificationServiceImpl) HandleWorkspaceSubscriptionEvent(
+ ctx context.Context,
+ userUID uuid.UUID,
+ eventData EventData,
+ operator types.SubscriptionOperator,
+ methods []NotificationMethod,
+) ([]*NotificationResult, error) {
+ // 构建事件数据
+ // eventData := map[string]interface{}{
+ // "workspace_name": workspace,
+ // "operator": string(operator),
+ // "old_plan_name": oldPlan,
+ // "new_plan_name": newPlan,
+ // "pay_status": string(payStatus),
+ //}
+ //
+ // if errorReason != "" {
+ // eventData["error_reason"] = errorReason
+ //}
+ eventDataMap := eventData.ToMap()
+ payStatus := eventDataMap["pay_status"]
+ // payMethod := eventDataMap["pay_method"]
+ // 根据操作类型设置优先级
+ var priority NotificationPriority
+ if payStatus == types.SubscriptionPayStatusFailed ||
+ operator == types.SubscriptionTransactionTypeCanceled {
+ priority = NotificationPriorityHigh
+ } else {
+ priority = NotificationPriorityNormal
+ }
+ var eventType EventType
+ if eventDataMap["type"] != nil {
+ if et, ok := eventDataMap["type"].(EventType); ok {
+ eventType = et
+ }
+ }
+ if eventType == "" {
+ switch operator {
+ case types.SubscriptionTransactionTypeCreated:
+ if payStatus == types.SubscriptionPayStatusPaid {
+ eventType = EventTypeWorkspaceSubscriptionCreatedSuccess
+ } else {
+ eventType = EventTypeWorkspaceSubscriptionCreatedFailed
+ }
+ case types.SubscriptionTransactionTypeUpgraded:
+ if payStatus == types.SubscriptionPayStatusPaid {
+ eventType = EventTypeWorkspaceSubscriptionUpgradedSuccess
+ } else {
+ eventType = EventTypeWorkspaceSubscriptionUpgradedFailed
+ }
+ case types.SubscriptionTransactionTypeDowngraded:
+ case types.SubscriptionTransactionTypeRenewed:
+ switch payStatus {
+ case types.SubscriptionPayStatusPaid:
+ eventType = EventTypeWorkspaceSubscriptionRenewedSuccess
+ case types.SubscriptionPayStatusFailedAndUseBalance:
+ eventType = EventTypeWorkspaceSubscriptionRenewedBalanceFallback
+ default:
+ eventType = EventTypeWorkspaceSubscriptionRenewedFailed
+ }
+ }
+ }
+
+ event := &NotificationEvent{
+ UserUID: userUID,
+ EventType: eventType,
+ EventData: eventData.ToMap(),
+ Methods: methods,
+ Priority: priority,
+ Timestamp: time.Now(),
+ }
+
+ return s.SendEventNotification(ctx, event)
+}
+
+// HandleTrafficEvent 处理流量相关事件
+func (s *eventNotificationServiceImpl) HandleTrafficEvent(
+ ctx context.Context,
+ userUID uuid.UUID,
+ status types.WorkspaceTrafficStatus,
+ usagePercent int,
+ workspace string,
+ methods []NotificationMethod,
+) ([]*NotificationResult, error) {
+ // 构建事件数据
+ eventData := map[string]any{
+ "status": string(status),
+ "usage_percent": usagePercent,
+ "workspace": workspace,
+ }
+
+ // 根据流量状态设置优先级
+ var priority NotificationPriority
+ switch status {
+ case types.WorkspaceTrafficStatusExhausted, types.WorkspaceTrafficStatusUsedUp:
+ priority = NotificationPriorityHigh
+ default:
+ switch {
+ case usagePercent >= 90:
+ priority = NotificationPriorityHigh
+ case usagePercent >= 80:
+ priority = NotificationPriorityNormal
+ default:
+ return nil, nil // 低于80%不发送通知
+ }
+ }
+
+ event := &NotificationEvent{
+ UserUID: userUID,
+ EventType: EventTypeTrafficStatusChange,
+ EventData: eventData,
+ Methods: methods,
+ Priority: priority,
+ Timestamp: time.Now(),
+ }
+
+ return s.SendEventNotification(ctx, event)
+}
+
+// HandleCustomEvent 处理自定义事件
+func (s *eventNotificationServiceImpl) HandleCustomEvent(
+ ctx context.Context,
+ userUID uuid.UUID,
+ title, content string,
+ extraData map[string]any,
+ methods []NotificationMethod,
+) ([]*NotificationResult, error) {
+ // 构建事件数据
+ eventData := map[string]any{
+ "title": title,
+ "content": content,
+ }
+
+ if extraData != nil {
+ eventData["extra_data"] = extraData
+ }
+
+ event := &NotificationEvent{
+ UserUID: userUID,
+ EventType: EventTypeCustom,
+ EventData: eventData,
+ Methods: methods,
+ Priority: NotificationPriorityNormal,
+ Timestamp: time.Now(),
+ }
+
+ return s.SendEventNotification(ctx, event)
+}
+
+// NotificationHelper 通知助手,提供便捷的事件发送方法
+type NotificationHelper struct {
+ service EventNotificationService
+}
+
+// NewNotificationHelper 创建通知助手
+func NewNotificationHelper(service EventNotificationService) *NotificationHelper {
+ return &NotificationHelper{
+ service: service,
+ }
+}
+
+// SendDebtNotification 发送欠费通知
+func (h *NotificationHelper) SendDebtNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ methods []NotificationMethod,
+) error {
+ _, err := h.service.HandleDebtStatusChange(
+ ctx,
+ userUID,
+ types.NormalPeriod,
+ types.DebtPeriod,
+ 0,
+ methods,
+ )
+ return err
+}
+
+// SendDebt3DaysNotification 发送欠费3天通知
+func (h *NotificationHelper) SendDebt3DaysNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ methods []NotificationMethod,
+) error {
+ _, err := h.service.HandleDebtStatusChange(
+ ctx,
+ userUID,
+ types.DebtPeriod,
+ types.DebtDeletionPeriod,
+ 3,
+ methods,
+ )
+ return err
+}
+
+// SendDebt7DaysPreCleanupNotification 发送欠费7天预清理通知
+func (h *NotificationHelper) SendDebt7DaysPreCleanupNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ methods []NotificationMethod,
+) error {
+ _, err := h.service.HandleDebtStatusChange(
+ ctx,
+ userUID,
+ types.DebtDeletionPeriod,
+ types.FinalDeletionPeriod,
+ 7,
+ methods,
+ )
+ return err
+}
+
+// SendTrafficLow80Notification 发送流量不足80%通知
+func (h *NotificationHelper) SendTrafficLow80Notification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ usagePercent int,
+ methods []NotificationMethod,
+) error {
+ _, err := h.service.HandleTrafficEvent(
+ ctx,
+ userUID,
+ types.WorkspaceTrafficStatusActive,
+ usagePercent,
+ "default",
+ methods,
+ )
+ return err
+}
+
+// SendTrafficExhaustedNotification 发送流量已用尽通知
+func (h *NotificationHelper) SendTrafficExhaustedNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ methods []NotificationMethod,
+) error {
+ _, err := h.service.HandleTrafficEvent(
+ ctx,
+ userUID,
+ types.WorkspaceTrafficStatusExhausted,
+ 100,
+ "default",
+ methods,
+ )
+ return err
+}
+
+// SendWorkspaceSubscriptionCreateSuccessNotification 发送创建订阅成功通知
+func (h *NotificationHelper) SendWorkspaceSubscriptionCreateSuccessNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ eventData EventData,
+ methods []NotificationMethod,
+) error {
+ _, err := h.service.HandleWorkspaceSubscriptionEvent(
+ ctx,
+ userUID,
+ eventData,
+ types.SubscriptionTransactionTypeCreated,
+ methods,
+ )
+ return err
+}
+
+// SendWorkspaceSubscriptionCreateFailedNotification 发送创建订阅失败通知
+func (h *NotificationHelper) SendWorkspaceSubscriptionCreateFailedNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ eventData EventData,
+ methods []NotificationMethod,
+) error {
+ _, err := h.service.HandleWorkspaceSubscriptionEvent(
+ ctx,
+ userUID,
+ eventData,
+ types.SubscriptionTransactionTypeCreated,
+ methods,
+ )
+ return err
+}
+
+// SendWorkspaceSubscriptionRenewSuccessNotification 发送续订成功通知
+func (h *NotificationHelper) SendWorkspaceSubscriptionRenewSuccessNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ eventData EventData,
+ methods []NotificationMethod,
+) error {
+ _, err := h.service.HandleWorkspaceSubscriptionEvent(
+ ctx,
+ userUID,
+ eventData,
+ types.SubscriptionTransactionTypeRenewed,
+ methods,
+ )
+ return err
+}
+
+// SendWorkspaceSubscriptionUpgradeSuccessNotification 发送升级成功通知
+func (h *NotificationHelper) SendWorkspaceSubscriptionUpgradeSuccessNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ eventData EventData,
+ methods []NotificationMethod,
+) error {
+ _, err := h.service.HandleWorkspaceSubscriptionEvent(
+ ctx,
+ userUID,
+ eventData,
+ types.SubscriptionTransactionTypeUpgraded,
+ methods,
+ )
+ return err
+}
+
+// SendWorkspaceSubscriptionUpgradeFailedNotification 发送升级失败通知
+func (h *NotificationHelper) SendWorkspaceSubscriptionUpgradeFailedNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ eventData EventData,
+ methods []NotificationMethod,
+) error {
+ _, err := h.service.HandleWorkspaceSubscriptionEvent(
+ ctx,
+ userUID,
+ eventData,
+ types.SubscriptionTransactionTypeUpgraded,
+ methods,
+ )
+ return err
+}
+
+// SendWorkspaceSubscriptionDowngradeSuccessNotification 发送降级成功通知
+func (h *NotificationHelper) SendWorkspaceSubscriptionDowngradeSuccessNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ eventData EventData,
+ methods []NotificationMethod,
+) error {
+ _, err := h.service.HandleWorkspaceSubscriptionEvent(
+ ctx,
+ userUID,
+ eventData,
+ types.SubscriptionTransactionTypeDowngraded,
+ methods,
+ )
+ return err
+}
+
+// SendWorkspaceSubscriptionRenewFailedNotification 发送续订失败通知
+func (h *NotificationHelper) SendWorkspaceSubscriptionRenewFailedNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ eventData EventData,
+ methods []NotificationMethod,
+) error {
+ _, err := h.service.HandleWorkspaceSubscriptionEvent(
+ ctx,
+ userUID,
+ eventData,
+ types.SubscriptionTransactionTypeRenewed,
+ methods,
+ )
+ return err
+}
+
+// SendDatabaseResourceLowNotification 发送数据库资源不足通知
+func (h *NotificationHelper) SendDatabaseResourceLowNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ databaseName string,
+ usagePercent int,
+ methods []NotificationMethod,
+) error {
+ extraData := map[string]any{
+ "database_name": databaseName,
+ "usage_percent": usagePercent,
+ }
+ title := "数据库资源预警"
+ content := fmt.Sprintf("您的数据库%s资源使用量已达到%d%%,请注意优化。", databaseName, usagePercent)
+
+ _, err := h.service.HandleCustomEvent(ctx, userUID, title, content, extraData, methods)
+ return err
+}
+
+// SendDatabaseAbnormalNotification 发送数据库异常通知
+func (h *NotificationHelper) SendDatabaseAbnormalNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ databaseName, errorReason string,
+ methods []NotificationMethod,
+) error {
+ extraData := map[string]any{
+ "database_name": databaseName,
+ "error_reason": errorReason,
+ }
+ title := "数据库异常通知"
+ content := fmt.Sprintf("您的数据库%s出现异常:%s", databaseName, errorReason)
+
+ _, err := h.service.HandleCustomEvent(ctx, userUID, title, content, extraData, methods)
+ return err
+}
+
+// SendCustomNotification 发送自定义通知
+func (h *NotificationHelper) SendCustomNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ title, content string,
+ methods []NotificationMethod,
+ priority NotificationPriority,
+ extraData map[string]any,
+) error {
+ _, err := h.service.HandleCustomEvent(ctx, userUID, title, content, extraData, methods)
+ return err
+}
+
+// SendScheduledNotification 发送定时通知
+func (h *NotificationHelper) SendScheduledNotification(
+ ctx context.Context,
+ userUID uuid.UUID,
+ eventType EventType,
+ scheduleAt time.Time,
+ methods []NotificationMethod,
+ extraData map[string]any,
+) error {
+ // 事件驱动的通知系统不支持定时发送,这个功能需要外部调度系统来实现
+ // 这里直接发送通知
+ title := "定时通知"
+ content := "这是一个定时通知"
+ if extraData != nil {
+ if t, ok := extraData["title"].(string); ok {
+ title = t
+ }
+ if c, ok := extraData["content"].(string); ok {
+ content = c
+ }
+ }
+
+ _, err := h.service.HandleCustomEvent(ctx, userUID, title, content, extraData, methods)
+ return err
+}
+
+// BatchNotificationRequest 批量通知请求
+type BatchNotificationRequest struct {
+ UserUID uuid.UUID `json:"user_uid"`
+ EventType EventType `json:"event_type"`
+ Methods []NotificationMethod `json:"methods"`
+ Priority NotificationPriority `json:"priority"`
+ Title string `json:"title"`
+ Content string `json:"content"`
+ TemplateData map[string]any `json:"template_data"`
+}
+
+// SendBatchNotifications 批量发送通知
+func (h *NotificationHelper) SendBatchNotifications(
+ ctx context.Context,
+ requests []BatchNotificationRequest,
+) ([]*NotificationResult, error) {
+ var allResults []*NotificationResult
+ var errors []string
+
+ for _, req := range requests {
+ extraData := req.TemplateData
+ if extraData == nil {
+ extraData = make(map[string]any)
+ }
+
+ results, err := h.service.HandleCustomEvent(
+ ctx,
+ req.UserUID,
+ req.Title,
+ req.Content,
+ extraData,
+ req.Methods,
+ )
+ if err != nil {
+ errors = append(
+ errors,
+ fmt.Sprintf("failed to send notification to user %s: %v", req.UserUID, err),
+ )
+ }
+
+ allResults = append(allResults, results...)
+ }
+
+ var finalError error
+ if len(errors) > 0 {
+ finalError = fmt.Errorf("batch notification errors: %v", errors)
+ }
+
+ return allResults, finalError
+}
diff --git a/controllers/pkg/user_notify/types.go b/controllers/pkg/user_notify/types.go
new file mode 100644
index 000000000000..d7a81187272b
--- /dev/null
+++ b/controllers/pkg/user_notify/types.go
@@ -0,0 +1,395 @@
+// Copyright © 2024 sealos.
+//
+// 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 usernotify
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "text/template"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/labring/sealos/controllers/pkg/types"
+)
+
+// NotificationMethod 通知方式枚举
+type NotificationMethod string
+
+const (
+ NotificationMethodVMS NotificationMethod = "vms"
+ NotificationMethodEmail NotificationMethod = "email"
+ NotificationMethodSMS NotificationMethod = "sms"
+)
+
+// NotificationPriority 通知优先级枚举
+type NotificationPriority string
+
+const (
+ NotificationPriorityLow NotificationPriority = "low"
+ NotificationPriorityNormal NotificationPriority = "normal"
+ NotificationPriorityHigh NotificationPriority = "high"
+ NotificationPriorityCritical NotificationPriority = "critical"
+)
+
+// EventType 事件类型,基于现有的业务状态
+type EventType string
+
+const (
+ // 债务相关事件
+ EventTypeDebtStatusChange EventType = "debt_status_change"
+
+ // 订阅相关事件
+ EventTypeSubscriptionStatusChange EventType = "subscription_status_change"
+ EventTypeSubscriptionRenewalFailed EventType = "subscription_renewal_failed"
+
+ EventTypeSubscriptionOperationDone EventType = "subscription_operation_done"
+ EventTypeSubscriptionPaymentDone EventType = "subscription_payment_done"
+
+ EventTypeUniversalWorkspaceSubscriptionEvent EventType = "universal_workspace_subscription_event"
+
+ EventTypeWorkspaceSubscriptionCreatedSuccess EventType = "workspace_subscription_created_success"
+ EventTypeWorkspaceSubscriptionCreatedFailed EventType = "workspace_subscription_created_failed"
+ EventTypeWorkspaceSubscriptionUpgradedSuccess EventType = "workspace_subscription_upgraded_success"
+ EventTypeWorkspaceSubscriptionUpgradedFailed EventType = "workspace_subscription_upgraded_failed"
+ EventTypeWorkspaceSubscriptionDowngradedSuccess EventType = "workspace_subscription_downgraded_success"
+ EventTypeWorkspaceSubscriptionDowngradedFailed EventType = "workspace_subscription_downgraded_failed"
+ EventTypeWorkspaceSubscriptionRenewedSuccess EventType = "workspace_subscription_renewed_success"
+ EventTypeWorkspaceSubscriptionRenewedBalanceFallback EventType = "workspace_subscription_renewed_balance_fallback"
+ EventTypeWorkspaceSubscriptionRenewedFailed EventType = "workspace_subscription_renewed_failed"
+ EventTypeWorkspaceSubscriptionExpired EventType = "workspace_subscription_expired"
+ EventTypeWorkspaceSubscriptionExpiredDeleteResources EventType = "workspace_subscription_expired_delete_resources"
+
+ // 流量相关事件
+ EventTypeTrafficStatusChange EventType = "traffic_status_change"
+ EventTypeTrafficUsageAlert EventType = "traffic_usage_alert"
+
+ // 债务预警事件
+ // 债务到期
+ EventTypeWorkspaceSubscriptionDebt EventType = "workspace_subscription_debt"
+ // 债务删除预警
+ EventTypeWorkspaceSubscriptionDebtPreDeletion EventType = "workspace_subscription_debt_pre_deletion"
+ // 债务最终删除预警
+ EventTypeWorkspaceSubscriptionDebtFinalDeletion EventType = "workspace_subscription_debt_final_deletion"
+
+ // 自定义事件
+ EventTypeCustom EventType = "custom"
+)
+
+// NotificationEvent 通知事件结构(输入)
+type NotificationEvent struct {
+ UserUID uuid.UUID `json:"user_uid"`
+ EventType EventType `json:"event_type"`
+ EventData map[string]any `json:"event_data"`
+ Recipient types.NotificationRecipient `json:"recipient,omitempty"`
+ Methods []NotificationMethod `json:"methods"`
+ Priority NotificationPriority `json:"priority,omitempty"`
+ Timestamp time.Time `json:"timestamp"`
+ // Whether to ignore users without contact information, the default is false to ignore
+ NotIgnoreIfNoContact bool `json:"ignore_if_no_contact,omitempty"`
+}
+
+type EventData interface {
+ ToMap() map[string]any
+}
+
+func (t *WorkspaceSubscriptionTrafficEventData) ToMap() map[string]any {
+ return map[string]any{
+ "type": t.Type,
+ "status": t.Status,
+ "used_percentage": t.UsagePercent,
+ "total_bytes": t.TotalBytes,
+ "used_bytes": t.UsedBytes,
+ "workspace_name": t.Workspace,
+ "region_domain": t.RegionDomain,
+ "plan_name": t.PlanName,
+ "features": t.Features,
+ "expiration_date": t.ExpirationDate,
+ }
+}
+
+func (t *WorkspaceSubscriptionTrafficEventData) GetType() EventType {
+ return t.Type
+}
+
+func (t *DebtEventData) ToMap() map[string]any {
+ return map[string]any{
+ "type": t.Type,
+ "last_status": t.LastStatus,
+ "current_status": t.CurrentStatus,
+ "debt_days": t.DebtDays,
+ }
+}
+
+func (t *DebtEventData) GetType() EventType {
+ return t.Type
+}
+
+func (t *WorkspaceSubscriptionDebtEventData) ToMap() map[string]any {
+ return map[string]any{
+ "type": t.Type,
+ "last_status": t.LastStatus,
+ "current_status": t.CurrentStatus,
+ "debt_days": t.DebtDays,
+ "region_domain": t.RegionDomain,
+ "workspace_name": t.WorkspaceName,
+ "expiration_date": t.ExpirationDate,
+ "plan_name": t.PlanName,
+ }
+}
+
+func (t *WorkspaceSubscriptionEventData) GetType() EventType {
+ return t.Type
+}
+
+func (t *CustomEventData) GetType() EventType {
+ return t.Type
+}
+
+func (t *WorkspaceSubscriptionEventData) ToMap() map[string]any {
+ return map[string]any{
+ "type": t.Type,
+ "region_domain": t.RegionDomain,
+ "expiration_date": t.ExpirationDate,
+ "workspace_name": t.WorkspaceName,
+ "domain": t.Domain,
+ "operator": t.Operator,
+ "status": t.Status,
+ "pay_status": t.PayStatus,
+ "pay_method": t.PayMethod,
+ "days_remaining": t.DaysRemaining,
+ "old_plan_name": t.OldPlanName,
+ "new_plan_name": t.NewPlanName,
+ "amount": t.Amount,
+ "error_reason": t.ErrorReason,
+ "next_pay_date": t.NextPayDate,
+ "features": t.Features,
+ }
+}
+
+func (t *CustomEventData) ToMap() map[string]any {
+ return map[string]any{
+ "type": t.Type,
+ "title": t.Title,
+ "content": t.Content,
+ "extra_data": t.ExtraData,
+ }
+}
+
+// NotificationMessage 通知消息结构(内部处理)
+type NotificationMessage struct {
+ UserUID uuid.UUID `json:"user_uid"`
+ EventType EventType `json:"event_type"`
+ Method NotificationMethod `json:"method"`
+ Priority NotificationPriority `json:"priority"`
+ Title string `json:"title"`
+ Content string `json:"content"`
+ Recipient types.NotificationRecipient `json:"recipient"`
+ EventData map[string]any `json:"event_data"`
+ TemplateID string `json:"template_id,omitempty"`
+ Timestamp time.Time `json:"timestamp"`
+}
+
+// NotificationResult 通知发送结果
+type NotificationResult struct {
+ UserUID uuid.UUID `json:"user_uid"`
+ EventType EventType `json:"event_type"`
+ Method NotificationMethod `json:"method"`
+ Success bool `json:"success"`
+ Error string `json:"error,omitempty"`
+ ProviderResponse string `json:"provider_response,omitempty"`
+ SentAt time.Time `json:"sent_at"`
+}
+
+// NotificationProvider 通知提供者接口(简化版)
+type NotificationProvider interface {
+ Send(ctx context.Context, message *NotificationMessage) (*NotificationResult, error)
+ GetName() string
+ IsAvailable() bool
+ GetSupportedMethods() []NotificationMethod
+}
+
+// UserContactProvider 用户联系方式提供者接口
+type UserContactProvider interface {
+ GetUserContact(ctx context.Context, userUID uuid.UUID) (*types.NotificationRecipient, error)
+ SetUserContact(userUID uuid.UUID, recipient *types.NotificationRecipient)
+ RemoveUserContact(userUID uuid.UUID)
+}
+
+// NotificationContentGenerator 通知内容生成器接口
+type NotificationContentGenerator interface {
+ GenerateContent(
+ event *NotificationEvent,
+ method NotificationMethod,
+ ) (title, content, templateID string, err error)
+}
+
+// ProviderConfig 通知提供者配置
+type ProviderConfig struct {
+ // VMS配置
+ VMSAPIKey string `json:"vms_api_key"`
+ VMSAPISecret string `json:"vms_api_secret"`
+ VMSEndpoint string `json:"vms_endpoint"`
+ VMSTemplates map[EventType]string `json:"vms_templates"` // 按事件类型配置模板ID
+ VMSNumberPool string `json:"vms_number_pool"`
+ VMSDefaultTemplate string `json:"vms_default_template"` // 默认模板ID
+
+ // Email配置
+ SMTPHost string `json:"smtp_host"`
+ SMTPPort int `json:"smtp_port"`
+ SMTPUsername string `json:"smtp_username"`
+ SMTPPassword string `json:"smtp_password"`
+ FromEmail string `json:"from_email"`
+ FromName string `json:"from_name"`
+ EmailTemplates map[EventType]string `json:"email_templates"` // 按事件类型配置邮件模板
+ EmailDefaultTemplate string `json:"email_default_template"` // 默认邮件模板
+
+ // SMS配置
+ SMSAccessKeyID string `json:"sms_access_key_id"`
+ SMSAccessKeySecret string `json:"sms_access_key_secret"`
+ SMSEndpoint string `json:"sms_endpoint"`
+ SMSSignName string `json:"sms_sign_name"`
+ SMSTemplates map[EventType]string `json:"sms_templates"` // 按事件类型配置短信模板
+ SMSDefaultTemplate string `json:"sms_default_template"` // 默认短信模板
+
+ // 通用配置
+ IsEnabled bool `json:"is_enabled"`
+ MaxRetries int `json:"max_retries"`
+ RetryDelay time.Duration `json:"retry_delay"`
+ Timeout time.Duration `json:"timeout"`
+ ExtraConfig map[string]any `json:"extra_config,omitempty"`
+}
+
+func ParseConfigsWithJSON(cfgStr string) (map[NotificationMethod]ProviderConfig, error) {
+ var configs map[NotificationMethod]ProviderConfig
+ err := json.Unmarshal([]byte(cfgStr), &configs)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse config json: %w", err)
+ }
+ return configs, nil
+}
+
+// GetVMSTemplateID 根据事件类型获取对应的模板ID
+func (c *ProviderConfig) GetVMSTemplateID(eventType EventType) string {
+ if c.VMSTemplates != nil {
+ if templateID, exists := c.VMSTemplates[eventType]; exists && templateID != "" {
+ return templateID
+ }
+ }
+ return c.VMSDefaultTemplate
+}
+
+// GetSMSTemplateCode 根据事件类型获取对应的短信模板代码
+func (c *ProviderConfig) GetSMSTemplateCode(eventType EventType) string {
+ if c.SMSTemplates != nil {
+ if templateCode, exists := c.SMSTemplates[eventType]; exists && templateCode != "" {
+ return templateCode
+ }
+ }
+ return c.SMSDefaultTemplate
+}
+
+func (c *ProviderConfig) GenerateEmailContent(event *NotificationEvent) (string, error) {
+ emailData, err := generateEmailContent(event)
+ if err != nil {
+ return "", err
+ }
+ if emailData.PlanDetails != nil {
+ fmt.Printf("emailData: %+v\n", emailData.PlanDetails.Features)
+ }
+ // 构建HTML内容
+ tmpl, err := template.New("email").
+ Parse(c.GetEmailTemplate(EventTypeUniversalWorkspaceSubscriptionEvent))
+ if err != nil {
+ return "", fmt.Errorf("failed to parse email template: %w", err)
+ }
+ var renderedContent strings.Builder
+ if err := tmpl.Execute(&renderedContent, emailData); err != nil {
+ return "", fmt.Errorf("failed to render email template: %w", err)
+ }
+ return renderedContent.String(), nil
+}
+
+// GetEmailTemplate 根据事件类型获取对应的邮件模板
+func (c *ProviderConfig) GetEmailTemplate(eventType EventType) string {
+ if c.EmailTemplates != nil {
+ if template, exists := c.EmailTemplates[eventType]; exists && template != "" {
+ return template
+ }
+ }
+ return WorkspaceSubscriptionEventEmailRenderTmpl
+}
+
+// DebtEventData 债务事件数据
+type DebtEventData struct {
+ Type EventType `json:"-"`
+ LastStatus types.DebtStatusType `json:"last_status"`
+ CurrentStatus types.DebtStatusType `json:"current_status"`
+ DebtDays int `json:"debt_days,omitempty"`
+}
+
+type WorkspaceSubscriptionDebtEventData struct {
+ Type EventType `json:"-"`
+ RegionDomain string `json:"region_domain"`
+ WorkspaceName string `json:"workspace_name"`
+ PlanName string `json:"plan_name"`
+ ExpirationDate string `json:"expiration_date"`
+ LastStatus types.SubscriptionStatus `json:"last_status"`
+ CurrentStatus types.SubscriptionStatus `json:"current_status"`
+ DebtDays int `json:"debt_days,omitempty"`
+}
+
+// WorkspaceSubscriptionEventData 订阅事件数据
+type WorkspaceSubscriptionEventData struct {
+ Type EventType `json:"type"`
+ RegionDomain string `json:"region_domain"`
+ ExpirationDate string `json:"expiration_date"`
+ WorkspaceName string `json:"workspace_name"`
+ Domain string `json:"domain,omitempty"`
+ Operator types.SubscriptionOperator `json:"operator,omitempty"`
+ Status types.SubscriptionStatus `json:"status,omitempty"`
+ PayStatus types.SubscriptionPayStatus `json:"pay_status,omitempty"`
+ PayMethod types.PaymentMethod `json:"pay_method,omitempty"`
+ Features []string `json:"features,omitempty"`
+ DaysRemaining int `json:"days_remaining,omitempty"`
+ OldPlanName string `json:"old_plan_name,omitempty"`
+ NewPlanName string `json:"new_plan_name,omitempty"`
+ Amount float64 `json:"amount,omitempty"`
+ NextPayDate string `json:"next_pay_date,omitempty"`
+ ErrorReason string `json:"error_reason,omitempty"`
+}
+
+// WorkspaceSubscriptionTrafficEventData 流量事件数据
+type WorkspaceSubscriptionTrafficEventData struct {
+ RegionDomain string `json:"region_domain"`
+ PlanName string `json:"plan_name,omitempty"`
+ Type EventType `json:"-"`
+ Status types.WorkspaceTrafficStatus `json:"status,omitempty"`
+ ExpirationDate string `json:"expiration_date"`
+ UsagePercent int `json:"used_percentage,omitempty"`
+ TotalBytes int64 `json:"total_bytes,omitempty"`
+ UsedBytes int64 `json:"used_bytes,omitempty"`
+ Workspace string `json:"workspace_name,omitempty"`
+ Features []string `json:"features,omitempty"`
+}
+
+// CustomEventData 自定义事件数据
+type CustomEventData struct {
+ Type EventType `json:"-"`
+ Title string `json:"title"`
+ Content string `json:"content"`
+ ExtraData map[string]any `json:"extra_data,omitempty"`
+}
diff --git a/controllers/pkg/utils/email.go b/controllers/pkg/utils/email.go
new file mode 100644
index 000000000000..c9712226e39e
--- /dev/null
+++ b/controllers/pkg/utils/email.go
@@ -0,0 +1,456 @@
+package utils
+
+import (
+ "strconv"
+ "time"
+
+ "github.com/go-gomail/gomail"
+ "github.com/labring/sealos/controllers/pkg/types"
+)
+
+type SMTPConfig struct {
+ ServerHost string
+ ServerPort int
+ Username string
+ FromEmail string
+ Passwd string
+ EmailTitle string
+}
+
+func (c *SMTPConfig) SendEmail(emailBody, to string) error {
+ m := gomail.NewMessage()
+ m.SetHeader("To", to)
+ m.SetAddressHeader("From", c.FromEmail, c.EmailTitle)
+ m.SetHeader("Subject", c.EmailTitle)
+ m.SetBody("text/html", emailBody)
+ d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.Username, c.Passwd)
+ return d.DialAndSend(m)
+}
+
+func (c *SMTPConfig) SendEmailWithSubject(subject, emailBody, to string) error {
+ m := gomail.NewMessage()
+ m.SetHeader("To", to)
+ m.SetAddressHeader("From", c.FromEmail, c.EmailTitle)
+ m.SetHeader("Subject", subject)
+ m.SetBody("text/html", emailBody)
+ d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.Username, c.Passwd)
+ return d.DialAndSend(m)
+}
+
+const (
+ EnvSMTPHost = "SMTP_HOST"
+ EnvSMTPPort = "SMTP_PORT"
+ EnvSMTPFrom = "SMTP_FROM"
+ EnvSMTPUser = "SMTP_USER"
+ EnvSMTPPassword = "SMTP_PASSWORD"
+ EnvSMTPTitle = "SMTP_TITLE"
+
+ EnvPaySuccessEmailTmpl = "PAY_SUCCESS_EMAIL_TMPL"
+ EnvPayFailedEmailTmpl = "PAY_FAILED_EMAIL_TMPL"
+ EnvSubSuccessEmailTmpl = "SUB_SUCCESS_EMAIL_TMPL"
+ EnvSubFailedEmailTmpl = "SUB_FAILED_EMAIL_TMPL"
+)
+
+type EmailRenderBuilder interface {
+ Build() map[string]any
+ GetType() string
+ SetUserInfo(userInfo *types.UserInfo)
+ GetSubject() string
+}
+
+type EmailPayRender struct {
+ Type string
+ userInfo *types.UserInfo
+ Domain string
+ TopUpAmount int64
+ AccountBalance int64
+}
+
+func (e *EmailPayRender) Build() map[string]any {
+ return map[string]any{
+ "FirstName": e.userInfo.FirstName,
+ "LastName": e.userInfo.LastName,
+ "Domain": e.Domain,
+ "TopUpAmount": strconv.FormatInt(e.TopUpAmount, 10),
+ "AccountBalance": strconv.FormatInt(e.AccountBalance, 10),
+ }
+}
+
+func (e *EmailPayRender) GetType() string {
+ return e.Type
+}
+
+func (e *EmailPayRender) GetSubject() string {
+ return "Top-Up Successful"
+}
+
+func (e *EmailPayRender) SetUserInfo(userInfo *types.UserInfo) {
+ e.userInfo = userInfo
+}
+
+func (e *EmailSubRender) Build() map[string]any {
+ build := map[string]any{
+ "FirstName": e.userInfo.FirstName,
+ "LastName": e.userInfo.LastName,
+ "Domain": e.Domain,
+ "SubscriptionPlanName": e.SubscriptionPlanName,
+ "StartDate": e.StartDate.Format(time.DateOnly),
+ "EndDate": e.EndDate.Format(time.DateOnly),
+ }
+ switch e.SubscriptionPlanName {
+ case "Hobby":
+ build["SubscriptionFeatures"] = []string{
+ "Includes $5 credits",
+ "16 vCPU / 32GiB RAM",
+ "Unlimited disk & traffic within plan",
+ "Multiple regions",
+ "3 workspaces / region",
+ "5 seats / workspace",
+ }
+ case "Pro":
+ build["SubscriptionFeatures"] = []string{
+ "Includes $20 credits",
+ "128 vCPU / 256GiB RAM",
+ "Unlimited disk & traffic within plan",
+ "Multiple regions",
+ "Multiple workspace / region",
+ "Multiple seat / workspace",
+ }
+ }
+ return build
+}
+
+type EmailSubRender struct {
+ Type string
+ Operator types.SubscriptionOperator
+
+ userInfo types.UserInfo
+ Domain string
+
+ SubscriptionPlanName string
+ StartDate time.Time
+ EndDate time.Time
+}
+
+func (e *EmailSubRender) GetType() string {
+ return e.Type
+}
+
+func (e *EmailSubRender) SetUserInfo(userInfo *types.UserInfo) {
+ e.userInfo = *userInfo
+}
+
+func (e *EmailSubRender) GetSubject() string {
+ switch e.Operator {
+ case types.SubscriptionTransactionTypeUpgraded:
+ return "Your Subscription Has Been Successfully Updated"
+ case types.SubscriptionTransactionTypeDowngraded:
+ return "Your Subscription Has Been Successfully Downgraded"
+ case types.SubscriptionTransactionTypeCanceled:
+ return "Your Subscription Has Been Successfully Canceled"
+ case types.SubscriptionTransactionTypeRenewed:
+ return "Your Subscription Has Been Successfully Renewed"
+ default:
+ return "Your Subscription Has Been Successfully Activated"
+ }
+}
+
+type EmailDebtRender struct {
+ Type string
+ CurrentStatus types.DebtStatusType
+
+ userName string
+ Domain string
+ Language string // 语言: "zh" 或 "en"
+ GraceReason []string
+ Balance int64 // 当前余额
+}
+
+type DebtGraceReason string
+
+const (
+ GraceReasonNoBalance DebtGraceReason = "insufficient balance"
+ GraceReasonSubExpired DebtGraceReason = "subscription expired"
+)
+
+func (e *EmailDebtRender) GetType() string {
+ return e.Type
+}
+
+func (e *EmailDebtRender) SetUserName(userName string) {
+ e.userName = userName
+}
+
+func (e *EmailDebtRender) SetLanguage(language string) {
+ e.Language = language
+}
+
+func (e *EmailDebtRender) SetBalance(balance int64) {
+ e.Balance = balance
+}
+
+func (e *EmailDebtRender) GetSubject() string {
+ if e.Language == "zh" {
+ return e.getSubjectZH()
+ }
+ return e.getSubjectEN()
+}
+
+func (e *EmailDebtRender) getSubjectEN() string {
+ if types.ContainDebtStatus(types.DebtStates, e.CurrentStatus) {
+ if e.CurrentStatus == types.FinalDeletionPeriod {
+ return "Important: Your Resources Have Been Deleted"
+ }
+ return "Important: Your Account Has Entered Grace Period"
+ }
+ return "Low Account Balance Reminder"
+}
+
+func (e *EmailDebtRender) getSubjectZH() string {
+ if types.ContainDebtStatus(types.DebtStates, e.CurrentStatus) {
+ if e.CurrentStatus == types.FinalDeletionPeriod {
+ return "重要提醒:您的资源已被删除"
+ }
+ return "重要提醒:您的账户已进入宽限期"
+ }
+ return "账户余额不足提醒"
+}
+
+func (e *EmailDebtRender) Build() map[string]any {
+ balanceInUnits := float64(e.Balance) / 1000000.0
+
+ build := map[string]any{
+ "Type": e.Type,
+ "FirstName": e.userName, // 使用 userName 作为 FirstName
+ "LastName": "", // 留空
+ "Domain": e.Domain,
+ "GraceReason": e.GraceReason,
+ "Language": e.Language,
+ "IsCNY": e.Language == "zh",
+ "Balance": e.Balance, // 原始值(兼容旧代码)
+ "BalanceInUnits": balanceInUnits, // 格式化后的值
+ }
+ if e.Type == "CriticalBalancePeriod" {
+ build["CreditsAvailable"] = "1"
+ }
+ if e.Type == "LowBalancePeriod" {
+ build["CreditsAvailable"] = "5"
+ }
+ return build
+}
+
+// DebtEmailTemplateEN 标准英文债务邮件模板
+const DebtEmailTemplateEN = `
+
+
+
+
+ Sealos - Account Balance Notification
+
+
+
+
+
+
+
+
+
+
+
+ {{if .FirstName}}Hi {{.FirstName}},{{else}}Hi,{{end}}
+
+
+ {{if eq .Type "LowBalancePeriod"}}
+
+ Your account balance is running low. You have ${{printf "%.2f" .BalanceInUnits}} USD in credits remaining.
+
+
+ To avoid any service disruption, please consider topping up your account soon. Your resources will continue to function normally, but we recommend maintaining a sufficient balance.
+
+ {{end}}
+
+ {{if eq .Type "CriticalBalancePeriod"}}
+
+ ⚠️ Critical: Your account balance is critically low.
+ You have ${{printf "%.2f" .BalanceInUnits}} USD in credits remaining.
+
+
+ Please top up your account as soon as possible to prevent service suspension. Your resources may be affected if the balance reaches zero.
+
+ {{end}}
+
+ {{if or (eq .Type "DebtPeriod") (eq .Type "DebtDeletionPeriod") (eq .Type "FinalDeletionPeriod")}}
+
+ ⚠️ Important: Your account has entered the grace period.
+ {{range .GraceReason}}
+ - {{.}}
+ {{end}}
+
+ {{end}}
+
+ {{if eq .Type "DebtPeriod"}}
+
+ Your services have been suspended. To restore your account and prevent data loss, please top up your account immediately. You have 7 days from today before your resources will be permanently deleted.
+
+ {{end}}
+
+ {{if eq .Type "DebtDeletionPeriod"}}
+
+ This is a final reminder. Your resources will be permanently deleted within 7 days. Please take immediate action to recover your account and prevent permanent data loss.
+
+ {{end}}
+
+ {{if eq .Type "FinalDeletionPeriod"}}
+
+ Your resources have been permanently deleted due to prolonged account inactivity and insufficient balance. We're sorry to see you go. If you wish to use Sealos services again in the future, you can start a new subscription at any time.
+
+ {{end}}
+
+
+
+
+ |
+
+
+ |
+
+
+
+`
+
+// DebtEmailTemplateZH 标准中文债务邮件模板
+const DebtEmailTemplateZH = `
+
+
+
+
+ Sealos - 账户余额通知
+
+
+
+
+
+
+
+
+
+
+
+ {{if .FirstName}}您好 {{.FirstName}},{{else}}您好,{{end}}
+
+
+ {{if eq .Type "LowBalancePeriod"}}
+
+ 您的账户余额不足。您当前的余额为 {{printf "%.2f" .BalanceInUnits}} 元。
+
+
+ 为避免影响您的正常使用,请及时充值。您的资源目前可以正常使用,但我们建议保持充足的余额以确保服务稳定。
+
+ {{end}}
+
+ {{if eq .Type "CriticalBalancePeriod"}}
+
+ ⚠️ 紧急提醒:您的账户余额即将耗尽。
+ 您当前的余额为 {{printf "%.2f" .BalanceInUnits}} 元。
+
+
+ 请尽快充值以防止服务暂停。如果余额归零,您的资源可能会受到影响。
+
+ {{end}}
+
+ {{if or (eq .Type "DebtPeriod") (eq .Type "DebtDeletionPeriod") (eq .Type "FinalDeletionPeriod")}}
+
+ ⚠️ 重要提醒:您的账户已进入宽限期。
+ {{range .GraceReason}}
+ - {{.}}
+ {{end}}
+
+ {{end}}
+
+ {{if eq .Type "DebtPeriod"}}
+
+ 您的服务已被暂停。要恢复账户并防止数据丢失,请立即充值。从今天起,您有 7 天 的时间来恢复账户,之后您的资源将被永久删除。
+
+ {{end}}
+
+ {{if eq .Type "DebtDeletionPeriod"}}
+
+ 这是最后提醒。您的资源将在 7 天内 被永久删除。请立即采取措施恢复您的账户,防止永久性数据丢失。
+
+ {{end}}
+
+ {{if eq .Type "FinalDeletionPeriod"}}
+
+ 由于账户长期处于非活跃状态且余额不足,您的资源已被永久删除。很遗憾未能继续为您服务。如果您将来希望再次使用 Sealos 服务,可以随时开始新的订阅。
+
+ {{end}}
+
+
+
+
+ |
+
+
+ |
+
+
+
+`
+
+// GetDebtEmailTemplate 根据语言返回对应的邮件模板
+func GetDebtEmailTemplate(language string) string {
+ if language == "zh" {
+ return DebtEmailTemplateZH
+ }
+ return DebtEmailTemplateEN
+}
diff --git a/controllers/pkg/utils/env/env.go b/controllers/pkg/utils/env/env.go
new file mode 100644
index 000000000000..e7a450030951
--- /dev/null
+++ b/controllers/pkg/utils/env/env.go
@@ -0,0 +1,69 @@
+// Copyright © 2023 sealos.
+//
+// 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 env
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "time"
+)
+
+func GetEnvWithDefault(key, defaultValue string) string {
+ if value, ok := os.LookupEnv(key); ok && value != "" {
+ return value
+ }
+ return defaultValue
+}
+
+func GetBoolWithDefault(key string, defaultValue bool) bool {
+ if env, ok := os.LookupEnv(key); ok && env != "" {
+ if value, err := strconv.ParseBool(env); err == nil {
+ return value
+ }
+ }
+ return defaultValue
+}
+
+func GetInt64EnvWithDefault(key string, defaultValue int64) int64 {
+ if env, ok := os.LookupEnv(key); ok && env != "" {
+ if value, err := strconv.ParseInt(env, 10, 64); err == nil {
+ return value
+ }
+ }
+ return defaultValue
+}
+
+func GetIntEnvWithDefault(key string, defaultValue int) int {
+ return int(GetInt64EnvWithDefault(key, int64(defaultValue)))
+}
+
+func GetDurationEnvWithDefault(key string, defaultValue time.Duration) time.Duration {
+ if env, ok := os.LookupEnv(key); ok && env != "" {
+ if value, err := time.ParseDuration(env); err == nil {
+ return value
+ }
+ }
+ return defaultValue
+}
+
+func CheckEnvSetting(keys []string) error {
+ for _, key := range keys {
+ if val, ok := os.LookupEnv(key); !ok || val == "" {
+ return fmt.Errorf("env %s not set", key)
+ }
+ }
+ return nil
+}
diff --git a/controllers/pkg/utils/flags/compression.go b/controllers/pkg/utils/flags/compression.go
new file mode 100644
index 000000000000..28be3aa83ba6
--- /dev/null
+++ b/controllers/pkg/utils/flags/compression.go
@@ -0,0 +1,73 @@
+// Copyright © 2023 sealos.
+//
+// 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 flags
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/containers/storage/pkg/archive"
+)
+
+type Compression int
+
+const (
+ Disable Compression = iota
+ Uncompressed
+ Bzip2
+ Gzip
+ Xz
+ Zstd
+)
+
+func (c *Compression) String() string {
+ switch *c {
+ case Disable:
+ return "disable"
+ case Uncompressed:
+ return "tar"
+ case Bzip2:
+ return "bzip2"
+ case Gzip:
+ return "gzip"
+ case Xz:
+ return "xz"
+ case Zstd:
+ return "zstd"
+ }
+ return "unknown"
+}
+
+func (c *Compression) Compression() archive.Compression {
+ return archive.Compression(int(*c) - 1)
+}
+
+func (c *Compression) Set(s string) error {
+ switch strings.ToLower(s) {
+ case "gz", "gzip":
+ *c = Gzip
+ case "zst", "zstd":
+ *c = Zstd
+ case "tar", "uncompressed":
+ *c = Uncompressed
+ case "", "disable":
+ *c = Disable
+ default:
+ return fmt.Errorf("unknown compression algorithm %s", s)
+ }
+ return nil
+}
+
+func (c *Compression) Type() string { return "compressionAlgorithm" }
diff --git a/controllers/pkg/utils/flags/flags.go b/controllers/pkg/utils/flags/flags.go
new file mode 100644
index 000000000000..79816f46f32d
--- /dev/null
+++ b/controllers/pkg/utils/flags/flags.go
@@ -0,0 +1,48 @@
+/*
+Copyright 2022 cuisongliu@qq.com.
+
+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 flags
+
+import (
+ "os"
+ "strings"
+
+ "github.com/labring/sealos/controllers/pkg/utils/logger"
+ "github.com/spf13/pflag"
+)
+
+// PrintFlags logs the flags in the flagset
+func PrintFlags(flags *pflag.FlagSet) {
+ flags.VisitAll(func(flag *pflag.Flag) {
+ logger.Debug("FLAG: --%s=%q", flag.Name, flag.Value)
+ })
+}
+
+// SetFlagsFromEnv set value of flag if not changed but has env key
+func SetFlagsFromEnv(prefix string, flags *pflag.FlagSet) {
+ if prefix != "" && !strings.HasSuffix(prefix, "_") {
+ prefix += "_"
+ }
+ flags.VisitAll(func(flag *pflag.Flag) {
+ if flag.Changed {
+ return
+ }
+ envVar := strings.ToUpper(strings.ReplaceAll(prefix+flag.Name, "-", "_"))
+ if v := os.Getenv(envVar); v != "" {
+ _ = flags.Set(flag.Name, v)
+ }
+ })
+}
diff --git a/controllers/pkg/utils/jwt.go b/controllers/pkg/utils/jwt.go
new file mode 100644
index 000000000000..865991020862
--- /dev/null
+++ b/controllers/pkg/utils/jwt.go
@@ -0,0 +1,147 @@
+package utils
+
+import (
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/golang-jwt/jwt"
+ "github.com/google/uuid"
+)
+
+type JWTManager struct {
+ secretKey []byte
+ tokenDuration time.Duration
+}
+
+type UserClaims struct {
+ jwt.StandardClaims `json:",inline"`
+ JwtUser `json:",inline"`
+}
+
+type JwtUser struct {
+ Requester string `json:"requester,omitempty"`
+ UserUID uuid.UUID `json:"userUid,omitempty"`
+ UserCrUID string `json:"userCrUid,omitempty"`
+ UserCrName string `json:"userCrName,omitempty"`
+ RegionUID string `json:"regionUid,omitempty"`
+ UserID string `json:"userId,omitempty"`
+ WorkspaceID string `json:"workspaceId,omitempty"`
+ WorkspaceUID string `json:"workspaceUid,omitempty"`
+}
+
+const (
+ AdminJWTRequester = "sealos-admin"
+ AdminJWTIssuer = "sealos-admin"
+ AdminJWTAudience = "sealos-account-admin"
+)
+
+func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager {
+ return &JWTManager{[]byte(secretKey), tokenDuration}
+}
+
+func (manager *JWTManager) GenerateToken(user JwtUser) (string, error) {
+ claims := UserClaims{
+ StandardClaims: jwt.StandardClaims{
+ ExpiresAt: time.Now().Add(manager.tokenDuration).Unix(),
+ },
+ JwtUser: user,
+ }
+
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ return token.SignedString(manager.secretKey)
+}
+
+func (manager *JWTManager) VerifyToken(tokenString string) (*UserClaims, error) {
+ token, err := jwt.ParseWithClaims(
+ tokenString,
+ &UserClaims{},
+ func(token *jwt.Token) (any, error) {
+ _, ok := token.Method.(*jwt.SigningMethodHMAC)
+ if !ok {
+ return nil, errors.New("unexpected token signing method")
+ }
+
+ return manager.secretKey, nil
+ },
+ )
+ if err != nil {
+ return nil, fmt.Errorf("invalid token: %w", err)
+ }
+
+ claims, ok := token.Claims.(*UserClaims)
+ if !ok {
+ return nil, errors.New("invalid token claims")
+ }
+
+ return claims, nil
+}
+
+func (manager *JWTManager) ParseUser(token string) (*JwtUser, error) {
+ claims, err := manager.VerifyToken(token)
+ if err != nil {
+ return nil, fmt.Errorf("invalid token: %w", err)
+ }
+ return &claims.JwtUser, nil
+}
+
+func (manager *JWTManager) GenerateAdminToken(user JwtUser) (string, error) {
+ now := time.Now()
+ claims := UserClaims{
+ StandardClaims: jwt.StandardClaims{
+ ExpiresAt: now.Add(manager.tokenDuration).Unix(),
+ IssuedAt: now.Unix(),
+ Issuer: AdminJWTIssuer,
+ Audience: AdminJWTAudience,
+ },
+ JwtUser: user,
+ }
+ claims.JwtUser.Requester = AdminJWTRequester
+
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ return token.SignedString(manager.secretKey)
+}
+
+func (manager *JWTManager) VerifyAdminToken(tokenString string) (*UserClaims, error) {
+ token, err := jwt.ParseWithClaims(
+ tokenString,
+ &UserClaims{},
+ func(token *jwt.Token) (any, error) {
+ if token.Method == nil || token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
+ return nil, errors.New("unexpected token signing method")
+ }
+
+ return manager.secretKey, nil
+ },
+ )
+ if err != nil {
+ return nil, fmt.Errorf("invalid admin token: %w", err)
+ }
+
+ claims, ok := token.Claims.(*UserClaims)
+ if !ok {
+ return nil, errors.New("invalid admin token claims")
+ }
+ if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
+ return nil, errors.New("admin token expiration is required")
+ }
+ if !claims.VerifyIssuer(AdminJWTIssuer, true) {
+ return nil, errors.New("invalid admin token issuer")
+ }
+ if !claims.VerifyAudience(AdminJWTAudience, true) {
+ return nil, errors.New("invalid admin token audience")
+ }
+ if claims.JwtUser.Requester != AdminJWTRequester {
+ return nil, errors.New("user is not admin")
+ }
+
+ return claims, nil
+}
+
+func (manager *JWTManager) ParseAdminUser(token string) (*JwtUser, error) {
+ claims, err := manager.VerifyAdminToken(token)
+ if err != nil {
+ return nil, err
+ }
+ return &claims.JwtUser, nil
+}
diff --git a/controllers/pkg/utils/jwt_test.go b/controllers/pkg/utils/jwt_test.go
new file mode 100644
index 000000000000..eb35c30b0bf3
--- /dev/null
+++ b/controllers/pkg/utils/jwt_test.go
@@ -0,0 +1,111 @@
+package utils
+
+import (
+ "testing"
+ "time"
+
+ "github.com/golang-jwt/jwt"
+)
+
+func TestAdminTokenClaims(t *testing.T) {
+ manager := NewJWTManager("admin-secret", time.Hour)
+ token, err := manager.GenerateAdminToken(JwtUser{Requester: "ordinary-user"})
+ if err != nil {
+ t.Fatalf("GenerateAdminToken() error = %v", err)
+ }
+
+ user, err := manager.ParseAdminUser(token)
+ if err != nil {
+ t.Fatalf("ParseAdminUser() error = %v", err)
+ }
+ if user.Requester != AdminJWTRequester {
+ t.Fatalf("ParseAdminUser() requester = %q, want %q", user.Requester, AdminJWTRequester)
+ }
+}
+
+func TestVerifyAdminTokenRejectsInvalidClaims(t *testing.T) {
+ manager := NewJWTManager("admin-secret", time.Hour)
+ now := time.Now().Unix()
+
+ tests := []struct {
+ name string
+ method jwt.SigningMethod
+ claims jwt.StandardClaims
+ secret string
+ }{
+ {
+ name: "ordinary claims",
+ claims: jwt.StandardClaims{
+ ExpiresAt: now + 3600,
+ },
+ secret: "admin-secret",
+ },
+ {
+ name: "wrong issuer",
+ claims: jwt.StandardClaims{
+ ExpiresAt: now + 3600,
+ Issuer: "account-api",
+ Audience: AdminJWTAudience,
+ },
+ secret: "admin-secret",
+ },
+ {
+ name: "wrong audience",
+ claims: jwt.StandardClaims{
+ ExpiresAt: now + 3600,
+ Issuer: AdminJWTIssuer,
+ Audience: "account-api",
+ },
+ secret: "admin-secret",
+ },
+ {
+ name: "expired",
+ claims: jwt.StandardClaims{
+ ExpiresAt: now - 1,
+ Issuer: AdminJWTIssuer,
+ Audience: AdminJWTAudience,
+ },
+ secret: "admin-secret",
+ },
+ {
+ name: "wrong secret",
+ claims: jwt.StandardClaims{
+ ExpiresAt: now + 3600,
+ Issuer: AdminJWTIssuer,
+ Audience: AdminJWTAudience,
+ },
+ secret: "api-secret",
+ },
+ {
+ name: "wrong algorithm",
+ method: jwt.SigningMethodHS512,
+ claims: jwt.StandardClaims{
+ ExpiresAt: now + 3600,
+ Issuer: AdminJWTIssuer,
+ Audience: AdminJWTAudience,
+ },
+ secret: "admin-secret",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ method := tt.method
+ if method == nil {
+ method = jwt.SigningMethodHS256
+ }
+ claims := UserClaims{
+ StandardClaims: tt.claims,
+ JwtUser: JwtUser{Requester: AdminJWTRequester},
+ }
+ token, err := jwt.NewWithClaims(method, claims).SignedString([]byte(tt.secret))
+ if err != nil {
+ t.Fatalf("SignedString() error = %v", err)
+ }
+
+ if _, err = manager.VerifyAdminToken(token); err == nil {
+ t.Fatal("VerifyAdminToken() error = nil, want error")
+ }
+ })
+ }
+}
diff --git a/controllers/pkg/utils/label/constant.go b/controllers/pkg/utils/label/constant.go
new file mode 100644
index 000000000000..172e9a4aa553
--- /dev/null
+++ b/controllers/pkg/utils/label/constant.go
@@ -0,0 +1,31 @@
+// Copyright © 2024 sealos.
+//
+// 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 label
+
+type AppKey = string
+
+// @see: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels
+const (
+ AppName AppKey = "app.kubernetes.io/name"
+ AppInstance AppKey = "app.kubernetes.io/instance"
+ AppVersion AppKey = "app.kubernetes.io/version"
+ AppComponent AppKey = "app.kubernetes.io/component"
+ AppPartOf AppKey = "app.kubernetes.io/part-of"
+ AppManagedBy AppKey = "app.kubernetes.io/managed-by"
+)
+
+const (
+ DefaultManagedBy = "sealos"
+)
diff --git a/controllers/pkg/utils/label/recommend.go b/controllers/pkg/utils/label/recommend.go
new file mode 100644
index 000000000000..e4291c18732c
--- /dev/null
+++ b/controllers/pkg/utils/label/recommend.go
@@ -0,0 +1,53 @@
+// Copyright © 2024 sealos.
+//
+// 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 label
+
+type Recommended struct {
+ Name string
+ Instance string
+ Version string
+ Component string
+ PartOf string
+ ManagedBy string
+}
+
+func (r *Recommended) Labels() map[string]string {
+ ret := map[string]string{}
+
+ if r.Name != "" {
+ ret[AppName] = r.Name
+ }
+ if r.Instance != "" {
+ ret[AppInstance] = r.Instance
+ }
+ if r.Version != "" {
+ ret[AppVersion] = r.Version
+ }
+ if r.Component != "" {
+ ret[AppComponent] = r.Component
+ }
+ if r.PartOf != "" {
+ ret[AppPartOf] = r.PartOf
+ }
+ if r.ManagedBy != "" {
+ ret[AppManagedBy] = r.ManagedBy
+ }
+
+ return ret
+}
+
+func RecommendedLabels(r *Recommended) map[string]string {
+ return r.Labels()
+}
diff --git a/controllers/pkg/utils/lock/descributed_lock.go b/controllers/pkg/utils/lock/descributed_lock.go
new file mode 100644
index 000000000000..3967011226bc
--- /dev/null
+++ b/controllers/pkg/utils/lock/descributed_lock.go
@@ -0,0 +1,180 @@
+package dlock
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "sync"
+ "time"
+
+ "gorm.io/gorm"
+)
+
+var (
+ ErrLockNotAcquired = errors.New("lock not acquired")
+ ErrLockNotHeld = errors.New("lock not held by this instance")
+)
+
+type DistributedLock struct {
+ db *gorm.DB
+ lockName string
+ holderID string
+ stopRenew chan struct{}
+ once sync.Once
+}
+
+func NewDistributedLock(db *gorm.DB, lockName, holderID string) *DistributedLock {
+ return &DistributedLock{
+ db: db,
+ lockName: lockName,
+ holderID: holderID,
+ stopRenew: make(chan struct{}),
+ once: sync.Once{},
+ }
+}
+
+// TryLock acquisition method
+func (dl *DistributedLock) TryLock(ctx context.Context, ttl time.Duration) error {
+ expiresAt := time.Now().UTC().Add(ttl)
+
+ err := dl.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ err := tx.Exec(`
+ CREATE TABLE IF NOT EXISTS distributed_locks (
+ lock_name TEXT PRIMARY KEY,
+ holder_id TEXT NOT NULL,
+ expires_at TIMESTAMPTZ NOT NULL,
+ version INT NOT NULL DEFAULT 1
+ )
+ `).Error
+ if err != nil {
+ return fmt.Errorf("failed to create table: %w", err)
+ }
+
+ // try to get the current lock state first
+ var currentLock struct {
+ HolderID string
+ ExpiresAt time.Time
+ }
+
+ err = tx.Raw(`
+ SELECT holder_id, expires_at
+ FROM distributed_locks
+ WHERE lock_name = ? FOR UPDATE
+ `, dl.lockName).Scan(¤tLock).Error
+
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+ return err
+ }
+
+ // determines whether the lock can be obtained
+ if currentLock.HolderID != "" &&
+ currentLock.ExpiresAt.After(time.Now().UTC()) &&
+ currentLock.HolderID != dl.holderID {
+ return ErrLockNotAcquired
+ }
+
+ // use upsert atomic operations
+ result := tx.Exec(`
+ INSERT INTO distributed_locks (lock_name, holder_id, expires_at, version)
+ VALUES (?, ?, ?, 1)
+ ON CONFLICT (lock_name) DO UPDATE
+ SET
+ holder_id = excluded.holder_id,
+ expires_at = excluded.expires_at,
+ version = distributed_locks.version + 1
+ WHERE distributed_locks.expires_at <= now() OR distributed_locks.holder_id = excluded.holder_id
+ `, dl.lockName, dl.holderID, expiresAt)
+
+ if result.Error != nil {
+ return result.Error
+ }
+
+ if result.RowsAffected == 0 {
+ return ErrLockNotAcquired
+ }
+
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ go dl.renewLock(ttl)
+ return nil
+}
+
+func (dl *DistributedLock) renewLock(ttl time.Duration) {
+ ticker := time.NewTicker(ttl / 2)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ticker.C:
+ expiresAt := time.Now().UTC().Add(ttl)
+
+ err := dl.db.Transaction(func(tx *gorm.DB) error {
+ result := tx.Exec(`
+ UPDATE distributed_locks
+ SET expires_at = ?, version = version + 1
+ WHERE lock_name = ? AND holder_id = ?
+ `, expiresAt, dl.lockName, dl.holderID)
+
+ if result.Error != nil {
+ return result.Error
+ }
+
+ if result.RowsAffected == 0 {
+ return ErrLockNotHeld
+ }
+
+ return nil
+ })
+ if err != nil {
+ // Failed to renew the lock. The lock may have been acquired by another instance
+ close(dl.stopRenew)
+ return
+ }
+
+ case <-dl.stopRenew:
+ return
+ }
+ }
+}
+
+func (dl *DistributedLock) Unlock() error {
+ dl.once.Do(func() {
+ close(dl.stopRenew)
+ })
+
+ return dl.db.Transaction(func(tx *gorm.DB) error {
+ result := tx.Exec(`
+ DELETE FROM distributed_locks
+ WHERE lock_name = ? AND holder_id = ?
+ `, dl.lockName, dl.holderID)
+
+ if result.Error != nil {
+ return result.Error
+ }
+ //
+ // if result.RowsAffected == 0 {
+ // return ErrLockNotHeld
+ //}
+
+ return nil
+ })
+}
+
+func (dl *DistributedLock) IsHeld(ctx context.Context) (bool, error) {
+ var count int64
+ err := dl.db.WithContext(ctx).Model(&struct {
+ LockName string `gorm:"column:lock_name"`
+ }{}).
+ Table("distributed_locks").
+ Where("lock_name = ? AND holder_id = ? AND expires_at > now()", dl.lockName, dl.holderID).
+ Count(&count).Error
+ if err != nil {
+ return false, err
+ }
+
+ return count > 0, nil
+}
diff --git a/controllers/pkg/utils/lock/descributed_lock_test.go b/controllers/pkg/utils/lock/descributed_lock_test.go
new file mode 100644
index 000000000000..2a0c2f07672e
--- /dev/null
+++ b/controllers/pkg/utils/lock/descributed_lock_test.go
@@ -0,0 +1,330 @@
+package dlock
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "gorm.io/driver/postgres"
+ "gorm.io/gorm"
+)
+
+// setupTestDB 创建测试数据库连接
+func setupTestDB(t *testing.T) *gorm.DB {
+ t.Helper()
+ // TODO need to set up a real test database
+ dsn := os.Getenv("TEST_DB_URI")
+ if dsn == "" {
+ t.Skip("requires TEST_DB_URI")
+ }
+ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
+ require.NoError(t, err)
+
+ // 确保表存在
+ err = db.Exec(`
+ CREATE TABLE IF NOT EXISTS distributed_locks (
+ lock_name TEXT PRIMARY KEY,
+ holder_id TEXT NOT NULL,
+ expires_at TIMESTAMPTZ NOT NULL,
+ version INT NOT NULL DEFAULT 1
+ )
+ `).Error
+ require.NoError(t, err)
+
+ // 清空测试数据
+ err = db.Exec("DELETE FROM distributed_locks").Error
+ require.NoError(t, err)
+
+ return db
+}
+
+func checkAssert(t *testing.T, ok bool) {
+ t.Helper()
+ if !ok {
+ t.Fatalf(
+ "[%s] Test failed: %v",
+ time.Now().UTC().Format(time.RFC3339),
+ errors.New("test failed"),
+ )
+ }
+}
+
+const (
+ instanceID = "instance-1"
+)
+
+func TestLockAcquireAndRelease(t *testing.T) {
+ db := setupTestDB(t)
+ lockName := "test-lock"
+ holderID := instanceID
+
+ lock := NewDistributedLock(db, lockName, holderID)
+
+ // 测试获取锁
+ err := lock.TryLock(context.Background(), 10*time.Second)
+ checkAssert(t, assert.NoError(t, err))
+
+ // 验证锁确实被持有
+ held, err := lock.IsHeld(context.Background())
+ checkAssert(t, assert.NoError(t, err))
+ checkAssert(t, assert.True(t, held))
+
+ // 测试释放锁
+ err = lock.Unlock()
+ checkAssert(t, assert.NoError(t, err))
+
+ // 验证锁已释放
+ held, err = lock.IsHeld(context.Background())
+ checkAssert(t, assert.NoError(t, err))
+ checkAssert(t, assert.False(t, held))
+}
+
+func TestLockMutualExclusion(t *testing.T) {
+ db := setupTestDB(t)
+ lockName := "mutex-lock"
+
+ // 第一个实例获取锁
+ lock1 := NewDistributedLock(db, lockName, instanceID)
+ err := lock1.TryLock(context.Background(), 10*time.Second)
+ checkAssert(t, assert.NoError(t, err))
+
+ // 第二个实例尝试获取相同的锁
+ lock2 := NewDistributedLock(db, lockName, "instance-2")
+ err = lock2.TryLock(context.Background(), 10*time.Second)
+ checkAssert(t, assert.Error(t, err))
+ checkAssert(t, assert.True(t, errors.Is(err, ErrLockNotAcquired)))
+
+ // 第一个实例释放锁
+ err = lock1.Unlock()
+ checkAssert(t, assert.NoError(t, err))
+
+ // 现在第二个实例应该能获取锁
+ err = lock2.TryLock(context.Background(), 10*time.Second)
+ checkAssert(t, assert.NoError(t, err))
+
+ // 清理
+ err = lock2.Unlock()
+ checkAssert(t, assert.NoError(t, err))
+}
+
+func TestConcurrentLockAcquisition(t *testing.T) {
+ db := setupTestDB(t)
+ lockName := "concurrent-lock"
+ numClients := 10
+ var wg sync.WaitGroup
+ successCh := make(chan bool, numClients)
+
+ barrier := make(chan struct{}) // 添加并发屏障
+
+ for i := range numClients {
+ wg.Add(1)
+ go func(instanceID int) {
+ defer wg.Done()
+ holderID := fmt.Sprintf("instance-%d", instanceID)
+ lock := NewDistributedLock(db, lockName, holderID)
+
+ <-barrier // 等待所有goroutine就绪
+
+ err := lock.TryLock(context.Background(), 5*time.Second)
+ if err == nil {
+ successCh <- true
+ fmt.Printf("Instance %d acquired the lock\n", instanceID)
+ time.Sleep(100 * time.Millisecond) // 模拟工作
+ err = lock.Unlock()
+ checkAssert(t, assert.NoError(t, err))
+ } else {
+ fmt.Printf("Instance %d failed to acquire the lock: %v\n", instanceID, err)
+ checkAssert(t, assert.True(t, errors.Is(err, ErrLockNotAcquired)))
+ }
+ }(i)
+ }
+
+ close(barrier) // 同时释放所有goroutine
+ wg.Wait()
+ close(successCh)
+
+ // 验证只有一个成功获取锁
+ successCount := 0
+ for range successCh {
+ successCount++
+ }
+ checkAssert(t, assert.Equal(t, 1, successCount))
+}
+
+func TestLockRenewal(t *testing.T) {
+ db := setupTestDB(t)
+ lockName := "renewal-lock"
+ holderID := instanceID
+
+ lock := NewDistributedLock(db, lockName, holderID)
+
+ // 获取锁,TTL很短
+ err := lock.TryLock(context.Background(), 1*time.Second)
+ checkAssert(t, assert.NoError(t, err))
+
+ // 等待超过初始TTL,但续约应该保持锁
+ time.Sleep(2 * time.Second)
+
+ // 验证锁仍然被持有
+ held, err := lock.IsHeld(context.Background())
+ checkAssert(t, assert.NoError(t, err))
+ checkAssert(t, assert.True(t, held))
+
+ // 停止续约
+ err = lock.Unlock()
+ checkAssert(t, assert.NoError(t, err))
+
+ // 验证锁已释放
+ held, err = lock.IsHeld(context.Background())
+ checkAssert(t, assert.NoError(t, err))
+ checkAssert(t, assert.False(t, held))
+}
+
+func TestLockExpiration(t *testing.T) {
+ db := setupTestDB(t)
+ lockName := "expiring-lock"
+
+ // 第一个实例获取锁,TTL很短
+ lock1 := NewDistributedLock(db, lockName, instanceID)
+ err := lock1.TryLock(context.Background(), 10*time.Second)
+ checkAssert(t, assert.NoError(t, err))
+
+ fmt.Printf("%s Instance 1 acquired the lock\n", time.Now().UTC())
+ // 等待锁过期
+ time.Sleep(11 * time.Second)
+
+ fmt.Printf("%s Instance 1 lock expired\n", time.Now().UTC())
+
+ // 第二个实例仍然不能获取锁,因为第一个实例持有锁后台不断续约
+ lock2 := NewDistributedLock(db, lockName, "instance-2")
+ err = lock2.TryLock(context.Background(), 10*time.Second)
+ checkAssert(t, assert.Error(t, err))
+
+ // 清理
+ err = lock2.Unlock()
+ checkAssert(t, assert.NoError(t, err))
+}
+
+func TestDoubleUnlock(t *testing.T) {
+ db := setupTestDB(t)
+ lockName := "double-unlock-lock"
+ holderID := instanceID
+
+ lock := NewDistributedLock(db, lockName, holderID)
+
+ // 获取锁
+ err := lock.TryLock(context.Background(), 10*time.Second)
+ checkAssert(t, assert.NoError(t, err))
+
+ // 第一次释放
+ err = lock.Unlock()
+ checkAssert(t, assert.NoError(t, err))
+
+ // 第二次释放也成功
+ err = lock.Unlock()
+ checkAssert(t, assert.NoError(t, err))
+}
+
+func TestContextCancellation(t *testing.T) {
+ db := setupTestDB(t)
+ lockName := "ctx-cancel-lock"
+ holderID := instanceID
+
+ // 先让另一个实例持有锁
+ otherLock := NewDistributedLock(db, lockName, "instance-2")
+ err := otherLock.TryLock(context.Background(), 10*time.Second)
+ checkAssert(t, assert.NoError(t, err))
+
+ // 创建可取消的上下文
+ ctx, cancel := context.WithCancel(context.Background())
+
+ // 在新的goroutine中尝试获取锁
+ var wg sync.WaitGroup
+ wg.Add(1)
+ var acquireErr error
+
+ go func() {
+ defer wg.Done()
+ lock := NewDistributedLock(db, lockName, holderID)
+ acquireErr = lock.TryLock(ctx, 10*time.Second)
+ }()
+
+ // 等待一会儿然后取消上下文
+ time.Sleep(1 * time.Millisecond)
+ cancel()
+
+ wg.Wait()
+
+ // 验证获取被取消
+ checkAssert(t, assert.Error(t, acquireErr))
+ checkAssert(t, assert.True(t, errors.Is(acquireErr, context.Canceled)))
+
+ // 清理
+ err = otherLock.Unlock()
+ checkAssert(t, assert.NoError(t, err))
+}
+
+func TestLongRunningTaskWithLock(t *testing.T) {
+ db := setupTestDB(t)
+ lockName := "long-task-lock"
+ holderID := instanceID
+
+ lock := NewDistributedLock(db, lockName, holderID)
+
+ // 获取锁,TTL较短以测试续约
+ err := lock.TryLock(context.Background(), 2*time.Second)
+ checkAssert(t, assert.NoError(t, err))
+
+ // 模拟长时间运行任务
+ done := make(chan struct{})
+ go func() {
+ for range 10 {
+ held, err := lock.IsHeld(context.Background())
+ checkAssert(t, assert.NoError(t, err))
+ if !held {
+ t.Log("锁丢失,任务中止")
+ return
+ }
+ time.Sleep(1 * time.Second)
+ }
+ close(done)
+ }()
+
+ // 等待任务完成或超时
+ select {
+ case <-done:
+ t.Log("任务成功完成")
+ case <-time.After(11 * time.Second):
+ t.Fatal("任务超时")
+ }
+
+ // 清理
+ err = lock.Unlock()
+ checkAssert(t, assert.NoError(t, err))
+}
+
+func TestAll(t *testing.T) {
+ // setupTestDB(t)
+
+ TestLockAcquireAndRelease(t)
+
+ TestLockMutualExclusion(t)
+
+ TestConcurrentLockAcquisition(t)
+
+ TestLockRenewal(t)
+
+ TestLockExpiration(t)
+
+ TestDoubleUnlock(t)
+
+ TestContextCancellation(t)
+
+ TestLongRunningTaskWithLock(t)
+}
diff --git a/controllers/pkg/utils/logger/feishu.go b/controllers/pkg/utils/logger/feishu.go
new file mode 100644
index 000000000000..16f9e9797af0
--- /dev/null
+++ b/controllers/pkg/utils/logger/feishu.go
@@ -0,0 +1,273 @@
+package logger
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/sirupsen/logrus"
+)
+
+/*
+func main() {
+ // Example usage:
+ // Replace with your Feishu Webhook URL, e.g., "https://open.feishu.cn/open-apis/bot/v2/hook/xxxx"
+ webhookURL := "" // Set your webhook here
+ // Set minimum level for Feishu alerts (e.g., WARN means WARN, ERROR, FATAL will send alerts)
+ // Set component name for the system component
+ logger := NewLogger(nil, webhookURL, WARN, "PaymentService")
+
+ logger.Debugln("This is a debug message (stdout only with default WARN level).")
+ logger.Infoln("This is an info message (stdout only with default WARN level).")
+ logger.Warnln("This is a warning (orange card in Feishu).")
+ logger.Errorln("This is an error (red card in Feishu).")
+ logger.Errorf("Error with format: %s and %d", "foo", 123)
+ // logger.Fatalln("This is a fatal error (red card in Feishu, then exit).")
+}
+*/
+
+// Level defines the log levels.
+type Level int
+
+const (
+ DEBUG Level = iota
+ INFO
+ WARN
+ ERROR
+ FATAL
+)
+
+// String converts Level to string representation.
+func (l Level) String() string {
+ switch l {
+ case DEBUG:
+ return "DEBUG"
+ case INFO:
+ return "INFO"
+ case WARN:
+ return "WARN"
+ case ERROR:
+ return "ERROR"
+ case FATAL:
+ return "FATAL"
+ default:
+ return "UNKNOWN"
+ }
+}
+
+// LarkCardMessage represents the structure for a Feishu Interactive Card message.
+type LarkCardMessage struct {
+ MsgType string `json:"msg_type"`
+ Card struct {
+ Header struct {
+ Template string `json:"template"`
+ Title struct {
+ Content string `json:"content"`
+ Tag string `json:"tag"`
+ } `json:"title"`
+ } `json:"header"`
+ Elements []struct {
+ Tag string `json:"tag"`
+ Content string `json:"content"`
+ } `json:"elements"`
+ } `json:"card"`
+}
+
+// Logger is a custom logger that outputs to stdout and sends styled alerts to Feishu based on level.
+type Logger struct {
+ stdLogger *logrus.Logger
+ webhook string
+ minLevel Level
+ component string // System component name
+}
+
+// NewLogger creates a new Logger instance.
+// - stdOutput: the output for standard logs (default os.Stdout if nil)
+// - webhook: the Feishu Webhook URL for alerts (empty string to disable)
+// - minLevel: the minimum log level for sending Feishu alerts
+// - component: the system component name to include in Feishu alerts
+func NewFeishuLogger(_ *os.File, webhook string, minLevel Level, component string) *Logger {
+ // if stdOutput == nil {
+ // stdOutput = os.Stdout
+ //}
+ if component == "" {
+ component = "Unknown"
+ }
+ logger := &Logger{
+ stdLogger: logrus.New(),
+ webhook: webhook,
+ minLevel: minLevel,
+ component: component,
+ }
+ logger.Infof(
+ "starting Feishu logger for component: %s with minLevel: %s",
+ component,
+ minLevel.String(),
+ )
+ return logger
+}
+
+// sendAlert sends a styled card message to Feishu Webhook based on log level.
+func (l *Logger) sendAlert(level Level, message string) error {
+ if l.webhook == "" || level < l.minLevel {
+ return nil // No webhook configured or level below threshold
+ }
+
+ // Determine card style based on log level
+ var templateColor string
+ switch level {
+ case DEBUG:
+ templateColor = "blue"
+ case INFO:
+ templateColor = "green"
+ case WARN:
+ templateColor = "orange"
+ case ERROR, FATAL:
+ templateColor = "red"
+ default:
+ templateColor = "blue"
+ }
+
+ // Construct Feishu Interactive Card
+ msg := LarkCardMessage{
+ MsgType: "interactive",
+ }
+ msg.Card.Header.Template = templateColor
+ msg.Card.Header.Title.Content = fmt.Sprintf("[%s] %s Alert", l.component, level.String())
+ msg.Card.Header.Title.Tag = "plain_text"
+ msg.Card.Elements = []struct {
+ Tag string `json:"tag"`
+ Content string `json:"content"`
+ }{
+ {
+ Tag: "markdown",
+ Content: fmt.Sprintf(
+ "**Message**: %s\n**Time**: %s",
+ message,
+ time.Now().Format(time.RFC3339),
+ ),
+ },
+ }
+
+ jsonData, err := json.Marshal(msg)
+ if err != nil {
+ return err
+ }
+
+ resp, err := http.Post(l.webhook, "application/json", bytes.NewBuffer(jsonData))
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("feishu webhook returned status: %d", resp.StatusCode)
+ }
+
+ return nil
+}
+
+// Debugf logs a debug-level message to stdout.
+func (l *Logger) Debugf(format string, v ...any) {
+ msg := fmt.Sprintf(format, v...)
+ l.stdLogger.Printf("[DEBUG] %s", msg)
+ if err := l.sendAlert(DEBUG, msg); err != nil {
+ l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err)
+ }
+}
+
+// Printf logs an info-level message to stdout (alias for Infof).
+func (l *Logger) Printf(format string, v ...any) {
+ l.Infof(format, v...)
+}
+
+// Infof logs an info-level message to stdout.
+func (l *Logger) Infof(format string, v ...any) {
+ msg := fmt.Sprintf(format, v...)
+ l.stdLogger.Printf("[INFO] %s", msg)
+ if err := l.sendAlert(INFO, msg); err != nil {
+ l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err)
+ }
+}
+
+// Warnf logs a warn-level message to stdout and sends a Feishu alert if level >= minLevel.
+func (l *Logger) Warnf(format string, v ...any) {
+ msg := fmt.Sprintf(format, v...)
+ l.stdLogger.Printf("[WARN] %s", msg)
+ if err := l.sendAlert(WARN, msg); err != nil {
+ l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err)
+ }
+}
+
+// Errorf logs an error-level message to stdout and sends a Feishu alert if level >= minLevel.
+func (l *Logger) Errorf(format string, v ...any) {
+ msg := fmt.Sprintf(format, v...)
+ l.stdLogger.Printf("[ERROR] %s", msg)
+ if err := l.sendAlert(ERROR, msg); err != nil {
+ l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err)
+ }
+}
+
+// Fatalf logs a fatal error to stdout, sends an alert if level >= minLevel, and exits.
+func (l *Logger) Fatalf(format string, v ...any) {
+ msg := fmt.Sprintf(format, v...)
+ l.stdLogger.Printf("[FATAL] %s", msg)
+ if err := l.sendAlert(FATAL, msg); err != nil {
+ l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err)
+ }
+ os.Exit(1)
+}
+
+// Debugln logs a debug-level message to stdout.
+func (l *Logger) Debugln(v ...any) {
+ msg := fmt.Sprintln(v...)
+ l.stdLogger.Printf("[DEBUG] %s", msg)
+ if err := l.sendAlert(DEBUG, msg); err != nil {
+ l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err)
+ }
+}
+
+// Println logs an info-level message to stdout (alias for Infoln).
+func (l *Logger) Println(v ...any) {
+ l.Infoln(v...)
+}
+
+// Infoln logs an info-level message to stdout.
+func (l *Logger) Infoln(v ...any) {
+ msg := fmt.Sprintln(v...)
+ l.stdLogger.Printf("[INFO] %s", msg)
+ if err := l.sendAlert(INFO, msg); err != nil {
+ l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err)
+ }
+}
+
+// Warnln logs a warn-level message to stdout and sends a Feishu alert if level >= minLevel.
+func (l *Logger) Warnln(v ...any) {
+ msg := fmt.Sprintln(v...)
+ l.stdLogger.Printf("[WARN] %s", msg)
+ if err := l.sendAlert(WARN, msg); err != nil {
+ l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err)
+ }
+}
+
+// Errorln logs an error-level message to stdout and sends a Feishu alert if level >= minLevel.
+func (l *Logger) Errorln(v ...any) {
+ msg := fmt.Sprintln(v...)
+ l.stdLogger.Printf("[ERROR] %s", msg)
+ if err := l.sendAlert(ERROR, msg); err != nil {
+ l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err)
+ }
+}
+
+// Fatalln logs a fatal error to stdout, sends an alert if level >= minLevel, and exits.
+func (l *Logger) Fatalln(v ...any) {
+ msg := fmt.Sprintln(v...)
+ l.stdLogger.Printf("[FATAL] %s", msg)
+ if err := l.sendAlert(FATAL, msg); err != nil {
+ l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err)
+ }
+ os.Exit(1)
+}
diff --git a/controllers/pkg/utils/logger/logger.go b/controllers/pkg/utils/logger/logger.go
new file mode 100644
index 000000000000..b8018578c36d
--- /dev/null
+++ b/controllers/pkg/utils/logger/logger.go
@@ -0,0 +1,180 @@
+// Copyright © 2022 sealos.
+//
+// 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 logger
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "time"
+
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+ "gopkg.in/natefinch/lumberjack.v2"
+)
+
+var defaultLogger *zap.Logger
+
+// init default logger with only console output info above
+func init() {
+ zc := zapcore.NewTee(newConsoleCore(zap.InfoLevel))
+ defaultLogger = zap.New(zc)
+}
+
+// CfgConsoleLogger config for console logs
+// cfg donot support concurrent calls (as any package should init cfg at startup once)
+func CfgConsoleLogger(debugMode, showPath bool) {
+ level, zos := genConfigs(debugMode, showPath)
+
+ zc := zapcore.NewTee(newConsoleCore(level))
+
+ defaultLogger = zap.New(zc, zos...)
+}
+
+// TODO: export more file configs
+// CfgConsoleAndFileLogger config for both console and file logs
+// cfg donot support concurrent calls (as any package should init cfg at startup once)
+func CfgConsoleAndFileLogger(debugMode bool, logDir, name string, showPath bool) {
+ level, zos := genConfigs(debugMode, showPath)
+
+ filename := fmt.Sprintf("%s/%s.log", logDir, name)
+
+ zc := zapcore.NewTee(newConsoleCore(level), newFileCore(filename, level))
+
+ defaultLogger = zap.New(zc, zos...)
+}
+
+func genConfigs(debugMode, showPath bool) (zapcore.LevelEnabler, []zap.Option) {
+ level := zapcore.InfoLevel
+ if debugMode {
+ level = zapcore.DebugLevel
+ }
+
+ zos := []zap.Option{
+ // zap.AddStacktrace(zapcore.WarnLevel),
+ }
+ if showPath {
+ // skip self wrapper
+ zos = append(zos, zap.AddCaller(), zap.AddCallerSkip(2))
+ }
+
+ return level, zos
+}
+
+func newConsoleCore(le zapcore.LevelEnabler) zapcore.Core {
+ consoleLogger := zapcore.Lock(os.Stdout)
+
+ zec := zap.NewProductionEncoderConfig()
+ zec.EncodeLevel = zapcore.LowercaseColorLevelEncoder
+ zec.EncodeTime = zapcore.ISO8601TimeEncoder
+ zec.EncodeTime = shortTimeEncoder
+ // zec.EncodeTime = zapcore.ISO8601TimeEncoder
+ zec.ConsoleSeparator = " "
+
+ consoleEncoder := zapcore.NewConsoleEncoder(zec)
+
+ return zapcore.NewCore(consoleEncoder, consoleLogger, le)
+}
+
+func newFileCore(filename string, le zapcore.LevelEnabler) zapcore.Core {
+ // TODO: export more rotate configs
+ fileLogger := zapcore.AddSync(&lumberjack.Logger{
+ Filename: filename,
+ MaxSize: 10, // megabytes per file
+ })
+
+ zec := zap.NewProductionEncoderConfig()
+ zec.EncodeTime = zapcore.ISO8601TimeEncoder
+
+ fileEncoder := zapcore.NewJSONEncoder(zec)
+ return zapcore.NewCore(fileEncoder, fileLogger, le)
+}
+
+const shortTimeLayout = "2006-01-02T15:04:05"
+
+func shortTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
+ enc.AppendString(t.Format(shortTimeLayout))
+}
+
+// IsDebugMode check DebugLevel enabled
+func IsDebugMode() bool {
+ return defaultLogger.Core().Enabled(zapcore.DebugLevel)
+}
+
+// Fatal logs a message at emergency level and exit.
+func Fatal(f any, v ...any) {
+ defaultLogger.Sugar().Fatalf(formatLog(zapcore.FatalLevel, f, v...))
+}
+
+// Panic logs a message at emergency level and exit.
+func Panic(f any, v ...any) {
+ defaultLogger.Sugar().Panicf(formatLog(zapcore.PanicLevel, f, v...))
+}
+
+// Error logs a message at error level.
+func Error(f any, v ...any) {
+ defaultLogger.Sugar().Errorf(formatLog(zapcore.ErrorLevel, f, v...))
+}
+
+// Warn logs a message at warning level.
+func Warn(f any, v ...any) {
+ defaultLogger.Sugar().Warnf(formatLog(zapcore.WarnLevel, f, v...))
+}
+
+// Info logs a message at info level.
+func Info(f any, v ...any) {
+ defaultLogger.Sugar().Infof(formatLog(zapcore.InfoLevel, f, v...))
+}
+
+// Debug logs a message at debug level.
+func Debug(f any, v ...any) {
+ defaultLogger.Sugar().Debugf(formatLog(zapcore.DebugLevel, f, v...))
+}
+
+func formatLog(l zapcore.Level, f any, v ...any) string {
+ var msg string
+ switch f := f.(type) {
+ case string:
+ msg = f
+ if len(v) == 0 {
+ return appendColor(l, msg)
+ }
+ if !strings.Contains(msg, "%") || strings.Contains(msg, "%%") {
+ // do not contain format char
+ msg += strings.Repeat(" %v", len(v))
+ }
+ default:
+ msg = fmt.Sprint(f)
+ if len(v) == 0 {
+ return appendColor(l, msg)
+ }
+ msg += strings.Repeat(" %v", len(v))
+ }
+ return appendColor(l, fmt.Sprintf(msg, v...))
+}
+
+func appendColor(l zapcore.Level, s string) string {
+ // default all red
+ c := uint8(31)
+ switch l {
+ case zapcore.DebugLevel:
+ c = uint8(35) // Magenta
+ case zapcore.InfoLevel:
+ c = uint8(34) // Blue
+ case zapcore.WarnLevel:
+ c = uint8(33) // Yellow
+ }
+ return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s)
+}
diff --git a/controllers/pkg/utils/logger/logger_test.go b/controllers/pkg/utils/logger/logger_test.go
new file mode 100644
index 000000000000..0c6ac5c4ae4b
--- /dev/null
+++ b/controllers/pkg/utils/logger/logger_test.go
@@ -0,0 +1,68 @@
+// Copyright © 2022 sealos.
+//
+// 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 logger
+
+import (
+ "errors"
+ "os"
+ "os/exec"
+ "testing"
+)
+
+func TestInfoLog(t *testing.T) {
+ CfgConsoleLogger(false, false)
+
+ Info("can see me")
+ Debug("cannot see me")
+
+ logDir := t.TempDir()
+ CfgConsoleAndFileLogger(true, logDir, "log_test.log", true)
+
+ Info("can see me")
+ Debug("cannot see me")
+ Warn("this is warn")
+ Error("this is error: %s", errors.New("this is error"))
+ Error("info %% is dead", errors.New("this is error"), 2)
+ Error(errors.New("this is error"))
+ Error(errors.New("this is error"), "more error")
+
+ if IsDebugMode() == false {
+ t.Error("not in debug mode")
+ }
+}
+
+func TestFatalLog(t *testing.T) {
+ if os.Getenv("LOG_FATAL") == "1" {
+ Fatal("this is fatal")
+ return
+ }
+ cmd := exec.Command(os.Args[0], "-test.run=TestFatalLog") // #nosec G204
+ cmd.Env = append(os.Environ(), "LOG_FATAL=1")
+ err := cmd.Run()
+ e := &exec.ExitError{}
+ if errors.As(err, &e) {
+ return
+ }
+ t.Fatalf("process ran with err %v, want exit status 1", err)
+}
+
+func TestPanicLog(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil {
+ t.Errorf("The code did not panic")
+ }
+ }()
+ Panic("this panics")
+}
diff --git a/controllers/pkg/utils/maps/concurrent_map.go b/controllers/pkg/utils/maps/concurrent_map.go
new file mode 100644
index 000000000000..be9d63cece20
--- /dev/null
+++ b/controllers/pkg/utils/maps/concurrent_map.go
@@ -0,0 +1,125 @@
+// Copyright © 2025 sealos.
+//
+// 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 maps
+
+import "sync"
+
+type ConcurrentMap struct {
+ mu sync.RWMutex
+ m map[string]any
+}
+
+func NewConcurrentMap() *ConcurrentMap {
+ return &ConcurrentMap{
+ m: make(map[string]any),
+ }
+}
+
+func (cm *ConcurrentMap) Set(key string, value any) {
+ cm.mu.Lock()
+ defer cm.mu.Unlock()
+ cm.m[key] = value
+}
+
+func (cm *ConcurrentMap) Get(key string) (any, bool) {
+ cm.mu.RLock()
+ defer cm.mu.RUnlock()
+ val, ok := cm.m[key]
+ return val, ok
+}
+
+func (cm *ConcurrentMap) GetAllKey() []string {
+ cm.mu.RLock()
+ defer cm.mu.RUnlock()
+
+ keys := make([]string, 0, len(cm.m))
+ for k := range cm.m {
+ keys = append(keys, k)
+ }
+ return keys
+}
+
+func (cm *ConcurrentMap) Delete(key string) {
+ cm.mu.Lock()
+ defer cm.mu.Unlock()
+ delete(cm.m, key)
+}
+
+func (cm *ConcurrentMap) DeleteAll() {
+ cm.mu.Lock()
+ defer cm.mu.Unlock()
+ cm.m = make(map[string]any)
+}
+
+func (cm *ConcurrentMap) Len() int {
+ cm.mu.RLock()
+ defer cm.mu.RUnlock()
+ return len(cm.m)
+}
+
+type ConcurrentNullValueMap struct {
+ mu sync.RWMutex
+ m map[string]struct{}
+}
+
+func NewConcurrentNullValueMap() *ConcurrentNullValueMap {
+ return &ConcurrentNullValueMap{
+ m: make(map[string]struct{}),
+ }
+}
+
+func (cm *ConcurrentNullValueMap) Set(keys ...string) {
+ cm.mu.Lock()
+ defer cm.mu.Unlock()
+ for _, key := range keys {
+ cm.m[key] = struct{}{}
+ }
+}
+
+func (cm *ConcurrentNullValueMap) Get(key string) (struct{}, bool) {
+ cm.mu.RLock()
+ defer cm.mu.RUnlock()
+ val, ok := cm.m[key]
+ return val, ok
+}
+
+func (cm *ConcurrentNullValueMap) GetAllKey() []string {
+ cm.mu.RLock()
+ defer cm.mu.RUnlock()
+
+ keys := make([]string, 0, len(cm.m))
+ for k := range cm.m {
+ keys = append(keys, k)
+ }
+ return keys
+}
+
+func (cm *ConcurrentNullValueMap) Delete(key string) {
+ cm.mu.Lock()
+ defer cm.mu.Unlock()
+ delete(cm.m, key)
+}
+
+func (cm *ConcurrentNullValueMap) DeleteAll() {
+ cm.mu.Lock()
+ defer cm.mu.Unlock()
+ cm.m = make(map[string]struct{})
+}
+
+func (cm *ConcurrentNullValueMap) Len() int {
+ cm.mu.RLock()
+ defer cm.mu.RUnlock()
+ return len(cm.m)
+}
diff --git a/controllers/pkg/utils/maps/ttl_map.go b/controllers/pkg/utils/maps/ttl_map.go
new file mode 100644
index 000000000000..45a8a8225918
--- /dev/null
+++ b/controllers/pkg/utils/maps/ttl_map.go
@@ -0,0 +1,61 @@
+package maps
+
+import (
+ "sync"
+ "time"
+)
+
+type item[T any] struct {
+ value T
+ expiresAt int64
+}
+
+type TTLMap[T any] struct {
+ m map[string]*item[T]
+ l sync.RWMutex
+ ttl time.Duration
+}
+
+func New[T any](maxTTL int) (m *TTLMap[T]) {
+ m = &TTLMap[T]{
+ m: make(map[string]*item[T]),
+ ttl: time.Duration(maxTTL) * time.Second,
+ }
+ go func() {
+ for now := range time.Tick(2 * time.Second) {
+ m.l.Lock()
+ for k, v := range m.m {
+ if now.UnixNano() >= v.expiresAt {
+ delete(m.m, k)
+ }
+ }
+ m.l.Unlock()
+ }
+ }()
+ return m
+}
+
+func (m *TTLMap[T]) Len() int {
+ m.l.RLock()
+ defer m.l.RUnlock()
+ return len(m.m)
+}
+
+func (m *TTLMap[T]) Put(k string, v T) {
+ m.l.Lock()
+ defer m.l.Unlock()
+ m.m[k] = &item[T]{
+ value: v,
+ expiresAt: time.Now().Add(m.ttl).UnixNano(),
+ }
+}
+
+func (m *TTLMap[T]) Get(k string) (v T, ok bool) {
+ m.l.RLock()
+ defer m.l.RUnlock()
+ it, ok := m.m[k]
+ if !ok || time.Now().UnixNano() >= it.expiresAt {
+ return v, false
+ }
+ return it.value, true
+}
diff --git a/controllers/pkg/utils/maps/ttl_map_test.go b/controllers/pkg/utils/maps/ttl_map_test.go
new file mode 100644
index 000000000000..a6b12674346f
--- /dev/null
+++ b/controllers/pkg/utils/maps/ttl_map_test.go
@@ -0,0 +1,181 @@
+package maps
+
+import (
+ "sync"
+ "testing"
+ "time"
+)
+
+func TestNew(t *testing.T) {
+ // Test that New creates a non-nil TTLMap with initialized map
+ m := New[string](60)
+ if m == nil {
+ t.Fatal("New() returned nil")
+ }
+ if m.m == nil {
+ t.Fatal("New() did not initialize map")
+ }
+ if len(m.m) != 0 {
+ t.Errorf("New() initialized map with non-zero length: %d", len(m.m))
+ }
+}
+
+func TestPutAndGet(t *testing.T) {
+ m := New[string](60)
+
+ // Test putting and getting a value
+ m.Put("key1", "value1")
+ if v, _ := m.Get("key1"); v != "value1" {
+ t.Errorf("Get(key1) = %v; want value1", v)
+ }
+
+ // Test getting non-existent key
+ if v, _ := m.Get("key2"); v != "" {
+ t.Errorf("Get(key2) = %v; want nil", v)
+ }
+
+ // Test updating existing key
+ m.Put("key1", "value2")
+ if v, _ := m.Get("key1"); v != "value2" {
+ t.Errorf("Get(key1) = %v; want value2", v)
+ }
+}
+
+func TestLen(t *testing.T) {
+ m := New[string](60)
+
+ // Test initial length
+ if l := m.Len(); l != 0 {
+ t.Errorf("Len() = %d; want 0", l)
+ }
+
+ // Test length after adding items
+ m.Put("key1", "value1")
+ m.Put("key2", "value2")
+ if l := m.Len(); l != 2 {
+ t.Errorf("Len() = %d; want 2", l)
+ }
+}
+
+func TestTTLExpiration(t *testing.T) {
+ m := New[string](1) // 1 second TTL, large initial capacity
+ var wg sync.WaitGroup
+ const numOps = 500 // Number of concurrent operations
+
+ // Concurrent writes
+ for i := range numOps {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ key := string(rune('a'+(i%26))) + string(rune(i))
+ m.Put(key, "value"+string(rune(i)))
+ }(i)
+ }
+
+ // Wait for all writes to complete
+ wg.Wait()
+
+ // Verify some values are present immediately
+ for i := range 10 { // Check a subset to avoid long test duration
+ key := string(rune('a'+(i%26))) + string(rune(i))
+ if v, _ := m.Get(key); v == "" {
+ t.Errorf("Get(%s) = nil; want non-nil value", key)
+ }
+ }
+
+ // Concurrent reads during TTL period
+ for i := range numOps {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ key := string(rune('a'+(i%26))) + string(rune(i))
+ m.Get(key)
+ }(i)
+ }
+
+ // Wait less than TTL so entries remain available.
+ time.Sleep(500 * time.Millisecond)
+ wg.Wait()
+
+ // Verify entries remain before their fixed expiration time.
+ if l := m.Len(); l < numOps/2 {
+ t.Errorf("Len() = %d; want at least %d after partial expiration", l, numOps/2)
+ }
+
+ // Wait for full expiration (1 second TTL + buffer)
+ time.Sleep(1500 * time.Millisecond)
+
+ // Concurrent reads after expiration
+ for i := range numOps {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ key := string(rune('a'+(i%26))) + string(rune(i))
+ if v, _ := m.Get(key); v != "" {
+ t.Errorf("Get(%s) = %v; want nil after expiration", key, v)
+ }
+ }(i)
+ }
+
+ wg.Wait()
+
+ // Verify length is 0 after expiration
+ if l := m.Len(); l != 0 {
+ t.Errorf("Len() = %d; want 0 after expiration", l)
+ }
+}
+
+func TestConcurrentAccess(t *testing.T) {
+ m := New[int](60)
+ var wg sync.WaitGroup
+
+ // Number of concurrent operations
+ const numOps = 100
+
+ // Concurrent writes
+ for i := range numOps {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ key := string(rune('a' + i%26))
+ m.Put(key, i)
+ }(i)
+ }
+
+ // Concurrent reads
+ for i := range numOps {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ key := string(rune('a' + i%26))
+ m.Get(key)
+ }(i)
+ }
+
+ wg.Wait()
+
+ // Verify some values
+ for i := range 26 {
+ key := string(rune('a' + i))
+ if v, _ := m.Get(key); v == 0 {
+ t.Errorf("Get(%s) = nil; want non-nil value", key)
+ }
+ }
+}
+
+func TestGetDoesNotExtendTTL(t *testing.T) {
+ m := New[string](1)
+ m.Put("key1", "value1")
+
+ for i := range 2 {
+ time.Sleep(400 * time.Millisecond)
+ if value, ok := m.Get("key1"); !ok || value != "value1" {
+ t.Fatalf("Get(key1) = (%q, %v), want (value1, true) at iteration %d", value, ok, i)
+ }
+ }
+
+ time.Sleep(400 * time.Millisecond)
+ if value, ok := m.Get("key1"); ok {
+ t.Fatalf("Get(key1) = (%q, true), want expired entry", value)
+ }
+}
diff --git a/controllers/pkg/utils/rate/rate_limiter.go b/controllers/pkg/utils/rate/rate_limiter.go
new file mode 100644
index 000000000000..207cf8e6437f
--- /dev/null
+++ b/controllers/pkg/utils/rate/rate_limiter.go
@@ -0,0 +1,69 @@
+// Copyright © 2023 sealos.
+//
+// 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 rate
+
+import (
+ "flag"
+ "time"
+)
+
+const (
+ defaultMinRetryDelay = 5 * time.Millisecond
+ defaultMaxRetryDelay = 1000 * time.Second
+ defaultQPS = float64(10.0)
+ defaultBurst = 100
+ flagMinRetryDelay = "min-retry-delay"
+ flagMaxRetryDelay = "max-retry-delay"
+ flagQPS = "default-qps"
+ flagBurst = "default-burst"
+)
+
+// LimiterOptions used on reconcilers.
+type LimiterOptions struct {
+ MinRetryDelay time.Duration
+ QPS float64
+ Burst int
+ MaxRetryDelay time.Duration
+}
+
+func (o *LimiterOptions) BindFlags(fs *flag.FlagSet) {
+ fs.DurationVar(
+ &o.MinRetryDelay,
+ flagMinRetryDelay,
+ defaultMinRetryDelay,
+ "The minimum amount of time for which an object being reconciled will have to wait before a retry.",
+ )
+ fs.DurationVar(
+ &o.MaxRetryDelay,
+ flagMaxRetryDelay,
+ defaultMaxRetryDelay,
+ "The maximum amount of time for which an object being reconciled will have to wait before a retry.",
+ )
+ fs.Float64Var(&o.QPS, flagQPS, defaultQPS, "The maximum number of batches per second to allow.")
+ fs.IntVar(
+ &o.Burst,
+ flagBurst,
+ defaultBurst,
+ "The maximum number of batches to allow in a short period of time.",
+ )
+}
+
+// func GetRateLimiter(opts LimiterOptions) ratelimiter.RateLimiter {
+// return workqueue.NewMaxOfRateLimiter(
+// workqueue.NewItemExponentialFailureRateLimiter(opts.MinRetryDelay, opts.MaxRetryDelay),
+// // 10 qps, 100 bucket size. This is only for retry speed and its only the overall factor (not per item)
+// &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(opts.QPS), opts.Burst)},
+// )
+//}
diff --git a/controllers/pkg/utils/retry/retry.go b/controllers/pkg/utils/retry/retry.go
new file mode 100644
index 000000000000..b225f8d96473
--- /dev/null
+++ b/controllers/pkg/utils/retry/retry.go
@@ -0,0 +1,33 @@
+// Copyright © 2021 sealos.
+//
+// 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 retry
+
+import (
+ "fmt"
+ "time"
+)
+
+func Retry(tryTimes int, trySleepTime time.Duration, action func() error) error {
+ var err error
+ for i := range tryTimes {
+ err = action()
+ if err == nil {
+ return nil
+ }
+
+ time.Sleep(trySleepTime * time.Duration(2*i+1))
+ }
+ return fmt.Errorf("retry action timeout: %w", err)
+}
diff --git a/controllers/pkg/utils/retry/retry_create_or_update.go b/controllers/pkg/utils/retry/retry_create_or_update.go
new file mode 100644
index 000000000000..910975bfbe61
--- /dev/null
+++ b/controllers/pkg/utils/retry/retry_create_or_update.go
@@ -0,0 +1,107 @@
+// Copyright © 2023 sealos.
+//
+// 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 retry
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ "k8s.io/apimachinery/pkg/api/equality"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// mutate wraps a MutateFn and applies validation to its result.
+func mutate(f MutateFn, key client.ObjectKey, obj client.Object) error {
+ if err := f(); err != nil {
+ return err
+ }
+ if newKey := client.ObjectKeyFromObject(obj); key != newKey {
+ return errors.New("mutateFn cannot mutate object name and/or object namespace")
+ }
+ return nil
+}
+
+type MutateFn func() error
+
+func CreateOrUpdate(
+ ctx context.Context,
+ c client.Client,
+ obj client.Object,
+ f MutateFn,
+ tryTimes int,
+ trySleepTime time.Duration,
+) (OperationResult, error) {
+ var result OperationResult
+ err := Retry(tryTimes, trySleepTime, func() error {
+ key := client.ObjectKeyFromObject(obj)
+ if err := c.Get(ctx, key, obj); err != nil {
+ if !apierrors.IsNotFound(err) {
+ result = OperationResultNone
+ return err
+ }
+ if err := mutate(f, key, obj); err != nil {
+ result = OperationResultNone
+ return err
+ }
+ if err := c.Create(ctx, obj); err != nil {
+ result = OperationResultNone
+ return err
+ }
+ result = OperationResultCreated
+ return nil
+ }
+
+ existing := obj.DeepCopyObject()
+ if err := mutate(f, key, obj); err != nil {
+ result = OperationResultNone
+ return err
+ }
+
+ if equality.Semantic.DeepEqual(existing, obj) {
+ result = OperationResultNone
+ return nil
+ }
+
+ if err := c.Update(ctx, obj); err != nil {
+ result = OperationResultNone
+ return err
+ }
+ result = OperationResultUpdated
+
+ return nil
+ })
+ if err != nil {
+ return OperationResultNone, err
+ }
+ return result, nil
+}
+
+// OperationResult is the action result of a CreateOrUpdate call.
+type OperationResult string
+
+const ( // They should complete the sentence "Deployment default/foo has been ..."
+ // OperationResultNone means that the resource has not been changed.
+ OperationResultNone OperationResult = "unchanged"
+ // OperationResultCreated means that a new resource is created.
+ OperationResultCreated OperationResult = "created"
+ // OperationResultUpdated means that an existing resource is updated.
+ OperationResultUpdated OperationResult = "updated"
+ // OperationResultUpdatedStatus means that an existing resource and its status is updated.
+ OperationResultUpdatedStatus OperationResult = "updatedStatus"
+ // OperationResultUpdatedStatusOnly means that only an existing status is updated.
+ OperationResultUpdatedStatusOnly OperationResult = "updatedStatusOnly"
+)
diff --git a/controllers/resources/.dockerignore b/controllers/resources/.dockerignore
new file mode 100644
index 000000000000..8e6fac709b69
--- /dev/null
+++ b/controllers/resources/.dockerignore
@@ -0,0 +1,3 @@
+# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
+# Ignore build and test binaries.
+testbin/
diff --git a/controllers/resources/.gitignore b/controllers/resources/.gitignore
new file mode 100644
index 000000000000..c0a7a54cac5a
--- /dev/null
+++ b/controllers/resources/.gitignore
@@ -0,0 +1,25 @@
+
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+bin
+testbin/*
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Kubernetes Generated files - skip generated files, except for vendored files
+
+!vendor/**/zz_generated.*
+
+# editor and IDE paraphernalia
+.idea
+*.swp
+*.swo
+*~
diff --git a/controllers/resources/Dockerfile b/controllers/resources/Dockerfile
new file mode 100644
index 000000000000..4c5ab5f5d187
--- /dev/null
+++ b/controllers/resources/Dockerfile
@@ -0,0 +1,22 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+FROM gcr.io/distroless/static:nonroot
+ARG TARGETARCH
+
+WORKDIR /
+USER 65532:65532
+
+COPY bin/controller-resources-$TARGETARCH /manager
+ENTRYPOINT ["/manager"]
diff --git a/controllers/resources/Makefile b/controllers/resources/Makefile
new file mode 100644
index 000000000000..fa0f4c09ba10
--- /dev/null
+++ b/controllers/resources/Makefile
@@ -0,0 +1,139 @@
+
+# Image URL to use all building/pushing image targets
+IMG ?= ghcr.io/labring/sealos-resources-controller:latest
+# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
+ENVTEST_K8S_VERSION = 1.23
+
+# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
+ifeq (,$(shell go env GOBIN))
+GOBIN=$(shell go env GOPATH)/bin
+else
+GOBIN=$(shell go env GOBIN)
+endif
+
+# Setting SHELL to bash allows bash commands to be executed by recipes.
+# This is a requirement for 'setup-envtest.sh' in the test target.
+# Options are set to exit when a recipe line exits non-zero or a piped command fails.
+SHELL = /usr/bin/env bash -o pipefail
+.SHELLFLAGS = -ec
+
+.PHONY: all
+all: build
+
+##@ General
+
+# The help target prints out all targets with their descriptions organized
+# beneath their categories. The categories are represented by '##@' and the
+# target descriptions by '##'. The awk commands is responsible for reading the
+# entire set of makefiles included in this invocation, looking for lines of the
+# file as xyz: ## something, and then pretty-format the target and help. Then,
+# if there's a line with ##@ something, that gets pretty-printed as a category.
+# More info on the usage of ANSI control characters for terminal formatting:
+# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
+# More info on the awk command:
+# http://linuxcommand.org/lc3_adv_awk.php
+
+.PHONY: help
+help: ## Display this help.
+ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
+
+##@ Development
+
+.PHONY: manifests
+manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
+ $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
+
+.PHONY: generate
+generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
+ $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
+
+.PHONY: fmt
+fmt: ## Run go fmt against code.
+ go fmt ./...
+
+.PHONY: vet
+vet: ## Run go vet against code.
+ go vet ./...
+
+.PHONY: test
+test: ## Run tests.
+ go test -race ./... -count=1
+
+##@ Build
+
+.PHONY: build
+build: ## Build manager binary.
+ LD_FLAGS="-s -w" && CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go
+
+.PHONY: run
+run: manifests generate fmt vet ## Run a controller from your host.
+ go run ./main.go
+
+.PHONY: docker-build
+docker-build: build
+ mv bin/manager bin/controller-resources-${TARGETARCH}
+ docker build -t $(IMG) . --build-arg TARGETARCH=${TARGETARCH}
+
+.PHONY: docker-push
+docker-push: docker-build
+ docker push $(IMG)
+
+##@ Deployment
+
+ifndef ignore-not-found
+ ignore-not-found = false
+endif
+
+.PHONY: install
+install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
+ $(KUSTOMIZE) build config/crd | kubectl apply -f -
+
+.PHONY: uninstall
+uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
+ $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
+
+.PHONY: deploy
+deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
+ cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
+ $(KUSTOMIZE) build config/default | kubectl apply -f -
+
+.PHONY: pre-deploy
+pre-deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
+ cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
+ $(KUSTOMIZE) build config/default > deploy/manifests/deploy.yaml
+
+.PHONY: undeploy
+undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
+ $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
+
+##@ Build Dependencies
+
+## Location to install dependencies to
+LOCALBIN ?= $(shell pwd)/bin
+$(LOCALBIN):
+ mkdir -p $(LOCALBIN)
+
+## Tool Binaries
+KUSTOMIZE ?= $(LOCALBIN)/kustomize
+CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
+ENVTEST ?= $(LOCALBIN)/setup-envtest
+
+## Tool Versions
+KUSTOMIZE_VERSION ?= v3.8.7
+CONTROLLER_TOOLS_VERSION ?= v0.8.0
+
+KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
+.PHONY: kustomize
+kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
+$(KUSTOMIZE): $(LOCALBIN)
+ curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN)
+
+.PHONY: controller-gen
+controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
+$(CONTROLLER_GEN): $(LOCALBIN)
+ GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
+
+.PHONY: envtest
+envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
+$(ENVTEST): $(LOCALBIN)
+ GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
diff --git a/controllers/resources/PROJECT b/controllers/resources/PROJECT
new file mode 100644
index 000000000000..6189761b97a5
--- /dev/null
+++ b/controllers/resources/PROJECT
@@ -0,0 +1,22 @@
+domain: sealos.io
+layout:
+- go.kubebuilder.io/v3
+projectName: resources
+repo: github.com/labring/sealos
+resources:
+- controller: true
+ domain: sealos.io
+ group: resources
+ kind: Metering
+ version: v1alpha1
+- controller: true
+ domain: sealos.io
+ group: resources
+ kind: Monitor
+ version: v1alpha1
+- controller: true
+ domain: sealos.io
+ group: resources
+ kind: Billing
+ version: v1alpha1
+version: "3"
diff --git a/controllers/resources/README.md b/controllers/resources/README.md
new file mode 100644
index 000000000000..fbe222063829
--- /dev/null
+++ b/controllers/resources/README.md
@@ -0,0 +1,94 @@
+# resources
+// TODO(user): Add simple overview of use/purpose
+
+## Description
+// TODO(user): An in-depth paragraph about your project and overview of use
+
+## Getting Started
+You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster.
+**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows).
+
+### Running on the cluster
+1. Install Instances of Custom Resources:
+
+```sh
+kubectl apply -f config/samples/
+```
+
+2. Build and push your image to the location specified by `IMG`:
+
+```sh
+make docker-build docker-push IMG=/resources:tag
+```
+
+3. Deploy the controller to the cluster with the image specified by `IMG`:
+
+```sh
+make deploy IMG=/resources:tag
+```
+
+### Uninstall CRDs
+To delete the CRDs from the cluster:
+
+```sh
+make uninstall
+```
+
+### Undeploy controller
+UnDeploy the controller to the cluster:
+
+```sh
+make undeploy
+```
+
+## Contributing
+// TODO(user): Add detailed information on how you would like others to contribute to this project
+
+### How it works
+This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)
+
+It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/)
+which provides a reconcile function responsible for synchronizing resources untile the desired state is reached on the cluster
+
+### Test It Out
+1. Install the CRDs into the cluster:
+
+```sh
+make install
+```
+
+2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running):
+
+```sh
+make run
+```
+
+**NOTE:** You can also run this in one step by running: `make install run`
+
+### Modifying the API definitions
+If you are editing the API definitions, generate the manifests such as CRs or CRDs using:
+
+```sh
+make manifests
+```
+
+**NOTE:** Run `make --help` for more information on all potential `make` targets
+
+More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)
+
+## License
+
+Copyright 2023 sealos.
+
+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.
+
diff --git a/controllers/resources/config/default/kustomization.yaml b/controllers/resources/config/default/kustomization.yaml
new file mode 100644
index 000000000000..3afa168e5626
--- /dev/null
+++ b/controllers/resources/config/default/kustomization.yaml
@@ -0,0 +1,88 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# Adds namespace to all resources.
+namespace: resources-system
+
+# Value of this field is prepended to the
+# names of all resources, e.g. a deployment named
+# "wordpress" becomes "alices-wordpress".
+# Note that it should also match with the prefix (text before '-') of the namespace
+# field above.
+namePrefix: resources-
+
+# Labels to add to all resources and selectors.
+#commonLabels:
+# someName: someValue
+
+bases:
+#- ../crd
+- ../rbac
+- ../manager
+# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
+# crd/kustomization.yaml
+#- ../webhook
+# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
+#- ../certmanager
+# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
+#- ../prometheus
+
+patchesStrategicMerge:
+# Protect the /metrics endpoint by putting it behind auth.
+# If you want your controller-manager to expose the /metrics
+# endpoint w/o any authn/z, please comment the following line.
+- manager_auth_proxy_patch.yaml
+
+# Mount the controller config file for loading manager configurations
+# through a ComponentConfig type
+#- manager_config_patch.yaml
+
+# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
+# crd/kustomization.yaml
+#- manager_webhook_patch.yaml
+
+# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
+# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
+# 'CERTMANAGER' needs to be enabled to use ca injection
+#- webhookcainjection_patch.yaml
+
+# the following config is for teaching kustomize how to do var substitution
+vars:
+# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
+#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
+# objref:
+# kind: Certificate
+# group: cert-manager.io
+# version: v1
+# name: serving-cert # this name should match the one in certificate.yaml
+# fieldref:
+# fieldpath: metadata.namespace
+#- name: CERTIFICATE_NAME
+# objref:
+# kind: Certificate
+# group: cert-manager.io
+# version: v1
+# name: serving-cert # this name should match the one in certificate.yaml
+#- name: SERVICE_NAMESPACE # namespace of the service
+# objref:
+# kind: Service
+# version: v1
+# name: webhook-service
+# fieldref:
+# fieldpath: metadata.namespace
+#- name: SERVICE_NAME
+# objref:
+# kind: Service
+# version: v1
+# name: webhook-service
diff --git a/controllers/resources/config/default/manager_auth_proxy_patch.yaml b/controllers/resources/config/default/manager_auth_proxy_patch.yaml
new file mode 100644
index 000000000000..0251da0ea9b3
--- /dev/null
+++ b/controllers/resources/config/default/manager_auth_proxy_patch.yaml
@@ -0,0 +1,54 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# This patch inject a sidecar container which is a HTTP proxy for the
+# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: controller-manager
+ namespace: system
+spec:
+ template:
+ spec:
+ containers:
+ - name: kube-rbac-proxy
+ securityContext:
+ runAsNonRoot: true
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - "ALL"
+ image: gcr.io/kubebuilder/kube-rbac-proxy:v0.11.0
+ args:
+ - "--secure-listen-address=0.0.0.0:8443"
+ - "--upstream=http://127.0.0.1:8080/"
+ - "--logtostderr=true"
+ - "--v=0"
+ ports:
+ - containerPort: 8443
+ protocol: TCP
+ name: https
+ resources:
+ limits:
+ cpu: 500m
+ memory: 128Mi
+ requests:
+ cpu: 5m
+ memory: 64Mi
+ - name: manager
+ args:
+ - "--health-probe-bind-address=:8081"
+ - "--metrics-bind-address=127.0.0.1:8080"
+ - "--leader-elect"
diff --git a/controllers/resources/config/default/manager_config_patch.yaml b/controllers/resources/config/default/manager_config_patch.yaml
new file mode 100644
index 000000000000..ca4883fc5ce5
--- /dev/null
+++ b/controllers/resources/config/default/manager_config_patch.yaml
@@ -0,0 +1,34 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: controller-manager
+ namespace: system
+spec:
+ template:
+ spec:
+ containers:
+ - name: manager
+ args:
+ - "--config=controller_manager_config.yaml"
+ volumeMounts:
+ - name: manager-config
+ mountPath: /controller_manager_config.yaml
+ subPath: controller_manager_config.yaml
+ volumes:
+ - name: manager-config
+ configMap:
+ name: manager-config
diff --git a/controllers/resources/config/manager/controller_manager_config.yaml b/controllers/resources/config/manager/controller_manager_config.yaml
new file mode 100644
index 000000000000..fd68e304ee2f
--- /dev/null
+++ b/controllers/resources/config/manager/controller_manager_config.yaml
@@ -0,0 +1,25 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: controller-runtime.sigs.k8s.io/v1alpha1
+kind: ControllerManagerConfig
+health:
+ healthProbeBindAddress: :8081
+metrics:
+ bindAddress: 127.0.0.1:8080
+webhook:
+ port: 9443
+leaderElection:
+ leaderElect: true
+ resourceName: a63686c3.sealos.io
diff --git a/controllers/resources/config/manager/kustomization.yaml b/controllers/resources/config/manager/kustomization.yaml
new file mode 100644
index 000000000000..6f28fccfa712
--- /dev/null
+++ b/controllers/resources/config/manager/kustomization.yaml
@@ -0,0 +1,30 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+resources:
+- manager.yaml
+
+generatorOptions:
+ disableNameSuffixHash: true
+
+configMapGenerator:
+- files:
+ - controller_manager_config.yaml
+ name: manager-config
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+images:
+- name: controller
+ newName: ghcr.io/labring/sealos-resources-controller
+ newTag: latest
diff --git a/controllers/resources/config/manager/manager.yaml b/controllers/resources/config/manager/manager.yaml
new file mode 100644
index 000000000000..c7a14bee3162
--- /dev/null
+++ b/controllers/resources/config/manager/manager.yaml
@@ -0,0 +1,79 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: v1
+kind: Namespace
+metadata:
+ labels:
+ control-plane: controller-manager
+ name: system
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: controller-manager
+ namespace: system
+ labels:
+ control-plane: controller-manager
+spec:
+ selector:
+ matchLabels:
+ control-plane: controller-manager
+ replicas: 1
+ template:
+ metadata:
+ annotations:
+ kubectl.kubernetes.io/default-container: manager
+ labels:
+ control-plane: controller-manager
+ spec:
+ securityContext:
+ runAsNonRoot: true
+ containers:
+ - command:
+ - /manager
+ env:
+ - name: MONGO_URI
+ valueFrom:
+ secretKeyRef:
+ name: mongo-secret
+ key: MONGO_URI
+ image: ghcr.io/labring/sealos-resources-controller:latest
+ imagePullPolicy: Always
+ name: manager
+ securityContext:
+ allowPrivilegeEscalation: false
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 8081
+ initialDelaySeconds: 15
+ periodSeconds: 20
+ readinessProbe:
+ httpGet:
+ path: /readyz
+ port: 8081
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ # TODO(user): Configure the resources accordingly based on the project requirements.
+ # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
+ resources:
+ limits:
+ cpu: 1000m
+ memory: 1280Mi
+ requests:
+ cpu: 10m
+ memory: 64Mi
+ serviceAccountName: controller-manager
+ terminationGracePeriodSeconds: 10
diff --git a/controllers/resources/config/prometheus/kustomization.yaml b/controllers/resources/config/prometheus/kustomization.yaml
new file mode 100644
index 000000000000..a32baf71b1b4
--- /dev/null
+++ b/controllers/resources/config/prometheus/kustomization.yaml
@@ -0,0 +1,16 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+resources:
+- monitor.yaml
diff --git a/controllers/resources/config/prometheus/monitor.yaml b/controllers/resources/config/prometheus/monitor.yaml
new file mode 100644
index 000000000000..32401c3d7fdc
--- /dev/null
+++ b/controllers/resources/config/prometheus/monitor.yaml
@@ -0,0 +1,34 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+
+# Prometheus Monitor Service (Metrics)
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+ labels:
+ control-plane: controller-manager
+ name: controller-manager-metrics-monitor
+ namespace: system
+spec:
+ endpoints:
+ - path: /metrics
+ port: https
+ scheme: https
+ bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
+ tlsConfig:
+ insecureSkipVerify: true
+ selector:
+ matchLabels:
+ control-plane: controller-manager
diff --git a/controllers/resources/config/rbac/auth_proxy_client_clusterrole.yaml b/controllers/resources/config/rbac/auth_proxy_client_clusterrole.yaml
new file mode 100644
index 000000000000..0e421adaba5d
--- /dev/null
+++ b/controllers/resources/config/rbac/auth_proxy_client_clusterrole.yaml
@@ -0,0 +1,23 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: metrics-reader
+rules:
+- nonResourceURLs:
+ - "/metrics"
+ verbs:
+ - get
diff --git a/controllers/resources/config/rbac/auth_proxy_role.yaml b/controllers/resources/config/rbac/auth_proxy_role.yaml
new file mode 100644
index 000000000000..7539c6cd1e86
--- /dev/null
+++ b/controllers/resources/config/rbac/auth_proxy_role.yaml
@@ -0,0 +1,31 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: proxy-role
+rules:
+- apiGroups:
+ - authentication.k8s.io
+ resources:
+ - tokenreviews
+ verbs:
+ - create
+- apiGroups:
+ - authorization.k8s.io
+ resources:
+ - subjectaccessreviews
+ verbs:
+ - create
diff --git a/controllers/resources/config/rbac/auth_proxy_role_binding.yaml b/controllers/resources/config/rbac/auth_proxy_role_binding.yaml
new file mode 100644
index 000000000000..84c16ebd8d6a
--- /dev/null
+++ b/controllers/resources/config/rbac/auth_proxy_role_binding.yaml
@@ -0,0 +1,26 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: proxy-rolebinding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: proxy-role
+subjects:
+- kind: ServiceAccount
+ name: controller-manager
+ namespace: system
diff --git a/controllers/resources/config/rbac/auth_proxy_service.yaml b/controllers/resources/config/rbac/auth_proxy_service.yaml
new file mode 100644
index 000000000000..70e47bf67833
--- /dev/null
+++ b/controllers/resources/config/rbac/auth_proxy_service.yaml
@@ -0,0 +1,29 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ control-plane: controller-manager
+ name: controller-manager-metrics-service
+ namespace: system
+spec:
+ ports:
+ - name: https
+ port: 8443
+ protocol: TCP
+ targetPort: https
+ selector:
+ control-plane: controller-manager
diff --git a/controllers/resources/config/rbac/kustomization.yaml b/controllers/resources/config/rbac/kustomization.yaml
new file mode 100644
index 000000000000..9ea536e0fd4f
--- /dev/null
+++ b/controllers/resources/config/rbac/kustomization.yaml
@@ -0,0 +1,32 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+resources:
+# All RBAC will be applied under this service account in
+# the deployment namespace. You may comment out this resource
+# if your manager will use a service account that exists at
+# runtime. Be sure to update RoleBinding and ClusterRoleBinding
+# subjects if changing service account names.
+- service_account.yaml
+- role.yaml
+- role_binding.yaml
+- leader_election_role.yaml
+- leader_election_role_binding.yaml
+# Comment the following 4 lines if you want to disable
+# the auth proxy (https://github.com/brancz/kube-rbac-proxy)
+# which protects your /metrics endpoint.
+- auth_proxy_service.yaml
+- auth_proxy_role.yaml
+- auth_proxy_role_binding.yaml
+- auth_proxy_client_clusterrole.yaml
diff --git a/controllers/resources/config/rbac/leader_election_role.yaml b/controllers/resources/config/rbac/leader_election_role.yaml
new file mode 100644
index 000000000000..7ccba03c9765
--- /dev/null
+++ b/controllers/resources/config/rbac/leader_election_role.yaml
@@ -0,0 +1,51 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions to do leader election.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: leader-election-role
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - configmaps
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+- apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
diff --git a/controllers/resources/config/rbac/leader_election_role_binding.yaml b/controllers/resources/config/rbac/leader_election_role_binding.yaml
new file mode 100644
index 000000000000..493b75ed3993
--- /dev/null
+++ b/controllers/resources/config/rbac/leader_election_role_binding.yaml
@@ -0,0 +1,26 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: leader-election-rolebinding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: leader-election-role
+subjects:
+- kind: ServiceAccount
+ name: controller-manager
+ namespace: system
diff --git a/controllers/resources/config/rbac/role.yaml b/controllers/resources/config/rbac/role.yaml
new file mode 100644
index 000000000000..64d2d82e7304
--- /dev/null
+++ b/controllers/resources/config/rbac/role.yaml
@@ -0,0 +1,101 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ creationTimestamp: null
+ name: manager-role
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - namespaces
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - nodes
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - persistentvolumeclaims
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - pods
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - resourcequotas
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - resourcequotas/status
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - services
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - services/status
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - app.sealos.io
+ resources:
+ - instances
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - app.sealos.io
+ resources:
+ - instances/status
+ verbs:
+ - get
+ - list
+ - watch
diff --git a/controllers/resources/config/rbac/role_binding.yaml b/controllers/resources/config/rbac/role_binding.yaml
new file mode 100644
index 000000000000..749383ffe1ec
--- /dev/null
+++ b/controllers/resources/config/rbac/role_binding.yaml
@@ -0,0 +1,26 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: manager-rolebinding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: manager-role
+subjects:
+- kind: ServiceAccount
+ name: controller-manager
+ namespace: system
diff --git a/controllers/resources/config/rbac/service_account.yaml b/controllers/resources/config/rbac/service_account.yaml
new file mode 100644
index 000000000000..b8ca78e010b4
--- /dev/null
+++ b/controllers/resources/config/rbac/service_account.yaml
@@ -0,0 +1,19 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: controller-manager
+ namespace: system
diff --git a/controllers/resources/controllers/cache/cache.go b/controllers/resources/controllers/cache/cache.go
new file mode 100644
index 000000000000..d0a38a57e1f2
--- /dev/null
+++ b/controllers/resources/controllers/cache/cache.go
@@ -0,0 +1,358 @@
+// Copyright 2026 labring.
+//
+// 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 cache
+
+import (
+ "context"
+ "fmt"
+
+ kbv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
+ appv1 "github.com/labring/sealos/controllers/app/api/v1"
+ "github.com/labring/sealos/controllers/pkg/gpu"
+ "github.com/labring/sealos/controllers/pkg/resources"
+ accounttypes "github.com/labring/sealos/controllers/pkg/types"
+ "github.com/labring/sealos/controllers/pkg/utils/label"
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/fields"
+ ctrl "sigs.k8s.io/controller-runtime"
+ ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const (
+ gpuAliasConfigKey = "alias"
+ gpuInfoConfigKey = "gpu"
+ backupRepositoryLabelKey = "dataprotection.kubeblocks.io/backup-repo-name"
+ networkStatusAnnotationKey = "network.sealos.io/status"
+ originalNodePortLabelKey = "network.sealos.io/original-nodeport"
+ persistentVolumeClaimPhaseKey = "status.phase"
+ backupPhaseKey = "status.phase"
+ serviceTypeKey = "spec.type"
+)
+
+// Options retains only the Kubernetes fields used by resource monitoring.
+func Options() ctrlcache.Options {
+ return ctrlcache.Options{
+ ReaderFailOnMissingInformer: true,
+ DefaultTransform: ctrlcache.TransformStripManagedFields(),
+ ByObject: map[client.Object]ctrlcache.ByObject{
+ &corev1.Namespace{}: {
+ Transform: transformNamespace,
+ },
+ &corev1.Pod{}: {
+ Transform: transformPod,
+ },
+ &corev1.PersistentVolumeClaim{}: {
+ Transform: transformPersistentVolumeClaim,
+ },
+ &kbv1alpha1.Backup{}: {
+ Transform: transformBackup,
+ },
+ &corev1.Service{}: {
+ Transform: transformService,
+ },
+ &appv1.Instance{}: {
+ Transform: transformInstance,
+ },
+ &corev1.ConfigMap{}: {
+ Namespaces: map[string]ctrlcache.Config{
+ gpu.NodeInfoConfigmapNamespace: {
+ FieldSelector: fields.OneTermEqualSelector(
+ "metadata.name",
+ gpu.NodeInfoConfigmapName,
+ ),
+ },
+ },
+ Transform: transformGPUConfigMap,
+ },
+ },
+ }
+}
+
+// UncachedObjects keeps general controller reads and all writes on complete API objects.
+func UncachedObjects() []client.Object {
+ return []client.Object{
+ &corev1.Namespace{},
+ &corev1.Pod{},
+ &corev1.PersistentVolumeClaim{},
+ &kbv1alpha1.Backup{},
+ &corev1.Service{},
+ &appv1.Instance{},
+ &corev1.ConfigMap{},
+ }
+}
+
+// SetupInformers registers projected informers that are read only by the periodic monitor.
+func SetupInformers(mgr ctrl.Manager) error {
+ instanceMetadata := &metav1.PartialObjectMetadata{}
+ instanceMetadata.SetGroupVersionKind(appv1.GroupVersion.WithKind("Instance"))
+ objects := []client.Object{
+ &corev1.Namespace{},
+ &corev1.Pod{},
+ &corev1.ConfigMap{},
+ instanceMetadata,
+ }
+
+ for _, object := range objects {
+ if _, err := mgr.GetCache().GetInformer(context.Background(), object); err != nil {
+ return fmt.Errorf("register informer for %T: %w", object, err)
+ }
+ }
+ return nil
+}
+
+func transformNamespace(obj any) (any, error) {
+ ns, ok := obj.(*corev1.Namespace)
+ if !ok {
+ return obj, nil
+ }
+
+ metadata := projectObjectMeta(ns.ObjectMeta)
+ metadata.Labels = copyMapValues(ns.Labels, userv1.UserLabelOwnerKey)
+ metadata.Annotations = copyMapValues(
+ ns.Annotations,
+ accounttypes.DebtNamespaceAnnoStatusKey,
+ accounttypes.WorkspaceSubscriptionStatusAnnoKey,
+ networkStatusAnnotationKey,
+ )
+ return &corev1.Namespace{
+ TypeMeta: ns.TypeMeta,
+ ObjectMeta: metadata,
+ }, nil
+}
+
+func transformPod(obj any) (any, error) {
+ pod, ok := obj.(*corev1.Pod)
+ if !ok {
+ return obj, nil
+ }
+
+ containers := make([]corev1.Container, 0, len(pod.Spec.Containers))
+ for i := range pod.Spec.Containers {
+ container := &pod.Spec.Containers[i]
+ projected := corev1.Container{
+ Name: container.Name,
+ Resources: corev1.ResourceRequirements{
+ Limits: copyResourceList(container.Resources.Limits),
+ Requests: copyResourceList(container.Resources.Requests),
+ },
+ }
+ if container.Name == "acmesolver" {
+ projected.Args = append([]string(nil), container.Args...)
+ }
+ containers = append(containers, projected)
+ }
+
+ metadata := projectObjectMeta(pod.ObjectMeta)
+ metadata.Labels = resourceLabels(pod.Labels)
+ projected := &corev1.Pod{
+ TypeMeta: pod.TypeMeta,
+ ObjectMeta: metadata,
+ Spec: corev1.PodSpec{
+ NodeName: pod.Spec.NodeName,
+ Containers: containers,
+ },
+ Status: corev1.PodStatus{
+ Phase: pod.Status.Phase,
+ },
+ }
+ if pod.Status.StartTime != nil {
+ projected.Status.StartTime = pod.Status.StartTime.DeepCopy()
+ }
+ return projected, nil
+}
+
+func transformPersistentVolumeClaim(obj any) (any, error) {
+ pvc, ok := obj.(*corev1.PersistentVolumeClaim)
+ if !ok {
+ return obj, nil
+ }
+
+ metadata := projectObjectMeta(pvc.ObjectMeta)
+ metadata.Labels = resourceLabels(pvc.Labels)
+ if len(pvc.OwnerReferences) > 0 {
+ metadata.OwnerReferences = []metav1.OwnerReference{{
+ Kind: pvc.OwnerReferences[0].Kind,
+ }}
+ }
+ requests := corev1.ResourceList{}
+ if storage, ok := pvc.Spec.Resources.Requests[corev1.ResourceStorage]; ok {
+ requests[corev1.ResourceStorage] = storage.DeepCopy()
+ }
+ return &corev1.PersistentVolumeClaim{
+ TypeMeta: pvc.TypeMeta,
+ ObjectMeta: metadata,
+ Spec: corev1.PersistentVolumeClaimSpec{
+ Resources: corev1.VolumeResourceRequirements{Requests: requests},
+ },
+ Status: corev1.PersistentVolumeClaimStatus{
+ Phase: pvc.Status.Phase,
+ },
+ }, nil
+}
+
+func transformBackup(obj any) (any, error) {
+ backup, ok := obj.(*kbv1alpha1.Backup)
+ if !ok {
+ return obj, nil
+ }
+
+ metadata := projectObjectMeta(backup.ObjectMeta)
+ metadata.Labels = resourceLabels(backup.Labels)
+ return &kbv1alpha1.Backup{
+ TypeMeta: backup.TypeMeta,
+ ObjectMeta: metadata,
+ Status: kbv1alpha1.BackupStatus{
+ Phase: backup.Status.Phase,
+ TotalSize: backup.Status.TotalSize,
+ },
+ }, nil
+}
+
+func transformService(obj any) (any, error) {
+ service, ok := obj.(*corev1.Service)
+ if !ok {
+ return obj, nil
+ }
+
+ ports := make([]corev1.ServicePort, len(service.Spec.Ports))
+ for i := range service.Spec.Ports {
+ ports[i].NodePort = service.Spec.Ports[i].NodePort
+ }
+ metadata := projectObjectMeta(service.ObjectMeta)
+ metadata.Labels = resourceLabels(service.Labels)
+ if value, ok := service.Labels[originalNodePortLabelKey]; ok {
+ if metadata.Labels == nil {
+ metadata.Labels = make(map[string]string)
+ }
+ metadata.Labels[originalNodePortLabelKey] = value
+ }
+ return &corev1.Service{
+ TypeMeta: service.TypeMeta,
+ ObjectMeta: metadata,
+ Spec: corev1.ServiceSpec{
+ Type: service.Spec.Type,
+ Ports: ports,
+ },
+ }, nil
+}
+
+func transformInstance(obj any) (any, error) {
+ switch instance := obj.(type) {
+ case *appv1.Instance:
+ metadata := projectObjectMeta(instance.ObjectMeta)
+ metadata.Labels = copyMapValues(instance.Labels, resources.AppStoreDeployLabelKey)
+ return &appv1.Instance{
+ TypeMeta: instance.TypeMeta,
+ ObjectMeta: metadata,
+ }, nil
+ case *metav1.PartialObjectMetadata:
+ metadata := projectObjectMeta(instance.ObjectMeta)
+ metadata.Labels = copyMapValues(instance.Labels, resources.AppStoreDeployLabelKey)
+ return &metav1.PartialObjectMetadata{
+ TypeMeta: instance.TypeMeta,
+ ObjectMeta: metadata,
+ }, nil
+ default:
+ return obj, nil
+ }
+}
+
+func transformGPUConfigMap(obj any) (any, error) {
+ configMap, ok := obj.(*corev1.ConfigMap)
+ if !ok {
+ return obj, nil
+ }
+
+ return &corev1.ConfigMap{
+ TypeMeta: configMap.TypeMeta,
+ ObjectMeta: projectObjectMeta(configMap.ObjectMeta),
+ Data: copyMapValues(
+ configMap.Data,
+ gpuAliasConfigKey,
+ gpuInfoConfigKey,
+ ),
+ }, nil
+}
+
+func resourceLabels(source map[string]string) map[string]string {
+ return copyMapValues(
+ source,
+ resources.DBPodLabelInstanceKey,
+ resources.DBPodLabelComponentNameKey,
+ resources.TerminalIDLabelKey,
+ label.AppManagedBy,
+ label.AppPartOf,
+ label.AppName,
+ resources.AppLabelKey,
+ resources.AppDeployLabelKey,
+ resources.JobNameLabelKey,
+ resources.ACMEChallengeKey,
+ backupRepositoryLabelKey,
+ resources.InstanceLabelKey,
+ )
+}
+
+func copyResourceList(source corev1.ResourceList) corev1.ResourceList {
+ if len(source) == 0 {
+ return nil
+ }
+ result := make(corev1.ResourceList, len(source))
+ for name, quantity := range source {
+ result[name] = quantity.DeepCopy()
+ }
+ return result
+}
+
+func projectObjectMeta(in metav1.ObjectMeta) metav1.ObjectMeta {
+ out := metav1.ObjectMeta{
+ Name: in.Name,
+ Namespace: in.Namespace,
+ UID: in.UID,
+ ResourceVersion: in.ResourceVersion,
+ Generation: in.Generation,
+ CreationTimestamp: in.CreationTimestamp,
+ }
+ if in.DeletionTimestamp != nil {
+ out.DeletionTimestamp = in.DeletionTimestamp.DeepCopy()
+ }
+ if in.DeletionGracePeriodSeconds != nil {
+ gracePeriod := *in.DeletionGracePeriodSeconds
+ out.DeletionGracePeriodSeconds = &gracePeriod
+ }
+ return out
+}
+
+func copyMapValues(source map[string]string, keys ...string) map[string]string {
+ var result map[string]string
+ for _, key := range keys {
+ if value, ok := source[key]; ok {
+ if result == nil {
+ result = make(map[string]string)
+ }
+ result[key] = value
+ }
+ }
+ return result
+}
+
+// Index field names shared with the monitor reconciler.
+const (
+ PersistentVolumeClaimPhaseKey = persistentVolumeClaimPhaseKey
+ BackupPhaseKey = backupPhaseKey
+ ServiceTypeKey = serviceTypeKey
+)
diff --git a/controllers/resources/controllers/cache/cache_test.go b/controllers/resources/controllers/cache/cache_test.go
new file mode 100644
index 000000000000..968e3fb869e8
--- /dev/null
+++ b/controllers/resources/controllers/cache/cache_test.go
@@ -0,0 +1,419 @@
+// Copyright 2026 labring.
+//
+// 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 cache
+
+import (
+ "reflect"
+ "testing"
+
+ kbv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
+ appv1 "github.com/labring/sealos/controllers/app/api/v1"
+ "github.com/labring/sealos/controllers/pkg/gpu"
+ "github.com/labring/sealos/controllers/pkg/resources"
+ accounttypes "github.com/labring/sealos/controllers/pkg/types"
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+func TestOptionsRegistersOnlyMonitorObjects(t *testing.T) {
+ options := Options()
+ if !options.ReaderFailOnMissingInformer {
+ t.Fatal("missing informer reads are allowed")
+ }
+ if options.DefaultTransform == nil {
+ t.Fatal("default managed fields transform is nil")
+ }
+
+ required := []client.Object{
+ &corev1.Namespace{},
+ &corev1.Pod{},
+ &corev1.PersistentVolumeClaim{},
+ &kbv1alpha1.Backup{},
+ &corev1.Service{},
+ &appv1.Instance{},
+ &corev1.ConfigMap{},
+ }
+ if len(options.ByObject) != len(required) {
+ t.Fatalf("cache object count = %d, want %d", len(options.ByObject), len(required))
+ }
+ for _, expected := range required {
+ found := false
+ for obj, byObject := range options.ByObject {
+ if reflect.TypeOf(obj) != reflect.TypeOf(expected) {
+ continue
+ }
+ found = true
+ if byObject.Transform == nil {
+ t.Fatalf("%T transform is nil", expected)
+ }
+ }
+ if !found {
+ t.Fatalf("%T cache options not found", expected)
+ }
+ }
+}
+
+func TestUncachedObjectsProtectProjectedWrites(t *testing.T) {
+ disabled := UncachedObjects()
+ if len(disabled) != len(Options().ByObject) {
+ t.Fatalf("uncached object count = %d, want %d", len(disabled), len(Options().ByObject))
+ }
+ for cached := range Options().ByObject {
+ found := false
+ for _, object := range disabled {
+ if reflect.TypeOf(cached) == reflect.TypeOf(object) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Fatalf("projected %T is not disabled on the general client", cached)
+ }
+ }
+}
+
+func TestOptionsLimitsGPUConfigMapCache(t *testing.T) {
+ options := Options()
+ for obj, byObject := range options.ByObject {
+ if _, ok := obj.(*corev1.ConfigMap); !ok {
+ continue
+ }
+ if len(byObject.Namespaces) != 1 {
+ t.Fatalf("configmap cache namespaces = %d, want 1", len(byObject.Namespaces))
+ }
+ config, ok := byObject.Namespaces[gpu.NodeInfoConfigmapNamespace]
+ if !ok {
+ t.Fatalf("configmap cache does not include %q", gpu.NodeInfoConfigmapNamespace)
+ }
+ if got, want := config.FieldSelector.String(), "metadata.name="+gpu.NodeInfoConfigmapName; got != want {
+ t.Fatalf("configmap field selector = %q, want %q", got, want)
+ }
+ return
+ }
+ t.Fatal("configmap cache options not found")
+}
+
+func TestTransformNamespaceKeepsMonitorSelectionFields(t *testing.T) {
+ ns := &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ns-user-a",
+ ResourceVersion: "42",
+ Labels: map[string]string{
+ userv1.UserLabelOwnerKey: "user-a",
+ "unused": "large-value",
+ },
+ Annotations: map[string]string{
+ accounttypes.DebtNamespaceAnnoStatusKey: accounttypes.SuspendDebtNamespaceAnnoStatus,
+ accounttypes.WorkspaceSubscriptionStatusAnnoKey: "active",
+ networkStatusAnnotationKey: "Suspend",
+ "unused": "large-value",
+ },
+ ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}},
+ },
+ Spec: corev1.NamespaceSpec{Finalizers: []corev1.FinalizerName{"kubernetes"}},
+ }
+
+ transformed, err := transformNamespace(ns)
+ if err != nil {
+ t.Fatalf("transform namespace: %v", err)
+ }
+ got, ok := transformed.(*corev1.Namespace)
+ if !ok {
+ t.Fatalf("transformed type = %T, want *v1.Namespace", transformed)
+ }
+ if got.Name != ns.Name || got.ResourceVersion != ns.ResourceVersion {
+ t.Fatalf("required identity metadata was not retained: %#v", got.ObjectMeta)
+ }
+ if !reflect.DeepEqual(got.Labels, map[string]string{userv1.UserLabelOwnerKey: "user-a"}) {
+ t.Fatalf("namespace labels = %#v", got.Labels)
+ }
+ if !reflect.DeepEqual(got.Annotations, map[string]string{
+ accounttypes.DebtNamespaceAnnoStatusKey: accounttypes.SuspendDebtNamespaceAnnoStatus,
+ accounttypes.WorkspaceSubscriptionStatusAnnoKey: "active",
+ networkStatusAnnotationKey: "Suspend",
+ }) {
+ t.Fatalf("namespace annotations = %#v", got.Annotations)
+ }
+ if len(got.ManagedFields) != 0 || len(got.Spec.Finalizers) != 0 {
+ t.Fatalf("unused namespace payload was retained: %#v", got)
+ }
+}
+
+func TestTransformPodKeepsResourceAccountingFields(t *testing.T) {
+ startTime := metav1.Now()
+ pod := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "app-a-0",
+ Namespace: "ns-user-a",
+ Labels: map[string]string{
+ resources.AppLabelKey: "app-a",
+ "unused": "large-value",
+ },
+ Annotations: map[string]string{"unused": "large-value"},
+ ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}},
+ },
+ Spec: corev1.PodSpec{
+ NodeName: "node-a",
+ Containers: []corev1.Container{
+ {
+ Name: "app",
+ Image: "large-image-name",
+ Args: []string{"unused", "arguments"},
+ Resources: corev1.ResourceRequirements{
+ Requests: corev1.ResourceList{
+ corev1.ResourceCPU: resource.MustParse("500m"),
+ },
+ Limits: corev1.ResourceList{
+ corev1.ResourceMemory: resource.MustParse("1Gi"),
+ },
+ Claims: []corev1.ResourceClaim{{Name: "unused-claim"}},
+ },
+ },
+ {
+ Name: "acmesolver",
+ Args: []string{"--domain=example.test"},
+ },
+ },
+ },
+ Status: corev1.PodStatus{
+ Phase: corev1.PodRunning,
+ StartTime: &startTime,
+ PodIP: "10.0.0.1",
+ },
+ }
+
+ transformed, err := transformPod(pod)
+ if err != nil {
+ t.Fatalf("transform pod: %v", err)
+ }
+ got, ok := transformed.(*corev1.Pod)
+ if !ok {
+ t.Fatalf("transformed type = %T, want *v1.Pod", transformed)
+ }
+ if got.Spec.NodeName != pod.Spec.NodeName || got.Status.Phase != pod.Status.Phase ||
+ got.Status.StartTime == nil || !got.Status.StartTime.Equal(pod.Status.StartTime) {
+ t.Fatalf("pod scheduling fields were not retained: %#v", got)
+ }
+ if got.Spec.Containers[0].Resources.Requests.Cpu().String() != "500m" ||
+ got.Spec.Containers[0].Resources.Limits.Memory().String() != "1Gi" {
+ t.Fatalf("pod resources were not retained: %#v", got.Spec.Containers[0].Resources)
+ }
+ if len(got.Spec.Containers[0].Args) != 0 || got.Spec.Containers[0].Image != "" ||
+ len(got.Spec.Containers[0].Resources.Claims) != 0 ||
+ !reflect.DeepEqual(got.Spec.Containers[1].Args, []string{"--domain=example.test"}) {
+ t.Fatalf("pod container projection is incorrect: %#v", got.Spec.Containers)
+ }
+ if got.Status.PodIP != "" || len(got.Annotations) != 0 || len(got.ManagedFields) != 0 {
+ t.Fatalf("unused pod payload was retained: %#v", got)
+ }
+ if named := resources.NewResourceNamed(got); named.Name() != "app-a" {
+ t.Fatalf("resource name = %q, want app-a", named.Name())
+ }
+}
+
+func TestTransformPersistentVolumeClaimKeepsIndexAndStorage(t *testing.T) {
+ pvc := &corev1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "data-app-a",
+ Namespace: "ns-user-a",
+ Labels: map[string]string{resources.AppLabelKey: "app-a", "unused": "value"},
+ OwnerReferences: []metav1.OwnerReference{
+ {Kind: "StatefulSet", Name: "app-a", UID: types.UID("owner-a")},
+ {Kind: "Other", Name: "unused"},
+ },
+ ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}},
+ },
+ Spec: corev1.PersistentVolumeClaimSpec{
+ VolumeName: "large-volume-name",
+ Resources: corev1.VolumeResourceRequirements{Requests: corev1.ResourceList{
+ corev1.ResourceStorage: resource.MustParse("20Gi"),
+ corev1.ResourceCPU: resource.MustParse("1"),
+ }},
+ },
+ Status: corev1.PersistentVolumeClaimStatus{
+ Phase: corev1.ClaimBound,
+ Capacity: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("20Gi")},
+ },
+ }
+
+ transformed, err := transformPersistentVolumeClaim(pvc)
+ if err != nil {
+ t.Fatalf("transform pvc: %v", err)
+ }
+ got, ok := transformed.(*corev1.PersistentVolumeClaim)
+ if !ok {
+ t.Fatalf("transformed type = %T, want *v1.PersistentVolumeClaim", transformed)
+ }
+ if got.Status.Phase != corev1.ClaimBound ||
+ got.Spec.Resources.Requests.Storage().String() != "20Gi" {
+ t.Fatalf("pvc accounting fields were not retained: %#v", got)
+ }
+ if len(got.OwnerReferences) != 1 || got.OwnerReferences[0].Kind != "StatefulSet" ||
+ got.OwnerReferences[0].Name != "" {
+ t.Fatalf("pvc owner projection = %#v", got.OwnerReferences)
+ }
+ if got.Spec.VolumeName != "" || len(got.Status.Capacity) != 0 ||
+ got.Spec.Resources.Requests.Cpu().Sign() != 0 || len(got.ManagedFields) != 0 {
+ t.Fatalf("unused pvc payload was retained: %#v", got)
+ }
+}
+
+func TestTransformBackupAndServiceKeepAccountingFields(t *testing.T) {
+ backup := &kbv1alpha1.Backup{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "backup-a",
+ Labels: map[string]string{
+ resources.InstanceLabelKey: "database-a",
+ "unused": "value",
+ },
+ ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}},
+ },
+ Status: kbv1alpha1.BackupStatus{
+ Phase: kbv1alpha1.BackupPhaseCompleted,
+ TotalSize: "2Gi",
+ FailureReason: "unused-large-value",
+ },
+ }
+ transformedBackup, err := transformBackup(backup)
+ if err != nil {
+ t.Fatalf("transform backup: %v", err)
+ }
+ gotBackup, ok := transformedBackup.(*kbv1alpha1.Backup)
+ if !ok {
+ t.Fatalf("transformed type = %T, want *v1alpha1.Backup", transformedBackup)
+ }
+ if gotBackup.Status.Phase != backup.Status.Phase || gotBackup.Status.TotalSize != "2Gi" ||
+ gotBackup.Status.FailureReason != "" || len(gotBackup.ManagedFields) != 0 {
+ t.Fatalf("backup projection is incorrect: %#v", gotBackup)
+ }
+
+ service := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "app-a",
+ Labels: map[string]string{
+ resources.AppLabelKey: "app-a",
+ originalNodePortLabelKey: "true",
+ "unused": "value",
+ },
+ ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}},
+ },
+ Spec: corev1.ServiceSpec{
+ Type: corev1.ServiceTypeNodePort,
+ ClusterIP: "10.96.0.1",
+ Selector: map[string]string{"large": "selector"},
+ Ports: []corev1.ServicePort{
+ {Name: "http", Port: 80, NodePort: 30080},
+ {Name: "https", Port: 443, NodePort: 30443},
+ },
+ },
+ }
+ transformedService, err := transformService(service)
+ if err != nil {
+ t.Fatalf("transform service: %v", err)
+ }
+ gotService, ok := transformedService.(*corev1.Service)
+ if !ok {
+ t.Fatalf("transformed type = %T, want *v1.Service", transformedService)
+ }
+ if gotService.Spec.Type != corev1.ServiceTypeNodePort ||
+ gotService.Spec.Ports[0].NodePort != 30080 || gotService.Spec.Ports[1].NodePort != 30443 {
+ t.Fatalf("service accounting fields were not retained: %#v", gotService.Spec)
+ }
+ if gotService.Labels[originalNodePortLabelKey] != "true" {
+ t.Fatalf("service network label was not retained: %#v", gotService.Labels)
+ }
+ if gotService.Spec.ClusterIP != "" || len(gotService.Spec.Selector) != 0 ||
+ gotService.Spec.Ports[0].Name != "" || gotService.Spec.Ports[0].Port != 0 ||
+ len(gotService.ManagedFields) != 0 {
+ t.Fatalf("unused service payload was retained: %#v", gotService)
+ }
+}
+
+func TestTransformInstanceAndGPUConfigMap(t *testing.T) {
+ instance := &appv1.Instance{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "instance-a",
+ Labels: map[string]string{
+ resources.AppStoreDeployLabelKey: "store-a",
+ "unused": "value",
+ },
+ Annotations: map[string]string{"unused": "value"},
+ ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}},
+ },
+ }
+ transformedInstance, err := transformInstance(instance)
+ if err != nil {
+ t.Fatalf("transform instance: %v", err)
+ }
+ gotInstance, ok := transformedInstance.(*appv1.Instance)
+ if !ok {
+ t.Fatalf("transformed type = %T, want *v1.Instance", transformedInstance)
+ }
+ if !reflect.DeepEqual(gotInstance.Labels, map[string]string{
+ resources.AppStoreDeployLabelKey: "store-a",
+ }) || len(gotInstance.Annotations) != 0 || len(gotInstance.ManagedFields) != 0 {
+ t.Fatalf("instance projection is incorrect: %#v", gotInstance)
+ }
+ instanceMetadata := &metav1.PartialObjectMetadata{
+ ObjectMeta: instance.ObjectMeta,
+ }
+ instanceMetadata.SetGroupVersionKind(appv1.GroupVersion.WithKind("Instance"))
+ transformedMetadata, err := transformInstance(instanceMetadata)
+ if err != nil {
+ t.Fatalf("transform instance metadata: %v", err)
+ }
+ gotMetadata, ok := transformedMetadata.(*metav1.PartialObjectMetadata)
+ if !ok {
+ t.Fatalf("transformed type = %T, want *metav1.PartialObjectMetadata", transformedMetadata)
+ }
+ if gotMetadata.GroupVersionKind() != instanceMetadata.GroupVersionKind() ||
+ !reflect.DeepEqual(gotMetadata.Labels, gotInstance.Labels) ||
+ len(gotMetadata.Annotations) != 0 || len(gotMetadata.ManagedFields) != 0 {
+ t.Fatalf("instance metadata projection is incorrect: %#v", gotMetadata)
+ }
+
+ configMap := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: gpu.NodeInfoConfigmapName,
+ Namespace: gpu.NodeInfoConfigmapNamespace,
+ ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}},
+ ResourceVersion: "42",
+ },
+ Data: map[string]string{
+ gpuAliasConfigKey: "alias-data",
+ gpuInfoConfigKey: "gpu-data",
+ "unused": "large-value",
+ },
+ BinaryData: map[string][]byte{"unused": []byte("large-value")},
+ }
+ transformedConfigMap, err := transformGPUConfigMap(configMap)
+ if err != nil {
+ t.Fatalf("transform configmap: %v", err)
+ }
+ gotConfigMap, ok := transformedConfigMap.(*corev1.ConfigMap)
+ if !ok {
+ t.Fatalf("transformed type = %T, want *v1.ConfigMap", transformedConfigMap)
+ }
+ if !reflect.DeepEqual(gotConfigMap.Data, map[string]string{
+ gpuAliasConfigKey: "alias-data",
+ gpuInfoConfigKey: "gpu-data",
+ }) || len(gotConfigMap.BinaryData) != 0 || len(gotConfigMap.ManagedFields) != 0 {
+ t.Fatalf("configmap projection is incorrect: %#v", gotConfigMap)
+ }
+}
diff --git a/controllers/resources/controllers/monitor_controller.go b/controllers/resources/controllers/monitor_controller.go
new file mode 100644
index 000000000000..548c2b46c970
--- /dev/null
+++ b/controllers/resources/controllers/monitor_controller.go
@@ -0,0 +1,1092 @@
+/*
+Copyright 2023 sealos.
+
+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 controllers
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "math"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ kbv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
+ "github.com/go-logr/logr"
+ appv1 "github.com/labring/sealos/controllers/app/api/v1"
+ "github.com/labring/sealos/controllers/pkg/database"
+ "github.com/labring/sealos/controllers/pkg/gpu"
+ objstorage "github.com/labring/sealos/controllers/pkg/objectstorage"
+ "github.com/labring/sealos/controllers/pkg/resources"
+ "github.com/labring/sealos/controllers/pkg/types"
+ "github.com/labring/sealos/controllers/pkg/utils/env"
+ "github.com/labring/sealos/controllers/pkg/utils/logger"
+ "github.com/labring/sealos/controllers/pkg/utils/retry"
+ resourcecache "github.com/labring/sealos/controllers/resources/controllers/cache"
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ "github.com/minio/minio-go/v7"
+ "golang.org/x/sync/errgroup"
+ "golang.org/x/sync/semaphore"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/fields"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/selection"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// MonitorReconciler reconciles a Monitor object
+type MonitorReconciler struct {
+ client.Client
+ cache client.Reader
+ logr.Logger
+ Interval time.Duration
+ Scheme *runtime.Scheme
+ stopCh chan struct{}
+ wg sync.WaitGroup
+ periodicReconcile time.Duration
+ gpuAliasCard map[string]corev1.ResourceName
+ gpuNodeAlias map[string]string
+ gpuMutex sync.RWMutex
+ DBClient database.Interface
+ TrafficClient database.Interface
+ Properties *resources.PropertyTypeLS
+ PromURL string
+ lastObjectMetrics objstorage.Metrics
+ currentObjectMetrics objstorage.Metrics
+ ObjStorageClient *minio.Client
+ ObjStorageMetricsClient *objstorage.MetricsClient
+ ObjStorageUserBackupSize map[string]int64
+ ObjectStorageInstance string
+}
+
+type quantity struct {
+ *resource.Quantity
+ detail string
+}
+
+type gpuAliasResource struct {
+ Card string `json:"card"`
+}
+
+type gpuAliasConfig struct {
+ Resource gpuAliasResource `json:"resource"`
+}
+
+type gpuNodeConfig struct {
+ Ref string `json:"gpu.ref"`
+ Product string `json:"gpu.product"`
+}
+
+const (
+ PrometheusURL = "PROM_URL"
+ ObjectStorageInstance = "OBJECT_STORAGE_INSTANCE"
+ ConcurrentLimit = "CONCURRENT_LIMIT"
+ envEphemeralStorageChargeThreshold = "EPHEMERAL_STORAGE_CHARGE_THRESHOLD"
+)
+
+const (
+ gpuAliasConfigKey = "alias"
+ gpuInfoConfigKey = "gpu"
+)
+
+var (
+ concurrentLimit = int64(DefaultConcurrencyLimit)
+ ephemeralStorageChargeThreshold = resource.MustParse(
+ env.GetEnvWithDefault(envEphemeralStorageChargeThreshold, "10Gi"),
+ )
+)
+
+const (
+ DefaultConcurrencyLimit = 1000
+)
+
+//+kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch
+//+kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=get;list;watch
+//+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch
+//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
+//+kubebuilder:rbac:groups=core,resources=resourcequotas,verbs=get;list;watch
+//+kubebuilder:rbac:groups=core,resources=resourcequotas/status,verbs=get;list;watch
+//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch
+//+kubebuilder:rbac:groups=core,resources=services/status,verbs=get;list;watch
+//+kubebuilder:rbac:groups=app.sealos.io,resources=instances,verbs=get;list;watch
+//+kubebuilder:rbac:groups=app.sealos.io,resources=instances/status,verbs=get;list;watch
+//+kubebuilder:rbac:groups=dataprotection.apecloud.io,resources=backups,verbs=get;list;watch
+//+kubebuilder:rbac:groups=dataprotection.apecloud.io,resources=backups/status,verbs=get;list;watch
+
+func NewMonitorReconciler(mgr ctrl.Manager) (*MonitorReconciler, error) {
+ r := &MonitorReconciler{
+ Client: mgr.GetClient(),
+ cache: mgr.GetCache(),
+ Logger: ctrl.Log.WithName("controllers").WithName("Monitor"),
+ stopCh: make(chan struct{}),
+ periodicReconcile: 1 * time.Minute,
+ PromURL: os.Getenv(PrometheusURL),
+ ObjectStorageInstance: os.Getenv(ObjectStorageInstance),
+ gpuAliasCard: make(map[string]corev1.ResourceName),
+ gpuNodeAlias: make(map[string]string),
+ }
+ concurrentLimit = env.GetInt64EnvWithDefault(ConcurrentLimit, DefaultConcurrencyLimit)
+ var err error
+ err = retry.Retry(2, 1*time.Second, func() error {
+ if err = r.refreshGPUConfig(context.Background()); err != nil {
+ return fmt.Errorf("failed to refresh gpu config: %w", err)
+ }
+ return nil
+ })
+ if err != nil {
+ fmt.Printf("INFO: refresh gpu config: %v, will retry in next reconcile", err)
+ }
+ r.gpuMutex.RLock()
+ aliasCount := len(r.gpuAliasCard)
+ nodeCount := len(r.gpuNodeAlias)
+ r.gpuMutex.RUnlock()
+ r.Info("refresh gpu config", "aliasCount", aliasCount, "nodeCount", nodeCount)
+ return r, nil
+}
+
+func InitIndexField(mgr ctrl.Manager) error {
+ if err := mgr.GetFieldIndexer().
+ IndexField(context.Background(), &corev1.PersistentVolumeClaim{}, resourcecache.PersistentVolumeClaimPhaseKey, func(rawObj client.Object) []string {
+ pvc, ok := rawObj.(*corev1.PersistentVolumeClaim)
+ if !ok {
+ return nil
+ }
+ return []string{string(pvc.Status.Phase)}
+ }); err != nil {
+ return err
+ }
+ if err := mgr.GetFieldIndexer().
+ IndexField(context.Background(), &kbv1alpha1.Backup{}, resourcecache.BackupPhaseKey, func(rawObj client.Object) []string {
+ backup, ok := rawObj.(*kbv1alpha1.Backup)
+ if !ok {
+ return nil
+ }
+ return []string{string(backup.Status.Phase)}
+ }); err != nil {
+ return err
+ }
+ return mgr.GetFieldIndexer().
+ IndexField(context.Background(), &corev1.Service{}, resourcecache.ServiceTypeKey, func(rawObj client.Object) []string {
+ svc, ok := rawObj.(*corev1.Service)
+ if !ok {
+ return nil
+ }
+ return []string{string(svc.Spec.Type)}
+ })
+}
+
+func (r *MonitorReconciler) StartReconciler(ctx context.Context) error {
+ r.startPeriodicReconcile()
+ if r.TrafficClient != nil || r.ObjStorageClient != nil {
+ r.startMonitorTraffic()
+ }
+ <-ctx.Done()
+ r.stopPeriodicReconcile()
+ return nil
+}
+
+func (r *MonitorReconciler) startPeriodicReconcile() {
+ r.wg.Add(1)
+ go func() {
+ defer r.wg.Done()
+ waitNextMinute()
+ ticker := time.NewTicker(r.periodicReconcile)
+ for {
+ select {
+ case <-ticker.C:
+ r.enqueueNamespacesForReconcile()
+ if err := r.refreshGPUConfig(context.Background()); err != nil {
+ r.Error(err, "refresh gpu config failed")
+ }
+ case <-r.stopCh:
+ ticker.Stop()
+ return
+ }
+ }
+ }()
+}
+
+func (r *MonitorReconciler) getNamespaceList() (*corev1.NamespaceList, error) {
+ namespaceList := &corev1.NamespaceList{}
+ req, err := labels.NewRequirement(userv1.UserLabelOwnerKey, selection.Exists, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create label requirement: %w", err)
+ }
+ return namespaceList, r.cache.List(context.Background(), namespaceList, &client.ListOptions{
+ LabelSelector: labels.NewSelector().Add(*req),
+ })
+}
+
+func waitNextMinute() {
+ waitTime := time.Until(time.Now().Truncate(time.Minute).Add(1 * time.Minute))
+ if waitTime > 0 {
+ logger.Info("wait for first reconcile", "waitTime", waitTime)
+ time.Sleep(waitTime)
+ }
+}
+
+func waitNextHour() {
+ waitTime := time.Until(time.Now().Truncate(time.Hour).Add(1 * time.Hour))
+ if waitTime > 0 {
+ logger.Info("wait for first reconcile", "waitTime", waitTime)
+ time.Sleep(waitTime)
+ }
+}
+
+func (r *MonitorReconciler) startMonitorTraffic() {
+ r.wg.Add(1)
+ go func() {
+ defer r.wg.Done()
+ startTime, endTime := time.Now().
+ UTC(),
+ time.Now().
+ Truncate(time.Hour).
+ Add(1*time.Hour).
+ UTC()
+ waitNextHour()
+ ticker := time.NewTicker(1 * time.Hour)
+ if err := r.MonitorTrafficUsed(startTime, endTime); err != nil {
+ r.Error(err, "failed to monitor pod traffic used")
+ }
+ for {
+ select {
+ case <-ticker.C:
+ startTime, endTime = endTime, endTime.Add(1*time.Hour)
+ if err := r.MonitorTrafficUsed(startTime, endTime); err != nil {
+ r.Error(err, "failed to monitor pod traffic used")
+ break
+ }
+ case <-r.stopCh:
+ ticker.Stop()
+ return
+ }
+ }
+ }()
+}
+
+func (r *MonitorReconciler) stopPeriodicReconcile() {
+ close(r.stopCh)
+ r.wg.Wait()
+}
+
+func (r *MonitorReconciler) enqueueNamespacesForReconcile() {
+ r.Info("enqueue namespaces for reconcile", "time", time.Now().Format(time.RFC3339))
+
+ namespaceList, err := r.getNamespaceList()
+ if err != nil {
+ r.Error(err, "failed to list namespaces")
+ return
+ }
+
+ filterNormalNamespace(namespaceList)
+
+ r.processNamespaceList(namespaceList)
+}
+
+func filterNormalNamespace(namespaceList *corev1.NamespaceList) {
+ items := make([]corev1.Namespace, 0, len(namespaceList.Items))
+ for i := range namespaceList.Items {
+ debtStatus := ""
+ anno := namespaceList.Items[i].Annotations
+ if anno != nil {
+ debtStatus = anno[types.DebtNamespaceAnnoStatusKey]
+ }
+ if debtStatus == types.SuspendDebtNamespaceAnnoStatus ||
+ debtStatus == types.SuspendCompletedDebtNamespaceAnnoStatus ||
+ debtStatus == types.FinalDeletionDebtNamespaceAnnoStatus ||
+ debtStatus == types.FinalDeletionCompletedDebtNamespaceAnnoStatus {
+ continue
+ }
+ items = append(items, namespaceList.Items[i])
+ }
+ namespaceList.Items = items
+ logger.Info(
+ "filter normal namespace",
+ "namespaceList len",
+ len(namespaceList.Items),
+ "time",
+ time.Now().Format(time.RFC3339),
+ )
+}
+
+func (r *MonitorReconciler) processNamespaceList(namespaceList *corev1.NamespaceList) {
+ logger.Info(
+ "start processNamespaceList",
+ "namespaceList len",
+ len(namespaceList.Items),
+ "time",
+ time.Now().Format(time.RFC3339),
+ )
+ if len(namespaceList.Items) == 0 {
+ r.Error(errors.New("no namespace to process"), "")
+ return
+ }
+ if err := r.preMonitorResourceUsage(); err != nil {
+ r.Error(err, "failed to pre monitor resource usage")
+ }
+ sem := semaphore.NewWeighted(concurrentLimit)
+ wg := sync.WaitGroup{}
+ wg.Add(len(namespaceList.Items))
+ for i := range namespaceList.Items {
+ go func(namespace *corev1.Namespace) {
+ defer wg.Done()
+ if err := sem.Acquire(context.Background(), 1); err != nil {
+ fmt.Printf("Failed to acquire semaphore: %v\n", err)
+ return
+ }
+ defer sem.Release(1)
+ if err := r.monitorResourceUsage(namespace); err != nil {
+ r.Error(err, "monitor pod resource", "namespace", namespace.Name)
+ }
+ }(&namespaceList.Items[i])
+ }
+ wg.Wait()
+ if err := r.monitorObjectStorageTraffic(); err != nil {
+ r.Error(err, "failed to monitor object storage traffic")
+ }
+ logger.Info("end processNamespaceList", "time", time.Now().Format(time.DateTime))
+}
+
+func (r *MonitorReconciler) preMonitorResourceUsage() error {
+ if r.ObjStorageMetricsClient != nil {
+ metrics, err := objstorage.QueryUserUsageAndTraffic(r.ObjStorageMetricsClient)
+ if err != nil {
+ r.lastObjectMetrics = r.currentObjectMetrics
+ return fmt.Errorf("failed to query object storage metrics: %w", err)
+ }
+ if r.currentObjectMetrics != nil {
+ r.lastObjectMetrics = r.currentObjectMetrics
+ } else {
+ latestObjTrafficSentMetrics := make(objstorage.Metrics)
+ startTime, endTime := time.Now().UTC().Add(-time.Hour), time.Now().UTC()
+ traffic, err := r.DBClient.GetAllLatestObjTraffic(startTime, endTime)
+ if err != nil {
+ return fmt.Errorf("failed to get all latest object storage traffic: %w", err)
+ }
+ for i := range traffic {
+ user := traffic[i].User
+ bucket := traffic[i].Bucket
+ if _, ok := metrics[user]; !ok {
+ continue
+ }
+
+ if traffic[i].Time.Before(time.Now().Add(-time.Hour)) {
+ continue
+ }
+
+ if _, ok := latestObjTrafficSentMetrics[user]; !ok {
+ latestObjTrafficSentMetrics[user] = objstorage.MetricData{
+ Sent: make(map[string]int64),
+ }
+ }
+
+ latestObjTrafficSentMetrics[user].Sent[bucket] = traffic[i].TotalSent
+ }
+ r.lastObjectMetrics = latestObjTrafficSentMetrics
+ }
+ r.currentObjectMetrics = metrics
+ logger.Info(
+ "success query object storage usage and traffic metrics",
+ "time",
+ time.Now().Format(time.DateTime),
+ )
+ }
+ return nil
+}
+
+func (r *MonitorReconciler) monitorResourceUsage(namespace *corev1.Namespace) error {
+ timeStamp := time.Now().UTC()
+ resUsed := map[string]map[corev1.ResourceName]*quantity{}
+ resNamed := make(map[string]*resources.ResourceNamed)
+ instances, err := r.getInstances(namespace.Name)
+ if err != nil {
+ return fmt.Errorf("failed to get instances: %w", err)
+ }
+ if err := r.monitorPodResourceUsage(namespace.Name, resUsed, resNamed, instances); err != nil {
+ return fmt.Errorf("failed to monitor pod resource usage: %w", err)
+ }
+
+ if err := r.monitorPVCResourceUsage(namespace.Name, resUsed, resNamed, instances); err != nil {
+ return fmt.Errorf("failed to monitor PVC resource usage: %w", err)
+ }
+
+ if err := r.monitorDatabaseBackupUsage(namespace.Name, resUsed, resNamed); err != nil {
+ return fmt.Errorf("failed to monitor backup resource usage: %w", err)
+ }
+
+ if err := r.monitorServiceResourceUsage(
+ namespace.Name,
+ resUsed,
+ resNamed,
+ instances,
+ ); err != nil {
+ return fmt.Errorf("failed to monitor service resource usage: %w", err)
+ }
+
+ r.monitorObjectStorageUsage(namespace.Name, resUsed, resNamed)
+
+ monitors := make([]*resources.Monitor, 0, len(resUsed))
+
+ for name, podResource := range resUsed {
+ isEmpty, used := r.getResourceUsed(podResource)
+ if isEmpty {
+ continue
+ }
+ monitors = append(monitors, &resources.Monitor{
+ Category: namespace.Name,
+ Used: used,
+ Time: timeStamp,
+ Type: resNamed[name].Type(),
+ Name: resNamed[name].Name(),
+ ParentType: resNamed[name].ParentType(),
+ ParentName: resNamed[name].ParentName(),
+ })
+ }
+ return r.DBClient.InsertMonitor(context.Background(), monitors...)
+}
+
+func (r *MonitorReconciler) getInstances(namespace string) (map[string]struct{}, error) {
+ instances := make(map[string]struct{})
+ insList := metav1.PartialObjectMetadataList{}
+ insList.SetGroupVersionKind(appv1.GroupVersion.WithKind("InstanceList"))
+ if err := r.cache.List(
+ context.Background(),
+ &insList,
+ client.InNamespace(namespace),
+ ); err != nil {
+ return nil, fmt.Errorf("failed to list instances: %w", err)
+ }
+ for i := range insList.Items {
+ name := insList.Items[i].Labels[resources.AppStoreDeployLabelKey]
+ if name == "" {
+ name = insList.Items[i].Name
+ }
+ instances[name] = struct{}{}
+ }
+ return instances, nil
+}
+
+func (r *MonitorReconciler) monitorPodResourceUsage(
+ namespace string,
+ resUsed map[string]map[corev1.ResourceName]*quantity,
+ resNamed map[string]*resources.ResourceNamed,
+ instances map[string]struct{},
+) error {
+ podList := &corev1.PodList{}
+ if err := r.cache.List(context.Background(), podList, &client.ListOptions{
+ Namespace: namespace,
+ }); err != nil {
+ return fmt.Errorf("failed to list pods: %w", err)
+ }
+
+ knownCardResources := r.getGPUCardResources()
+ for i := range podList.Items {
+ pod := &podList.Items[i]
+ if pod.Spec.NodeName == "" ||
+ pod.Status.Phase == corev1.PodSucceeded &&
+ time.Since(pod.Status.StartTime.Time) > 1*time.Minute {
+ continue
+ }
+ podResNamed := resources.NewResourceNamed(pod)
+ podResNamed.SetInstanceParent(instances)
+ resNamed[podResNamed.String()] = podResNamed
+ if resUsed[podResNamed.String()] == nil {
+ resUsed[podResNamed.String()] = initResources()
+ }
+ usesGPU := podUsesGPU(pod, knownCardResources)
+ var aliasKey string
+ var cardResource corev1.ResourceName
+ if usesGPU {
+ var err error
+ aliasKey, cardResource, err = r.getGPUConfigForNode(pod.Spec.NodeName)
+ if err != nil {
+ r.Error(
+ err,
+ "get gpu config failed",
+ "pod",
+ pod.Name,
+ "namespace",
+ pod.Namespace,
+ "node",
+ pod.Spec.NodeName,
+ )
+ }
+ }
+ podEphemeralStorage := resource.NewQuantity(0, resource.BinarySI)
+ // skip pods that do not start for more than 1 minute
+ skip := pod.Status.Phase != corev1.PodRunning &&
+ (pod.Status.StartTime == nil || time.Since(pod.Status.StartTime.Time) > 1*time.Minute)
+ for _, container := range pod.Spec.Containers {
+ // gpu only use limit and not ignore pod pending status
+ if usesGPU && cardResource != "" {
+ if gpuRequest, ok := container.Resources.Limits[cardResource]; ok {
+ if err := r.getGPUResourceUsage(
+ pod,
+ aliasKey,
+ gpuRequest,
+ resUsed[podResNamed.String()],
+ ); err != nil {
+ r.Error(err, "get gpu resource usage failed", "pod", pod.Name)
+ }
+ }
+ }
+ if skip {
+ continue
+ }
+ if cpuRequest, ok := container.Resources.Limits[corev1.ResourceCPU]; ok {
+ resUsed[podResNamed.String()][corev1.ResourceCPU].Add(cpuRequest)
+ } else {
+ resUsed[podResNamed.String()][corev1.ResourceCPU].Add(
+ container.Resources.Requests[corev1.ResourceCPU],
+ )
+ }
+ if memoryRequest, ok := container.Resources.Limits[corev1.ResourceMemory]; ok {
+ resUsed[podResNamed.String()][corev1.ResourceMemory].Add(memoryRequest)
+ } else {
+ resUsed[podResNamed.String()][corev1.ResourceMemory].Add(
+ container.Resources.Requests[corev1.ResourceMemory],
+ )
+ }
+ if ephemeralRequest, ok := container.Resources.Limits[corev1.ResourceEphemeralStorage]; ok {
+ podEphemeralStorage.Add(ephemeralRequest)
+ } else {
+ podEphemeralStorage.Add(
+ container.Resources.Requests[corev1.ResourceEphemeralStorage],
+ )
+ }
+ }
+ if !skip && podEphemeralStorage.Cmp(ephemeralStorageChargeThreshold) == 1 {
+ podEphemeralStorage.Sub(ephemeralStorageChargeThreshold)
+ resUsed[podResNamed.String()][corev1.ResourceStorage].Add(*podEphemeralStorage)
+ }
+ }
+ return nil
+}
+
+func (r *MonitorReconciler) monitorPVCResourceUsage(
+ namespace string,
+ resUsed map[string]map[corev1.ResourceName]*quantity,
+ resNamed map[string]*resources.ResourceNamed,
+ instances map[string]struct{},
+) error {
+ pvcList := &corev1.PersistentVolumeClaimList{}
+ if err := r.cache.List(context.Background(), pvcList, &client.ListOptions{
+ Namespace: namespace,
+ FieldSelector: fields.OneTermEqualSelector(
+ resourcecache.PersistentVolumeClaimPhaseKey,
+ string(corev1.ClaimBound),
+ ),
+ }); err != nil {
+ return fmt.Errorf("failed to list pvc: %w", err)
+ }
+ for i := range pvcList.Items {
+ pvc := &pvcList.Items[i]
+ if len(pvc.OwnerReferences) > 0 && pvc.OwnerReferences[0].Kind == "BackupRepo" {
+ continue
+ }
+ pvcRes := resources.NewResourceNamed(pvc)
+ pvcRes.SetInstanceParent(instances)
+ if resUsed[pvcRes.String()] == nil {
+ resNamed[pvcRes.String()] = pvcRes
+ resUsed[pvcRes.String()] = initResources()
+ }
+ resUsed[pvcRes.String()][corev1.ResourceStorage].Add(
+ pvc.Spec.Resources.Requests[corev1.ResourceStorage],
+ )
+ }
+ return nil
+}
+
+func (r *MonitorReconciler) monitorDatabaseBackupUsage(
+ namespace string,
+ resUsed map[string]map[corev1.ResourceName]*quantity,
+ resNamed map[string]*resources.ResourceNamed,
+) error {
+ backupList := &kbv1alpha1.BackupList{}
+ if err := r.cache.List(context.Background(), backupList, &client.ListOptions{
+ Namespace: namespace,
+ FieldSelector: fields.OneTermEqualSelector(
+ resourcecache.BackupPhaseKey,
+ string(kbv1alpha1.BackupPhaseCompleted),
+ ),
+ }); err != nil {
+ return fmt.Errorf("failed to list backup: %w", err)
+ }
+ if len(backupList.Items) == 0 {
+ return nil
+ }
+ for i := range backupList.Items {
+ backup := &backupList.Items[i]
+ backupRes := resources.NewResourceNamed(backup)
+ // fmt.Printf("backup name: %v, backup size: %v, backupRes: %s \n", backupList.Items[i].Name, backupList.Items[i].Status.TotalSize, backupRes.String())
+ if resUsed[backupRes.String()] == nil {
+ resNamed[backupRes.String()] = backupRes
+ resUsed[backupRes.String()] = initResources()
+ }
+ resUsed[backupRes.String()][corev1.ResourceStorage].Add(
+ resource.MustParse(backup.Status.TotalSize),
+ )
+ }
+ return nil
+}
+
+// instance is the app instance name
+func (r *MonitorReconciler) monitorServiceResourceUsage(
+ namespace string,
+ resUsed map[string]map[corev1.ResourceName]*quantity,
+ resNamed map[string]*resources.ResourceNamed,
+ instances map[string]struct{},
+) error {
+ svcList := &corev1.ServiceList{}
+ if err := r.cache.List(context.Background(), svcList, &client.ListOptions{
+ Namespace: namespace,
+ FieldSelector: fields.OneTermEqualSelector(
+ resourcecache.ServiceTypeKey,
+ string(corev1.ServiceTypeNodePort),
+ ),
+ }); err != nil {
+ return fmt.Errorf("failed to list svc: %w", err)
+ }
+ for i := range svcList.Items {
+ svc := &svcList.Items[i]
+ if len(svc.Spec.Ports) == 0 {
+ continue
+ }
+ port := make(map[int32]struct{})
+ for _, svcPort := range svc.Spec.Ports {
+ port[svcPort.NodePort] = struct{}{}
+ }
+ svcRes := resources.NewResourceNamed(svc)
+ svcRes.SetInstanceParent(instances)
+ if resUsed[svcRes.String()] == nil {
+ resNamed[svcRes.String()] = svcRes
+ resUsed[svcRes.String()] = initResources()
+ }
+ // nodeport 1:1000, the measurement is quantity 1000
+ resUsed[svcRes.String()][corev1.ResourceServicesNodePorts].Add(
+ *resource.NewQuantity(int64(1000*len(port)), resource.BinarySI),
+ )
+ }
+ return nil
+}
+
+func (r *MonitorReconciler) getResourceUsed(
+ podResource map[corev1.ResourceName]*quantity,
+) (bool, map[uint8]int64) {
+ used := map[uint8]int64{}
+ isEmpty := true
+ for i := range podResource {
+ if podResource[i].MilliValue() == 0 {
+ continue
+ }
+ isEmpty = false
+ if pType, ok := r.Properties.StringMap[i.String()]; ok {
+ used[pType.Enum] = int64(
+ math.Ceil(float64(podResource[i].MilliValue()) / float64(pType.Unit.MilliValue())),
+ )
+ continue
+ }
+ r.Error(errors.New("not found resource type"), "", "resource", i.String())
+ }
+ return isEmpty, used
+}
+
+func (r *MonitorReconciler) monitorObjectStorageUsage(
+ namespace string,
+ resMap map[string]map[corev1.ResourceName]*quantity,
+ namedMap map[string]*resources.ResourceNamed,
+) {
+ username := config.GetUserNameByNamespace(namespace)
+ if r.currentObjectMetrics == nil || r.currentObjectMetrics[username].Usage == nil {
+ return
+ }
+ for bucket, usage := range r.currentObjectMetrics[username].Usage {
+ if bucket == "" || usage <= 0 {
+ continue
+ }
+ objStorageNamed := resources.NewObjStorageResourceNamed(bucket)
+ namedMap[objStorageNamed.String()] = objStorageNamed
+ if _, ok := resMap[objStorageNamed.String()]; !ok {
+ resMap[objStorageNamed.String()] = initResources()
+ }
+ resMap[objStorageNamed.String()][corev1.ResourceStorage].Add(
+ *resource.NewQuantity(usage, resource.BinarySI),
+ )
+ }
+}
+
+func (r *MonitorReconciler) monitorObjectStorageTraffic() error {
+ if r.currentObjectMetrics == nil {
+ return nil
+ }
+ var objTraffic []*types.ObjectStorageTraffic
+ now := time.Now().UTC()
+ for user, metric := range r.currentObjectMetrics {
+ if len(metric.Sent) == 0 {
+ continue
+ }
+ for bucket, m := range metric.Sent {
+ sent := int64(0)
+ if r.lastObjectMetrics != nil && r.lastObjectMetrics[user].Sent != nil {
+ if _, ok := r.lastObjectMetrics[user].Sent[bucket]; ok {
+ if m == -1 {
+ r.currentObjectMetrics[user].Sent[bucket] = r.lastObjectMetrics[user].Sent[bucket]
+ m = r.lastObjectMetrics[user].Sent[bucket]
+ } else {
+ ss := m - r.lastObjectMetrics[user].Sent[bucket]
+ if ss > 0 {
+ sent = ss
+ }
+ }
+ }
+ }
+ objTraffic = append(objTraffic, &types.ObjectStorageTraffic{
+ Time: now,
+ User: user,
+ Bucket: bucket,
+ TotalSent: m,
+ Sent: sent,
+ })
+ }
+ }
+ if len(objTraffic) != 0 {
+ if err := r.DBClient.SaveObjTraffic(objTraffic...); err != nil {
+ return fmt.Errorf("failed to save object storage traffic: %w", err)
+ }
+ }
+ return nil
+}
+
+func (r *MonitorReconciler) MonitorTrafficUsed(startTime, endTime time.Time) error {
+ logger.Info(
+ "start getTrafficUsed",
+ "startTime",
+ startTime.Format(time.RFC3339),
+ "endTime",
+ endTime.Format(time.RFC3339),
+ )
+ execTime := time.Now().UTC()
+ if r.TrafficClient != nil {
+ if err := r.monitorPodTrafficUsed(startTime, endTime); err != nil {
+ r.Error(err, "failed to monitor pod traffic used")
+ }
+ }
+ if r.ObjStorageClient != nil {
+ if err := r.monitorObjectStorageTrafficUsed(startTime, endTime); err != nil {
+ r.Error(err, "failed to monitor object storage traffic used")
+ }
+ }
+ r.Info(
+ "success to monitor pod traffic used",
+ "startTime",
+ startTime.Format(time.RFC3339),
+ "endTime",
+ endTime.Format(time.RFC3339),
+ "execTime",
+ time.Since(execTime).String(),
+ )
+ return nil
+}
+
+func (r *MonitorReconciler) monitorObjectStorageTrafficUsed(startTime, endTime time.Time) error {
+ buckets, err := r.DBClient.GetTimeObjBucketBucket(startTime, endTime)
+ if err != nil {
+ return fmt.Errorf("failed to get object storage buckets: %w", err)
+ }
+ r.Info("object storage buckets", "buckets len", len(buckets))
+ wg, _ := errgroup.WithContext(context.Background())
+ wg.SetLimit(10)
+ for i := range buckets {
+ bucket := buckets[i]
+ if !strings.Contains(bucket, "-") {
+ continue
+ }
+ wg.Go(func() error {
+ return r.handlerObjectStorageTrafficUsed(startTime, endTime, bucket)
+ })
+ }
+ return wg.Wait()
+}
+
+func (r *MonitorReconciler) handlerObjectStorageTrafficUsed(
+ startTime, endTime time.Time,
+ bucket string,
+) error {
+ bytes, err := r.DBClient.HandlerTimeObjBucketSentTraffic(startTime, endTime, bucket)
+ if err != nil {
+ return fmt.Errorf("failed to get object storage flow: %w", err)
+ }
+ // Because the obtained traffic includes traffic communicating with the controller, filter out traffic smaller than 1 MB
+ if bytes < 1024*1024 {
+ return nil
+ }
+ unit := r.Properties.StringMap[resources.ResourceNetwork].Unit
+ used := int64(
+ math.Ceil(
+ float64(
+ resource.NewQuantity(bytes, resource.BinarySI).MilliValue(),
+ ) / float64(
+ unit.MilliValue(),
+ ),
+ ),
+ )
+
+ namespace := "ns-" + strings.SplitN(bucket, "-", 2)[0]
+ ro := resources.Monitor{
+ Category: namespace,
+ Name: bucket,
+ Used: map[uint8]int64{r.Properties.StringMap[resources.ResourceNetwork].Enum: used},
+ Time: endTime.Add(-1 * time.Minute),
+ Type: resources.AppType[resources.ObjectStorage],
+ }
+ r.Info("object storage traffic used", "monitor", ro)
+ err = r.DBClient.InsertMonitor(context.Background(), &ro)
+ if err != nil {
+ return fmt.Errorf("failed to insert monitor: %w", err)
+ }
+ return nil
+}
+
+func (r *MonitorReconciler) monitorPodTrafficUsed(startTime, endTime time.Time) error {
+ monitors, err := r.DBClient.GetDistinctMonitorCombinations(startTime, endTime)
+ if err != nil {
+ return fmt.Errorf("failed to get distinct monitor combinations: %w", err)
+ }
+ r.Info("distinct monitor combinations", "monitors len", len(monitors))
+ wg, _ := errgroup.WithContext(context.Background())
+ wg.SetLimit(100)
+ for i := range monitors {
+ monitor := monitors[i]
+ wg.Go(func() error {
+ return r.handlerTrafficUsed(startTime, endTime, monitor)
+ })
+ }
+ return wg.Wait()
+}
+
+func (r *MonitorReconciler) handlerTrafficUsed(
+ startTime, endTime time.Time,
+ monitor resources.Monitor,
+) error {
+ bytes, err := r.TrafficClient.GetTrafficSentBytes(
+ startTime,
+ endTime,
+ monitor.Category,
+ monitor.Type,
+ monitor.Name,
+ )
+ if err != nil {
+ return fmt.Errorf("failed to get traffic sent bytes: %w", err)
+ }
+ unit := r.Properties.StringMap[resources.ResourceNetwork].Unit
+ used := int64(
+ math.Ceil(
+ float64(
+ resource.NewQuantity(bytes, resource.BinarySI).MilliValue(),
+ ) / float64(
+ unit.MilliValue(),
+ ),
+ ),
+ )
+ if used == 0 {
+ return nil
+ }
+ // logger.Info("traffic used ", "monitor", monitor, "used", used, "unit", unit, "bytes", bytes)
+ ro := resources.Monitor{
+ Category: monitor.Category,
+ Name: monitor.Name,
+ Used: map[uint8]int64{r.Properties.StringMap[resources.ResourceNetwork].Enum: used},
+ Time: endTime.Add(-1 * time.Minute),
+ Type: monitor.Type,
+ }
+ err = r.DBClient.InsertMonitor(context.Background(), &ro)
+ if err != nil {
+ return fmt.Errorf("failed to insert monitor: %w", err)
+ }
+ return nil
+}
+
+func (r *MonitorReconciler) refreshGPUConfig(ctx context.Context) error {
+ configmap := &corev1.ConfigMap{}
+ if err := r.cache.Get(ctx, client.ObjectKey{
+ Namespace: gpu.NodeInfoConfigmapNamespace,
+ Name: gpu.NodeInfoConfigmapName,
+ }, configmap); err != nil {
+ return err
+ }
+ aliasRaw := strings.TrimSpace(configmap.Data[gpuAliasConfigKey])
+ gpuRaw := strings.TrimSpace(configmap.Data[gpuInfoConfigKey])
+ if aliasRaw == "" || gpuRaw == "" {
+ return fmt.Errorf(
+ "gpu configmap %s/%s missing data",
+ gpu.NodeInfoConfigmapNamespace,
+ gpu.NodeInfoConfigmapName,
+ )
+ }
+ aliasMap := make(map[string]gpuAliasConfig)
+ if err := json.Unmarshal([]byte(aliasRaw), &aliasMap); err != nil {
+ return fmt.Errorf("unmarshal gpu alias failed: %w", err)
+ }
+ gpuMap := make(map[string]gpuNodeConfig)
+ if err := json.Unmarshal([]byte(gpuRaw), &gpuMap); err != nil {
+ return fmt.Errorf("unmarshal gpu info failed: %w", err)
+ }
+ aliasCard := make(map[string]corev1.ResourceName, len(aliasMap))
+ for aliasKey, alias := range aliasMap {
+ if alias.Resource.Card == "" {
+ continue
+ }
+ aliasCard[aliasKey] = corev1.ResourceName(alias.Resource.Card)
+ }
+ nodeAlias := make(map[string]string, len(gpuMap))
+ for nodeName, info := range gpuMap {
+ ref := strings.TrimSpace(info.Ref)
+ if ref == "" {
+ ref = strings.TrimSpace(info.Product)
+ }
+ if ref == "" {
+ continue
+ }
+ nodeAlias[nodeName] = ref
+ }
+ r.gpuMutex.Lock()
+ r.gpuAliasCard = aliasCard
+ r.gpuNodeAlias = nodeAlias
+ r.gpuMutex.Unlock()
+ return nil
+}
+
+func (r *MonitorReconciler) getGPUConfigForNode(
+ nodeName string,
+) (string, corev1.ResourceName, error) {
+ r.gpuMutex.RLock()
+ aliasKey, ok := r.gpuNodeAlias[nodeName]
+ cardResource := r.gpuAliasCard[aliasKey]
+ r.gpuMutex.RUnlock()
+ if ok && cardResource != "" {
+ return aliasKey, cardResource, nil
+ }
+ if err := r.refreshGPUConfig(context.Background()); err != nil {
+ return "", "", err
+ }
+ r.gpuMutex.RLock()
+ aliasKey, ok = r.gpuNodeAlias[nodeName]
+ cardResource = r.gpuAliasCard[aliasKey]
+ r.gpuMutex.RUnlock()
+ if !ok || cardResource == "" {
+ return "", "", fmt.Errorf("node %s not found gpu config", nodeName)
+ }
+ return aliasKey, cardResource, nil
+}
+
+func (r *MonitorReconciler) getGPUCardResources() []corev1.ResourceName {
+ r.gpuMutex.RLock()
+ defer r.gpuMutex.RUnlock()
+ cardResources := make([]corev1.ResourceName, 0, len(r.gpuAliasCard))
+ for _, card := range r.gpuAliasCard {
+ if card == "" {
+ continue
+ }
+ cardResources = append(cardResources, card)
+ }
+ return cardResources
+}
+
+func podUsesGPU(pod *corev1.Pod, cardResources []corev1.ResourceName) bool {
+ if len(cardResources) == 0 {
+ return false
+ }
+ for _, container := range pod.Spec.Containers {
+ for _, card := range cardResources {
+ if _, ok := container.Resources.Limits[card]; ok {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func (r *MonitorReconciler) getGPUResourceUsage(
+ pod *corev1.Pod,
+ aliasKey string,
+ gpuReq resource.Quantity,
+ rs map[corev1.ResourceName]*quantity,
+) (err error) {
+ if aliasKey == "" {
+ return errors.New("gpu alias is empty")
+ }
+ gpuResource := resources.NewGpuResource(aliasKey)
+ if _, ok := rs[gpuResource]; !ok {
+ rs[gpuResource] = initGpuResources()
+ }
+ logger.Info(
+ "gpu request",
+ "pod",
+ pod.Name,
+ "namespace",
+ pod.Namespace,
+ "gpu req",
+ gpuReq.String(),
+ "node",
+ pod.Spec.NodeName,
+ "gpu alias",
+ aliasKey,
+ )
+ rs[gpuResource].Add(gpuReq)
+ return nil
+}
+
+func initResources() (rs map[corev1.ResourceName]*quantity) {
+ rs = make(map[corev1.ResourceName]*quantity)
+ rs[resources.ResourceGPU] = initGpuResources()
+ rs[corev1.ResourceCPU] = &quantity{
+ Quantity: resource.NewQuantity(0, resource.DecimalSI),
+ detail: "",
+ }
+ rs[corev1.ResourceMemory] = &quantity{
+ Quantity: resource.NewQuantity(0, resource.BinarySI),
+ detail: "",
+ }
+ rs[corev1.ResourceStorage] = &quantity{
+ Quantity: resource.NewQuantity(0, resource.BinarySI),
+ detail: "",
+ }
+ rs[resources.ResourceNetwork] = &quantity{
+ Quantity: resource.NewQuantity(0, resource.BinarySI),
+ detail: "",
+ }
+ rs[corev1.ResourceServicesNodePorts] = &quantity{
+ Quantity: resource.NewQuantity(0, resource.DecimalSI),
+ detail: "",
+ }
+ return rs
+}
+
+func initGpuResources() *quantity {
+ return &quantity{Quantity: resource.NewQuantity(0, resource.DecimalSI), detail: ""}
+}
+
+func (r *MonitorReconciler) DropMonitorCollectionOlder() error {
+ return r.DBClient.DropMonitorCollectionsOlderThan(30)
+}
diff --git a/controllers/resources/controllers/monitor_controller_test.go b/controllers/resources/controllers/monitor_controller_test.go
new file mode 100644
index 000000000000..59ca1fac3c03
--- /dev/null
+++ b/controllers/resources/controllers/monitor_controller_test.go
@@ -0,0 +1,15 @@
+// Copyright © 2023 sealos.
+//
+// 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 controllers
diff --git a/controllers/resources/controllers/network_controller.go b/controllers/resources/controllers/network_controller.go
new file mode 100644
index 000000000000..fb0beede7a19
--- /dev/null
+++ b/controllers/resources/controllers/network_controller.go
@@ -0,0 +1,485 @@
+/*
+Copyright 2025.
+
+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 controllers
+
+import (
+ "context"
+ stderrors "errors"
+ "fmt"
+ "reflect"
+
+ "github.com/go-logr/logr"
+ corev1 "k8s.io/api/core/v1"
+ networkingv1 "k8s.io/api/networking/v1"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/util/workqueue"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+// NetworkReconciler reconciles Namespace, Ingress, and Service objects to manage network traffic
+type NetworkReconciler struct {
+ Client client.Client
+ Log logr.Logger
+}
+
+const (
+ NetworkStatusAnnoKey = "network.sealos.io/status"
+ NetworkSuspend = "Suspend"
+ NetworkResume = "Resume"
+ NetworkResumeCompleted = "ResumeCompleted"
+ NodePortLabelKey = "network.sealos.io/original-nodeport"
+ IngressClassKey = "kubernetes.io/ingress.class"
+
+ Disable = "disable"
+ True = "true"
+)
+
+//+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;update;patch
+//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;update;patch
+//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;update;patch
+
+func (r *NetworkReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ logger := r.Log.WithValues("Namespace", req.Namespace, "Name", req.NamespacedName)
+
+ logger.Info("Reconciling Network")
+ // Fetch the namespace
+ ns := corev1.Namespace{}
+ keyObj := client.ObjectKey{Name: req.Namespace}
+ if req.Namespace == "" && req.Name != "" {
+ keyObj = client.ObjectKey{Name: req.Name}
+ }
+ if err := r.Client.Get(ctx, keyObj, &ns); err != nil {
+ return ctrl.Result{}, client.IgnoreNotFound(err)
+ }
+
+ // Skip if namespace is terminating
+ if ns.Status.Phase == corev1.NamespaceTerminating {
+ logger.Info("namespace is terminating")
+ return ctrl.Result{}, nil
+ }
+
+ // Check network status annotation
+ networkStatus, ok := ns.Annotations[NetworkStatusAnnoKey]
+ if !ok {
+ logger.Info("no network status annotation found")
+ return ctrl.Result{}, nil
+ }
+
+ logger.Info("network status", "status", networkStatus)
+
+ // Skip completed state
+ if networkStatus == NetworkResumeCompleted {
+ logger.Info("skipping completed network status")
+ return ctrl.Result{}, nil
+ }
+
+ switch networkStatus {
+ case NetworkSuspend:
+ // If NamespacedName.Namespace is empty, then req is the namespace itself, and req.namespacedname.name is the Name of the namespace
+ if req.Namespace == "" {
+ // Handle namespace suspension
+ if err := r.suspendNetworkResources(ctx, req.Name); err != nil {
+ logger.Error(err, "failed to suspend network resources")
+ return ctrl.Result{}, err
+ }
+ break
+ }
+ if err := r.handleResource(ctx, req.NamespacedName, ns); err != nil {
+ logger.Error(err, "failed to handle resource")
+ return ctrl.Result{}, err
+ }
+ return ctrl.Result{}, nil
+ case NetworkResume:
+ namespace := req.Namespace
+ if req.Namespace == "" {
+ namespace = req.Name
+ }
+ // Handle namespace resumption
+ if err := r.resumeNetworkResources(ctx, namespace); err != nil {
+ logger.Error(err, "failed to resume network resources")
+ return ctrl.Result{}, err
+ }
+ // Update namespace status
+ if ns.Annotations == nil {
+ ns.Annotations = make(map[string]string)
+ }
+ ns.Annotations[NetworkStatusAnnoKey] = NetworkResumeCompleted
+ if err := r.Client.Update(ctx, &ns); err != nil {
+ logger.Error(err, "failed to update namespace network status to ResumeCompleted")
+ return ctrl.Result{}, err
+ }
+ default:
+ logger.Error(stderrors.New("unknown network status"), "", "status", networkStatus)
+ }
+
+ return ctrl.Result{}, nil
+}
+
+func (r *NetworkReconciler) handleResource(
+ ctx context.Context,
+ key client.ObjectKey,
+ ns corev1.Namespace,
+) error {
+ // Only process resources in suspended namespaces
+ networkStatus, ok := ns.Annotations[NetworkStatusAnnoKey]
+ if !ok || networkStatus != NetworkSuspend {
+ return nil
+ }
+
+ // Try fetching as Ingress
+ ingress := networkingv1.Ingress{}
+ if err := r.Client.Get(ctx, key, &ingress); err == nil {
+ if ingress.Annotations == nil {
+ ingress.Annotations = make(map[string]string)
+ }
+ if ingress.Annotations[IngressClassKey] != Disable {
+ ingress.Annotations[IngressClassKey] = Disable
+ if err := r.Client.Update(ctx, &ingress); err != nil {
+ return fmt.Errorf("failed to suspend ingress %s: %w", key.Name, err)
+ }
+ r.Log.V(1).Info("Suspended ingress", "name", key.Name)
+ }
+ return nil
+ } else if !errors.IsNotFound(err) {
+ return fmt.Errorf("failed to get ingress %s: %w", key.Name, err)
+ }
+
+ // Try fetching as Service
+ svc := corev1.Service{}
+ if err := r.Client.Get(ctx, key, &svc); err == nil {
+ if svc.Spec.Type == corev1.ServiceTypeNodePort &&
+ (svc.Labels == nil || svc.Labels[NodePortLabelKey] != True) {
+ if svc.Labels == nil {
+ svc.Labels = make(map[string]string)
+ }
+ svc.Labels[NodePortLabelKey] = True
+ svc.Spec.Type = corev1.ServiceTypeClusterIP
+ if err := r.Client.Update(ctx, &svc); err != nil {
+ return fmt.Errorf("failed to suspend service %s: %w", key.Name, err)
+ }
+ r.Log.V(1).Info("Suspended service", "name", key.Name)
+ }
+ return nil
+ } else if !errors.IsNotFound(err) {
+ return fmt.Errorf("failed to get service %s: %w", key.Name, err)
+ }
+
+ return nil
+}
+
+func (r *NetworkReconciler) suspendNetworkResources(ctx context.Context, namespace string) error {
+ // Suspend Ingresses
+ ingressList := networkingv1.IngressList{}
+ if err := r.Client.List(ctx, &ingressList, client.InNamespace(namespace)); err != nil {
+ return fmt.Errorf("failed to list ingresses in namespace %s: %w", namespace, err)
+ }
+ for _, ingress := range ingressList.Items {
+ if ingress.Annotations == nil {
+ ingress.Annotations = make(map[string]string)
+ }
+ if ingress.Annotations[IngressClassKey] != Disable {
+ ingress.Annotations[IngressClassKey] = Disable
+ if err := r.Client.Update(ctx, &ingress); err != nil {
+ return fmt.Errorf("failed to suspend ingress %s: %w", ingress.Name, err)
+ }
+ r.Log.V(1).Info("Suspended ingress", "name", ingress.Name)
+ }
+ }
+
+ // Suspend NodePort Services
+ serviceList := corev1.ServiceList{}
+ if err := r.Client.List(ctx, &serviceList, client.InNamespace(namespace)); err != nil {
+ return fmt.Errorf("failed to list services in namespace %s: %w", namespace, err)
+ }
+ for _, svc := range serviceList.Items {
+ if svc.Spec.Type != corev1.ServiceTypeNodePort {
+ continue
+ }
+ if svc.Labels == nil {
+ svc.Labels = make(map[string]string)
+ }
+ svc.Labels[NodePortLabelKey] = True
+ svc.Spec.Type = corev1.ServiceTypeClusterIP
+ if err := r.Client.Update(ctx, &svc); err != nil {
+ return fmt.Errorf("failed to suspend service %s: %w", svc.Name, err)
+ }
+ r.Log.V(1).Info("Suspended service", "name", svc.Name)
+ }
+
+ return nil
+}
+
+func (r *NetworkReconciler) resumeNetworkResources(ctx context.Context, namespace string) error {
+ // Resume Ingresses
+ ingressList := networkingv1.IngressList{}
+ if err := r.Client.List(ctx, &ingressList, client.InNamespace(namespace)); err != nil {
+ return fmt.Errorf("failed to list ingresses in namespace %s: %w", namespace, err)
+ }
+ for _, ingress := range ingressList.Items {
+ if ingress.Annotations == nil || ingress.Annotations[IngressClassKey] != Disable {
+ continue
+ }
+ ingress.Annotations[IngressClassKey] = "nginx"
+ if err := r.Client.Update(ctx, &ingress); err != nil {
+ return fmt.Errorf("failed to resume ingress %s: %w", ingress.Name, err)
+ }
+ r.Log.V(1).Info("Resumed ingress", "name", ingress.Name)
+ }
+
+ // Resume NodePort Services
+ serviceList := corev1.ServiceList{}
+ if err := r.Client.List(ctx, &serviceList, client.InNamespace(namespace)); err != nil {
+ return fmt.Errorf("failed to list services in namespace %s: %w", namespace, err)
+ }
+ for _, svc := range serviceList.Items {
+ if svc.Labels == nil || svc.Labels[NodePortLabelKey] != True {
+ continue
+ }
+ svc.Spec.Type = corev1.ServiceTypeNodePort
+ delete(svc.Labels, NodePortLabelKey)
+ if err := r.Client.Update(ctx, &svc); err != nil {
+ return fmt.Errorf("failed to resume service %s: %w", svc.Name, err)
+ }
+ r.Log.V(1).Info("Resumed service", "name", svc.Name)
+ }
+
+ return nil
+}
+
+// SuspendedNamespaceHandler enqueues requests for Ingress and Service objects only in suspended namespaces
+type SuspendedNamespaceHandler struct {
+ Client client.Client
+ Logger logr.Logger
+}
+
+func (e *SuspendedNamespaceHandler) Create(
+ ctx context.Context,
+ evt event.TypedCreateEvent[client.Object],
+ q workqueue.TypedRateLimitingInterface[reconcile.Request],
+) {
+ if isNil(evt.Object) {
+ e.Logger.Error(nil, "CreateEvent received with no metadata", "event", evt)
+ return
+ }
+
+ ns := corev1.Namespace{}
+ if err := e.Client.Get(
+ ctx,
+ types.NamespacedName{Name: evt.Object.GetNamespace()},
+ &ns,
+ ); err != nil {
+ e.Logger.Error(err, "failed to get namespace", "namespace", evt.Object.GetNamespace())
+ return
+ }
+
+ networkStatus, ok := ns.Annotations[NetworkStatusAnnoKey]
+ if !ok || networkStatus != NetworkSuspend {
+ return
+ }
+
+ item := reconcile.Request{NamespacedName: types.NamespacedName{
+ Name: evt.Object.GetName(),
+ Namespace: evt.Object.GetNamespace(),
+ }}
+ q.Add(item)
+}
+
+func (e *SuspendedNamespaceHandler) Update(
+ ctx context.Context,
+ evt event.TypedUpdateEvent[client.Object],
+ q workqueue.TypedRateLimitingInterface[reconcile.Request],
+) {
+ switch {
+ case !isNil(evt.ObjectNew):
+ ns := corev1.Namespace{}
+ if err := e.Client.Get(
+ ctx,
+ types.NamespacedName{Name: evt.ObjectNew.GetNamespace()},
+ &ns,
+ ); err != nil {
+ e.Logger.Error(
+ err,
+ "failed to get namespace",
+ "namespace",
+ evt.ObjectNew.GetNamespace(),
+ )
+ return
+ }
+
+ networkStatus, ok := ns.Annotations[NetworkStatusAnnoKey]
+ if !ok || networkStatus != NetworkSuspend {
+ return
+ }
+
+ item := reconcile.Request{NamespacedName: types.NamespacedName{
+ Name: evt.ObjectNew.GetName(),
+ Namespace: evt.ObjectNew.GetNamespace(),
+ }}
+ q.Add(item)
+ case !isNil(evt.ObjectOld):
+ ns := corev1.Namespace{}
+ if err := e.Client.Get(
+ ctx,
+ types.NamespacedName{Name: evt.ObjectOld.GetNamespace()},
+ &ns,
+ ); err != nil {
+ e.Logger.Error(
+ err,
+ "failed to get namespace",
+ "namespace",
+ evt.ObjectOld.GetNamespace(),
+ )
+ return
+ }
+
+ networkStatus, ok := ns.Annotations[NetworkStatusAnnoKey]
+ if !ok || networkStatus != NetworkSuspend {
+ return
+ }
+
+ item := reconcile.Request{NamespacedName: types.NamespacedName{
+ Name: evt.ObjectOld.GetName(),
+ Namespace: evt.ObjectOld.GetNamespace(),
+ }}
+ q.Add(item)
+ default:
+ e.Logger.Error(nil, "UpdateEvent received with no metadata", "event", evt)
+ }
+}
+
+func (e *SuspendedNamespaceHandler) Delete(
+ ctx context.Context,
+ evt event.TypedDeleteEvent[client.Object],
+ q workqueue.TypedRateLimitingInterface[reconcile.Request],
+) {
+ // No action needed for delete events
+}
+
+func (e *SuspendedNamespaceHandler) Generic(
+ ctx context.Context,
+ evt event.TypedGenericEvent[client.Object],
+ q workqueue.TypedRateLimitingInterface[reconcile.Request],
+) {
+ // No action needed for generic events
+}
+
+func isNil(arg any) bool {
+ if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Pointer ||
+ v.Kind() == reflect.Interface ||
+ v.Kind() == reflect.Slice ||
+ v.Kind() == reflect.Map ||
+ v.Kind() == reflect.Chan ||
+ v.Kind() == reflect.Func) && v.IsNil()) {
+ return true
+ }
+ return false
+}
+
+func (r *NetworkReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ r.Log = ctrl.Log.WithName("controllers").WithName("Network")
+ r.Client = mgr.GetClient()
+ suspendedHandler := &SuspendedNamespaceHandler{Client: r.Client, Logger: r.Log}
+
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&corev1.Namespace{}, builder.WithPredicates(NetworkAnnotationPredicate{})).
+ Watches(
+ &networkingv1.Ingress{},
+ suspendedHandler,
+ builder.WithPredicates(predicate.Funcs{
+ CreateFunc: func(e event.CreateEvent) bool {
+ return true
+ },
+ UpdateFunc: func(e event.UpdateEvent) bool {
+ newIngress, ok := e.ObjectNew.(*networkingv1.Ingress)
+ if !ok {
+ return false
+ }
+ return newIngress.Annotations != nil &&
+ newIngress.Annotations[IngressClassKey] != Disable
+ },
+ DeleteFunc: func(e event.DeleteEvent) bool {
+ return false
+ },
+ GenericFunc: func(e event.GenericEvent) bool {
+ return false
+ },
+ }),
+ ).
+ Watches(
+ &corev1.Service{},
+ suspendedHandler,
+ builder.WithPredicates(predicate.Funcs{
+ CreateFunc: func(e event.CreateEvent) bool {
+ svc, ok := e.Object.(*corev1.Service)
+ if !ok {
+ return false
+ }
+ return svc.Spec.Type == corev1.ServiceTypeNodePort
+ },
+ UpdateFunc: func(e event.UpdateEvent) bool {
+ newSvc, ok := e.ObjectNew.(*corev1.Service)
+ if !ok {
+ return false
+ }
+ return newSvc.Spec.Type == corev1.ServiceTypeNodePort &&
+ (newSvc.Labels == nil || newSvc.Labels[NodePortLabelKey] != True)
+ },
+ DeleteFunc: func(e event.DeleteEvent) bool {
+ return false
+ },
+ GenericFunc: func(e event.GenericEvent) bool {
+ return false
+ },
+ }),
+ ).
+ Complete(r)
+}
+
+// NetworkAnnotationPredicate filters namespace events based on network status annotation changes
+type NetworkAnnotationPredicate struct {
+ predicate.Funcs
+}
+
+func (NetworkAnnotationPredicate) Create(e event.CreateEvent) bool {
+ networkStatus, ok := e.Object.GetAnnotations()[NetworkStatusAnnoKey]
+ return ok && networkStatus != NetworkResumeCompleted
+}
+
+func (NetworkAnnotationPredicate) Update(e event.UpdateEvent) bool {
+ oldObj, ok1 := e.ObjectOld.(*corev1.Namespace)
+ newObj, ok2 := e.ObjectNew.(*corev1.Namespace)
+ if !ok1 || !ok2 || newObj.Annotations == nil {
+ return false
+ }
+ oldStatus := oldObj.Annotations[NetworkStatusAnnoKey]
+ newStatus := newObj.Annotations[NetworkStatusAnnoKey]
+ return oldStatus != newStatus && newStatus != NetworkResumeCompleted
+}
+
+func (NetworkAnnotationPredicate) Delete(e event.DeleteEvent) bool {
+ return false
+}
+
+func (NetworkAnnotationPredicate) Generic(e event.GenericEvent) bool {
+ return false
+}
diff --git a/controllers/resources/controllers/quota_controller.go b/controllers/resources/controllers/quota_controller.go
new file mode 100644
index 000000000000..2c44f6d61bdb
--- /dev/null
+++ b/controllers/resources/controllers/quota_controller.go
@@ -0,0 +1,339 @@
+package controllers
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/go-logr/logr"
+ "github.com/labring/sealos/controllers/pkg/types"
+ "github.com/labring/sealos/controllers/pkg/utils/env"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/tools/record"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+)
+
+// NamespaceQuotaReconciler reconciles namespace events and adjusts quotas
+type NamespaceQuotaReconciler struct {
+ client.Client
+ Logger logr.Logger
+ Scheme *runtime.Scheme
+ limitExpansionCycle time.Duration
+ Recorder record.EventRecorder
+ namespaceLocks map[string]*sync.Mutex
+}
+
+// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch
+// +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;patch
+// +kubebuilder:rbac:groups=core,resources=resourcequotas,verbs=get;list;watch;create;update;patch
+
+// Reconcile handles namespace events
+func (r *NamespaceQuotaReconciler) Reconcile(
+ ctx context.Context,
+ req ctrl.Request,
+) (ctrl.Result, error) {
+ evt := &corev1.Event{}
+ if err := r.Get(ctx, req.NamespacedName, evt); err == nil {
+ if strings.Contains(evt.Message, "exceeded quota") &&
+ (evt.Reason == "FailedCreate" || evt.Reason == "Devbox is exceeded quota") {
+ // lock
+ if r.namespaceLocks[evt.Namespace] == nil {
+ r.namespaceLocks[evt.Namespace] = &sync.Mutex{}
+ }
+ // Try to acquire the lock
+ if r.namespaceLocks[evt.Namespace].TryLock() {
+ defer r.namespaceLocks[evt.Namespace].Unlock()
+ } else {
+ r.Logger.Info("Namespace is already being processed", "namespace", evt.Namespace)
+ return ctrl.Result{}, nil
+ }
+ if err := r.handleQuotaExceeded(ctx, evt); err != nil {
+ r.Logger.Error(
+ err,
+ "failed to handle quota exceeded",
+ "namespace",
+ evt.Namespace,
+ "event",
+ evt.Message,
+ )
+ return ctrl.Result{RequeueAfter: 5 * time.Minute}, err
+ }
+ }
+ } else if client.IgnoreNotFound(err) != nil {
+ return ctrl.Result{}, err
+ }
+ return ctrl.Result{}, nil
+}
+
+// handleQuotaExceeded increases quota by 50% if not already increased
+func (r *NamespaceQuotaReconciler) handleQuotaExceeded(
+ ctx context.Context,
+ evt *corev1.Event,
+) error {
+ ns := evt.Namespace
+ quotas := &corev1.ResourceQuotaList{}
+ if err := r.List(ctx, quotas, client.InNamespace(ns)); err != nil {
+ return fmt.Errorf("failed to list ResourceQuotas: %w", err)
+ }
+ if len(quotas.Items) == 0 || len(quotas.Items) > 1 {
+ return nil
+ }
+
+ quota := quotas.Items[0]
+ newQuota := quota.DeepCopy()
+
+ // Check if quota was recently updated
+ if quota.Labels != nil {
+ if lastUpdate, exists := quota.Labels["last-quota-update"]; exists {
+ lastUpdateTime, err := time.Parse("2006-01-02-15-04-05", lastUpdate)
+ if err == nil && time.Since(lastUpdateTime) < r.limitExpansionCycle {
+ r.Logger.Info("Quota was recently updated, skipping update", "namespace", ns)
+ return nil
+ }
+ }
+ }
+
+ if updateRequired := AdjustQuota(newQuota); updateRequired {
+ // Add/update label for last update time
+ if newQuota.Labels == nil {
+ newQuota.Labels = make(map[string]string)
+ }
+ newQuota.Labels["last-quota-update"] = time.Now().UTC().Format("2006-01-02-15-04-05")
+
+ numStr, ok := newQuota.Labels["auto-adapt-quota-num"]
+ if !ok {
+ numStr = "0"
+ }
+ num, err := strconv.Atoi(numStr)
+ if err != nil {
+ r.Logger.Error(err, "failed to parse quota number", "namespace", ns)
+ num = 0
+ }
+ newQuota.Labels["auto-adapt-quota-num"] = strconv.Itoa(num + 1)
+ if err := r.Update(ctx, newQuota); err != nil {
+ return fmt.Errorf("failed to update ResourceQuota %s: %w", newQuota.Name, err)
+ }
+ r.Logger.Info(
+ "Quota updated",
+ "namespace",
+ ns,
+ "history count",
+ num,
+ "newQuota",
+ newQuota.Spec.Hard,
+ )
+ r.Recorder.Event(
+ evt,
+ corev1.EventTypeNormal,
+ "QuotaAdjusted",
+ fmt.Sprintf("Increased quota by 50%% due to event msg: '%s'", evt.Message),
+ )
+ }
+
+ return nil
+}
+
+// getResourceUsage retrieves the used quantity for a given resource from the ResourceQuota status.
+func getResourceUsage(
+ resourceName corev1.ResourceName,
+ status corev1.ResourceQuotaStatus,
+) (resource.Quantity, error) {
+ usedQuantity, exists := status.Used[resourceName]
+ if !exists {
+ return resource.Quantity{}, fmt.Errorf("resource %s not found in status", resourceName)
+ }
+ return usedQuantity, nil
+}
+
+func AdjustQuota(quota *corev1.ResourceQuota) bool {
+ updateRequired := false
+
+ // Define base and upper limits for each resource
+ limits := map[corev1.ResourceName]struct {
+ baseLimit resource.Quantity
+ upperLimit resource.Quantity
+ }{
+ corev1.ResourceLimitsCPU: {
+ baseLimit: resource.MustParse("64"),
+ upperLimit: resource.MustParse("200"),
+ },
+ corev1.ResourceRequestsCPU: {
+ baseLimit: resource.MustParse("64"),
+ upperLimit: resource.MustParse("200"),
+ },
+ corev1.ResourceLimitsMemory: {
+ baseLimit: resource.MustParse("256Gi"),
+ upperLimit: resource.MustParse("1024Gi"),
+ },
+ corev1.ResourceRequestsMemory: {
+ baseLimit: resource.MustParse("256Gi"),
+ upperLimit: resource.MustParse("1024Gi"),
+ },
+ corev1.ResourceRequestsStorage: {
+ baseLimit: resource.MustParse("300Gi"),
+ upperLimit: resource.MustParse("800Gi"),
+ },
+ corev1.ResourceServicesNodePorts: {
+ baseLimit: resource.MustParse("50"),
+ upperLimit: resource.MustParse("200"),
+ },
+ }
+
+ for resourceName, quantity := range quota.Spec.Hard {
+ if resourceName == corev1.ResourceLimitsCPU || resourceName == corev1.ResourceRequestsCPU ||
+ resourceName == corev1.ResourceLimitsMemory || resourceName == corev1.ResourceRequestsMemory ||
+ resourceName == corev1.ResourceRequestsStorage || resourceName == corev1.ResourceServicesNodePorts {
+ limit, exists := limits[resourceName]
+ if !exists {
+ continue
+ }
+
+ // Case 1: Below base limit, double the quota
+ if quantity.Cmp(limit.baseLimit) < 0 {
+ newQuantity := resource.MustParse(
+ fmt.Sprintf("%.0f", float64(quantity.Value())*2),
+ )
+ quota.Spec.Hard[resourceName] = resource.MustParse(
+ formatQuantity(newQuantity, resourceName),
+ )
+ updateRequired = true
+ continue
+ }
+
+ // Case 3: Between base and upper limit, check usage ratio
+ usedQuantity, err := getResourceUsage(resourceName, quota.Status)
+ if err != nil {
+ continue
+ }
+
+ // Calculate usage ratio
+ usageRatio := float64(usedQuantity.Value()) / float64(quantity.Value())
+
+ // Expand quota by 1.5x if usage is above 50%
+ if usageRatio > 0.5 {
+ newQuantity := resource.MustParse(
+ fmt.Sprintf("%.0f", float64(quantity.Value())*1.5),
+ )
+ // Check if new quantity exceeds upper limit
+ if quantity.Cmp(limit.upperLimit) >= 0 {
+ newQuantity = resource.MustParse(
+ fmt.Sprintf("%.0f", float64(quantity.Value())*1.3),
+ )
+ }
+ // Ensure new quantity does not exceed upper limit
+ if newQuantity.Cmp(limit.upperLimit) > 0 {
+ newQuantity = limit.upperLimit
+ }
+ quota.Spec.Hard[resourceName] = resource.MustParse(
+ formatQuantity(newQuantity, resourceName),
+ )
+ updateRequired = true
+ }
+ }
+ }
+
+ return updateRequired
+}
+
+func formatQuantity(quantity resource.Quantity, resourceName corev1.ResourceName) string {
+ switch resourceName {
+ case corev1.ResourceLimitsCPU, corev1.ResourceRequestsCPU:
+ // Use cores if >= 1000m, otherwise use milliCPU
+ if quantity.MilliValue() >= 1000 {
+ return fmt.Sprintf("%.2f", float64(quantity.MilliValue())/1000)
+ }
+ return quantity.String()
+ case corev1.ResourceLimitsMemory, corev1.ResourceRequestsMemory:
+ // Use GiB if >= 1Gi, otherwise use MiB or bytes
+ if quantity.Value() >= 1<<30 { // 1 GiB
+ return fmt.Sprintf("%.0fGi", float64(quantity.Value())/(1<<30))
+ } else if quantity.Value() >= 1<<20 { // 1 MiB
+ return fmt.Sprintf("%.0fMi", float64(quantity.Value())/(1<<20))
+ }
+ return quantity.String()
+ case corev1.ResourceRequestsStorage, corev1.ResourceLimitsEphemeralStorage:
+ // Use GiB if >= 1Gi, otherwise use MiB
+ if quantity.Value() >= 1<<30 { // 1 GiB
+ return fmt.Sprintf("%.0fGi", float64(quantity.Value())/(1<<30))
+ } else if quantity.Value() >= 1<<20 { // 1 MiB
+ return fmt.Sprintf("%.0fMi", float64(quantity.Value())/(1<<20))
+ }
+ return quantity.String()
+ default:
+ return quantity.String()
+ }
+}
+
+// SetupWithManager sets up the controller with the Manager
+func (r *NamespaceQuotaReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ r.Logger = ctrl.Log.WithName("namespace-quota-controller")
+ r.namespaceLocks = make(map[string]*sync.Mutex)
+ r.limitExpansionCycle = env.GetDurationEnvWithDefault(
+ "LIMIT_QUOTA_EXPANSION_CYCLE",
+ 24*time.Hour,
+ )
+
+ checkEventPredicate := func(obj client.Object) bool {
+ eventObj, ok := obj.(*corev1.Event)
+ if !ok ||
+ (eventObj.Reason != "FailedCreate" && eventObj.Reason != "Devbox is exceeded quota") ||
+ !strings.Contains(eventObj.Message, "exceeded quota") ||
+ strings.Contains(eventObj.Message, "debt-limit0") {
+ return false
+ }
+
+ // Get the namespace of the Event
+ nsName := obj.GetNamespace()
+ if !strings.HasPrefix(nsName, "ns-") {
+ return false
+ }
+
+ // Fetch the namespace object to check annotations
+ var ns corev1.Namespace
+ if err := mgr.GetClient().
+ Get(context.Background(), client.ObjectKey{Name: nsName}, &ns); err != nil {
+ r.Logger.Error(err, "Failed to fetch namespace", "namespace", nsName)
+ return false
+ }
+
+ annos := ns.GetAnnotations()
+ if annos != nil {
+ if status, ok := annos[types.DebtNamespaceAnnoStatusKey]; ok &&
+ status != types.NormalDebtNamespaceAnnoStatus {
+ return false
+ }
+ if _, ok := annos[types.WorkspaceSubscriptionStatusAnnoKey]; ok {
+ return false
+ }
+ }
+ return true
+ }
+ // Predicate for filtering Events based on their namespace and reason
+ eventPredicate := predicate.Funcs{
+ CreateFunc: func(e event.CreateEvent) bool {
+ return checkEventPredicate(e.Object)
+ },
+ UpdateFunc: func(e event.UpdateEvent) bool {
+ return checkEventPredicate(e.ObjectNew)
+ },
+ DeleteFunc: func(e event.DeleteEvent) bool {
+ return false // Don't reconcile on delete
+ },
+ GenericFunc: func(e event.GenericEvent) bool {
+ return checkEventPredicate(e.Object)
+ },
+ }
+
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&corev1.Event{}, builder.WithPredicates(eventPredicate)).
+ Complete(r)
+}
diff --git a/controllers/resources/deploy/Kubefile b/controllers/resources/deploy/Kubefile
new file mode 100644
index 000000000000..d08056377d44
--- /dev/null
+++ b/controllers/resources/deploy/Kubefile
@@ -0,0 +1,9 @@
+FROM scratch
+
+USER 65532:65532
+
+COPY registry registry
+COPY charts charts
+COPY resources-controller-entrypoint.sh resources-controller-entrypoint.sh
+
+CMD ["bash resources-controller-entrypoint.sh"]
diff --git a/controllers/resources/deploy/README.md b/controllers/resources/deploy/README.md
new file mode 100644
index 000000000000..560b7f0b8e61
--- /dev/null
+++ b/controllers/resources/deploy/README.md
@@ -0,0 +1,86 @@
+# resources-controller
+
+## 说明
+sealos run 镜像时会在目标节点执行 Kubefile,本镜像通过 Helm 安装/升级 resources controller。
+
+## 必填参数
+
+**无**(默认自动从集群 ConfigMap 读取 MongoDB 配置)
+
+## 默认配置读取规则
+
+当未指定 `MONGO_URI` 环境变量时,系统将按以下顺序自动读取配置:
+
+1. **主数据库**:从 `sealos-system` 命名空间下的 `sealos-config` ConfigMap 读取 `databaseMongodbURI` 字段
+2. **流量数据库**:从 `sealos-system` 命名空间下的 `nm-agent-config` ConfigMap 读取 `MONGO_URI` 字段,若不存在则使用主数据库配置
+
+> 如需禁用自动配置,可设置 `RESOURCES_ENV_AUTO_CONFIG_ENABLED=false`
+
+## 如何运行
+
+```shell
+# 最简配置(自动读取集群 ConfigMap 中的 MongoDB 配置)
+sealos run ghcr.io/labring/sealos-cloud-resources-controller:latest
+```
+
+> 默认使用 mongodb 作为存储,sealos-resources 为数据库名
+
+## 可选参数
+
+- MONGO_URI: MongoDB 连接 URI(包含用户名和密码),若不指定则自动从 ConfigMap 读取
+- TRAFFIC_MONGO_URI: 流量统计 MongoDB 连接 URI,默认与 MONGO_URI 相同
+- RESOURCES_TRAFFICS_SERVICE_CONNECT_ADDRESS: 流量服务连接地址
+- RELEASE_NAMESPACE: Helm 安装命名空间,默认 `resources-system`
+- RELEASE_NAME: Helm release 名称,默认 `resources`
+- HELM_OPTS: 透传 Helm 参数
+- CHART_PATH: Helm chart 路径,默认 `./charts/resources-controller`
+- RESOURCES_ENV_AUTO_CONFIG_ENABLED: 是否自动从 ConfigMap 读取配置,默认 `true`
+- RESOURCES_BACKUP_ENABLED: 是否启用备份,默认 `true`
+
+## 示例
+
+```shell
+# 1. 最简配置(使用集群默认 MongoDB)
+sealos run ghcr.io/labring/sealos-cloud-resources-controller:latest
+
+# 2. 指定自定义 MongoDB
+sealos run ghcr.io/labring/sealos-cloud-resources-controller:latest \
+ --env MONGO_URI="mongodb://user:pass@host:27017/resources?authSource=admin"
+
+# 3. 自定义命名空间和流量 MongoDB
+sealos run ghcr.io/labring/sealos-cloud-resources-controller:latest \
+ --env MONGO_URI="mongodb://user:pass@mongo1:27017/resources?authSource=admin" \
+ --env TRAFFIC_MONGO_URI="mongodb://user:pass@mongo2:27017/traffic?authSource=admin" \
+ --env RELEASE_NAMESPACE="my-resources"
+
+# 4. 使用 HELM_OPTS 自定义资源配置
+sealos run ghcr.io/labring/sealos-cloud-resources-controller:latest \
+ --env MONGO_URI="mongodb://user:pass@host:27017/resources?authSource=admin" \
+ --env HELM_OPTS="--set resources.limits.cpu=2000m --set resources.limits.memory=2048Mi"
+
+# 5. 完整配置示例
+sealos run ghcr.io/labring/sealos-cloud-resources-controller:latest \
+ --env MONGO_URI="mongodb://admin:password123@10.0.0.1:27017/resources?authSource=admin" \
+ --env TRAFFIC_MONGO_URI="mongodb://admin:password123@10.0.0.1:27017/traffic?authSource=admin" \
+ --env RELEASE_NAMESPACE="production" \
+ --env RESOURCES_BACKUP_ENABLED="false" \
+ --env HELM_OPTS="--set replicaCount=2"
+```
+
+## Helm Chart 可配置参数
+
+可通过 `HELM_OPTS` 传递以下参数:
+
+- `replicaCount`: 副本数,默认 `1`
+- `image`: 容器镜像,默认 `ghcr.io/labring/sealos-resources-controller:latest`
+- `imagePullPolicy`: 镜像拉取策略,默认 `Always`
+- `secret.name`: Secret 名称,默认 `mongo-secret`
+- `secret.mongoURI`: MongoDB 连接 URI
+- `secret.trafficMongoURI`: 流量 MongoDB 连接 URI
+- `secret.trafficsServiceConnectAddress`: 流量服务连接地址
+- `metrics.enabled`: 是否启用 metrics,默认 `false`
+- `resources.limits.cpu/memory`: CPU/内存限制
+- `resources.requests.cpu/memory`: CPU/内存请求
+- `nodeSelector`: 节点选择器
+- `tolerations`: 容忍度配置
+- `affinity`: 亲和性配置
diff --git a/controllers/resources/deploy/charts/resources-controller/Chart.yaml b/controllers/resources/deploy/charts/resources-controller/Chart.yaml
new file mode 100644
index 000000000000..0385a392de60
--- /dev/null
+++ b/controllers/resources/deploy/charts/resources-controller/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: resources
+description: Helm chart for the sealos resources controller
+type: application
+version: 0.1.0
+appVersion: "0.1.0"
diff --git a/controllers/resources/deploy/charts/resources-controller/resources-controller-values.yaml b/controllers/resources/deploy/charts/resources-controller/resources-controller-values.yaml
new file mode 100644
index 000000000000..18ddcd967a9e
--- /dev/null
+++ b/controllers/resources/deploy/charts/resources-controller/resources-controller-values.yaml
@@ -0,0 +1,20 @@
+# Custom values for resources controller helm chart.
+# This file contains user-customizable configurations.
+
+replicaCount: 1
+
+resources:
+ limits:
+ cpu: 1000m
+ memory: 1280Mi
+ requests:
+ cpu: 10m
+ memory: 64Mi
+
+configmap:
+ # User custom configurations
+ # Note: These values can be overridden by auto-configured values from sealos-config ConfigMap
+ enableAutoResourceQuota: "false"
+ concurrentLimit: "1000"
+ ephemeralStorageChargeThreshold: "10Gi"
+ limitQuotaExpansionCycle: "24h"
diff --git a/controllers/resources/deploy/charts/resources-controller/templates/_helpers.tpl b/controllers/resources/deploy/charts/resources-controller/templates/_helpers.tpl
new file mode 100644
index 000000000000..a49424b0c0cf
--- /dev/null
+++ b/controllers/resources/deploy/charts/resources-controller/templates/_helpers.tpl
@@ -0,0 +1,60 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "resources.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+*/}}
+{{- define "resources.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "resources.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "resources.labels" -}}
+helm.sh/chart: {{ include "resources.chart" . }}
+{{ include "resources.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "resources.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "resources.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "resources.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "resources.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/controllers/resources/deploy/charts/resources-controller/templates/cert.yaml b/controllers/resources/deploy/charts/resources-controller/templates/cert.yaml
new file mode 100644
index 000000000000..71cf83f1d8d3
--- /dev/null
+++ b/controllers/resources/deploy/charts/resources-controller/templates/cert.yaml
@@ -0,0 +1,27 @@
+{{- if .Values.metrics.enabled }}
+apiVersion: cert-manager.io/v1
+kind: Issuer
+metadata:
+ labels:
+ {{- include "resources.labels" . | nindent 4 }}
+ name: selfsigned-issuer
+spec:
+ selfSigned: {}
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+ labels:
+ {{- include "resources.labels" . | nindent 4 }}
+ name: metrics-certs
+spec:
+ privateKey:
+ rotationPolicy: Always
+ dnsNames:
+ - {{ include "resources.fullname" . }}-controller-manager-metrics-service.{{ .Release.Namespace }}.svc
+ - {{ include "resources.fullname" . }}-controller-manager-metrics-service.{{ .Release.Namespace }}.svc.cluster.local
+ issuerRef:
+ kind: Issuer
+ name: selfsigned-issuer
+ secretName: {{ .Values.metrics.secretName }}
+{{- end }}
diff --git a/controllers/resources/deploy/charts/resources-controller/templates/configmap.yaml b/controllers/resources/deploy/charts/resources-controller/templates/configmap.yaml
new file mode 100644
index 000000000000..a474dfbdecef
--- /dev/null
+++ b/controllers/resources/deploy/charts/resources-controller/templates/configmap.yaml
@@ -0,0 +1,36 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ .Values.configmap.name }}
+ namespace: {{ .Release.Namespace }}
+ labels:
+ control-plane: controller-manager
+ {{- include "resources.labels" . | nindent 4 }}
+data:
+ {{- $data := dict
+ "MONGO_URI" .Values.configmap.mongoURI
+ "TRAFFIC_MONGO_URI" .Values.configmap.trafficMongoURI
+ "TRAFFICS_SERVICE_CONNECT_ADDRESS" .Values.configmap.trafficsServiceConnectAddress
+ "MINIO_ENDPOINT" .Values.configmap.minioEndpoint
+ "MINIO_AK" .Values.configmap.minioAK
+ "MINIO_SK" .Values.configmap.minioSK
+ "MINIO_METRICS_ADDR" .Values.configmap.minioMetricsAddr
+ "MINIO_METRICS_SECURE" .Values.configmap.minioMetricsSecure
+ "PROM_URL" .Values.configmap.promURL
+ "OBJECT_STORAGE_INSTANCE" .Values.configmap.objectStorageInstance
+ "ENABLE_AUTO_RESOURCE_QUOTA" .Values.configmap.enableAutoResourceQuota
+ "CONCURRENT_LIMIT" .Values.configmap.concurrentLimit
+ "EPHEMERAL_STORAGE_CHARGE_THRESHOLD" .Values.configmap.ephemeralStorageChargeThreshold
+ "LIMIT_QUOTA_EXPANSION_CYCLE" .Values.configmap.limitQuotaExpansionCycle
+ -}}
+ {{- if eq (default "overwrite" .Values.configmapMergeStrategy) "preserve" }}
+ {{- $existing := (lookup "v1" "ConfigMap" .Release.Namespace .Values.configmap.name) }}
+ {{- if $existing }}
+ {{- range $k, $v := $existing.data }}
+ {{- $_ := set $data $k $v }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- range $k, $v := $data }}
+ {{ $k }}: {{ $v | quote }}
+ {{- end }}
diff --git a/controllers/resources/deploy/charts/resources-controller/templates/deployment.yaml b/controllers/resources/deploy/charts/resources-controller/templates/deployment.yaml
new file mode 100644
index 000000000000..af217d624c7b
--- /dev/null
+++ b/controllers/resources/deploy/charts/resources-controller/templates/deployment.yaml
@@ -0,0 +1,121 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "resources.fullname" . }}-controller-manager
+ namespace: {{ .Release.Namespace }}
+ labels:
+ control-plane: controller-manager
+ {{- include "resources.labels" . | nindent 4 }}
+spec:
+ replicas: {{ .Values.replicaCount }}
+ selector:
+ matchLabels:
+ control-plane: controller-manager
+ template:
+ metadata:
+ annotations:
+ kubectl.kubernetes.io/default-container: manager
+ checksum/resources-config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
+ {{- with .Values.podAnnotations }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ labels:
+ control-plane: controller-manager
+ {{- include "resources.labels" . | nindent 8 }}
+ {{- with .Values.podLabels }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ spec:
+ {{- with .Values.imagePullSecrets }}
+ imagePullSecrets:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ serviceAccountName: {{ include "resources.serviceAccountName" . }}
+ securityContext:
+ {{- toYaml .Values.podSecurityContext | nindent 8 }}
+ containers:
+ - name: manager
+ command:
+ - /manager
+ args:
+ - --health-probe-bind-address=:8081
+ - --leader-elect
+ {{- if .Values.metrics.enabled }}
+ - --metrics-secure=true
+ - --metrics-bind-address=:8443
+ {{- else }}
+ - --metrics-bind-address=127.0.0.1:8080
+ {{- end }}
+ envFrom:
+ - configMapRef:
+ name: {{ .Values.configmap.name }}
+ {{- with .Values.extraEnv }}
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ ports:
+ {{- if .Values.metrics.enabled }}
+ - containerPort: 8443
+ name: https
+ protocol: TCP
+ {{- end }}
+ image: "{{ .Values.image }}"
+ imagePullPolicy: {{ .Values.imagePullPolicy }}
+ livenessProbe:
+ {{- toYaml .Values.livenessProbe | nindent 12 }}
+ readinessProbe:
+ {{- toYaml .Values.readinessProbe | nindent 12 }}
+ resources:
+ {{- toYaml .Values.resources | nindent 12 }}
+ securityContext:
+ {{- toYaml .Values.securityContext | nindent 12 }}
+ volumeMounts:
+ {{- with .Values.volumeMounts }}
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- if .Values.metrics.enabled }}
+ - name: metrics-certs
+ mountPath: {{ .Values.metrics.certPath }}
+ readOnly: true
+ {{- end }}
+ terminationGracePeriodSeconds: 10
+ affinity:
+ {{- if .Values.affinity }}
+ {{- toYaml .Values.affinity | nindent 8 }}
+ {{- else }}
+ podAntiAffinity:
+ preferredDuringSchedulingIgnoredDuringExecution:
+ - weight: 100
+ podAffinityTerm:
+ labelSelector:
+ matchExpressions:
+ - key: control-plane
+ operator: In
+ values:
+ - controller-manager
+ topologyKey: kubernetes.io/hostname
+ {{- end }}
+ {{- with .Values.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ volumes:
+ {{- if .Values.metrics.enabled }}
+ - name: metrics-certs
+ secret:
+ secretName: {{ .Values.metrics.secretName }}
+ optional: false
+ items:
+ - key: ca.crt
+ path: ca.crt
+ - key: tls.crt
+ path: tls.crt
+ - key: tls.key
+ path: tls.key
+ {{- end }}
+ {{- with .Values.volumes }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
diff --git a/controllers/resources/deploy/charts/resources-controller/templates/rbac.yaml b/controllers/resources/deploy/charts/resources-controller/templates/rbac.yaml
new file mode 100644
index 000000000000..efc9366aad5c
--- /dev/null
+++ b/controllers/resources/deploy/charts/resources-controller/templates/rbac.yaml
@@ -0,0 +1,263 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: {{ include "resources.fullname" . }}-leader-election-role
+ namespace: {{ .Release.Namespace }}
+ labels:
+ control-plane: controller-manager
+ {{- include "resources.labels" . | nindent 4 }}
+rules:
+ - apiGroups:
+ - ""
+ resources:
+ - configmaps
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+ - apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+ - apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ include "resources.fullname" . }}-manager-role
+ labels:
+ {{- include "resources.labels" . | nindent 4 }}
+rules:
+ - apiGroups:
+ - ""
+ resources:
+ - namespaces
+ verbs:
+ - get
+ - list
+ - watch
+ - update
+ - patch
+ - apiGroups:
+ - networking.k8s.io
+ resources:
+ - ingresses
+ verbs:
+ - get
+ - list
+ - watch
+ - update
+ - patch
+ - apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - apiGroups:
+ - ""
+ resources:
+ - nodes
+ verbs:
+ - get
+ - list
+ - watch
+ - apiGroups:
+ - ""
+ resources:
+ - persistentvolumeclaims
+ verbs:
+ - get
+ - list
+ - watch
+ - apiGroups:
+ - ""
+ resources:
+ - pods
+ verbs:
+ - get
+ - list
+ - watch
+ - apiGroups:
+ - ""
+ resources:
+ - resourcequotas
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - apiGroups:
+ - ""
+ resources:
+ - resourcequotas/status
+ verbs:
+ - get
+ - list
+ - watch
+ - apiGroups:
+ - ""
+ resources:
+ - services
+ verbs:
+ - get
+ - list
+ - watch
+ - update
+ - patch
+ - apiGroups:
+ - ""
+ resources:
+ - services/status
+ verbs:
+ - get
+ - list
+ - watch
+ - apiGroups:
+ - ""
+ resources:
+ - events
+ - configmaps
+ verbs:
+ - create
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - app.sealos.io
+ resources:
+ - instances
+ verbs:
+ - get
+ - list
+ - watch
+ - apiGroups:
+ - app.sealos.io
+ resources:
+ - instances/status
+ verbs:
+ - get
+ - list
+ - watch
+ - apiGroups:
+ - dataprotection.kubeblocks.io
+ resources:
+ - backups
+ verbs:
+ - get
+ - list
+ - watch
+ - apiGroups:
+ - dataprotection.kubeblocks.io
+ resources:
+ - backups/status
+ verbs:
+ - get
+ - list
+ - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ include "resources.fullname" . }}-metrics-reader
+ labels:
+ {{- include "resources.labels" . | nindent 4 }}
+rules:
+ - nonResourceURLs:
+ - /metrics
+ verbs:
+ - get
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ include "resources.fullname" . }}-proxy-role
+ labels:
+ {{- include "resources.labels" . | nindent 4 }}
+rules:
+ - apiGroups:
+ - authentication.k8s.io
+ resources:
+ - tokenreviews
+ verbs:
+ - create
+ - apiGroups:
+ - authorization.k8s.io
+ resources:
+ - subjectaccessreviews
+ verbs:
+ - create
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: {{ include "resources.fullname" . }}-leader-election-rolebinding
+ namespace: {{ .Release.Namespace }}
+ labels:
+ control-plane: controller-manager
+ {{- include "resources.labels" . | nindent 4 }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: {{ include "resources.fullname" . }}-leader-election-role
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "resources.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "resources.fullname" . }}-manager-rolebinding
+ labels:
+ {{- include "resources.labels" . | nindent 4 }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: {{ include "resources.fullname" . }}-manager-role
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "resources.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "resources.fullname" . }}-proxy-rolebinding
+ labels:
+ {{- include "resources.labels" . | nindent 4 }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: {{ include "resources.fullname" . }}-proxy-role
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "resources.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
diff --git a/controllers/resources/deploy/charts/resources-controller/templates/service.yaml b/controllers/resources/deploy/charts/resources-controller/templates/service.yaml
new file mode 100644
index 000000000000..52287ca44bfa
--- /dev/null
+++ b/controllers/resources/deploy/charts/resources-controller/templates/service.yaml
@@ -0,0 +1,18 @@
+{{- if .Values.metrics.enabled }}
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "resources.fullname" . }}-controller-manager-metrics-service
+ namespace: {{ .Release.Namespace }}
+ labels:
+ control-plane: controller-manager
+ {{- include "resources.labels" . | nindent 4 }}
+spec:
+ ports:
+ - name: https
+ port: {{ .Values.service.metrics.port }}
+ protocol: TCP
+ targetPort: https
+ selector:
+ control-plane: controller-manager
+{{- end }}
diff --git a/controllers/resources/deploy/charts/resources-controller/templates/serviceaccount.yaml b/controllers/resources/deploy/charts/resources-controller/templates/serviceaccount.yaml
new file mode 100644
index 000000000000..789c2346e15b
--- /dev/null
+++ b/controllers/resources/deploy/charts/resources-controller/templates/serviceaccount.yaml
@@ -0,0 +1,15 @@
+{{- if .Values.serviceAccount.create }}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ include "resources.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
+ labels:
+ control-plane: controller-manager
+ {{- include "resources.labels" . | nindent 4 }}
+ {{- with .Values.serviceAccount.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
+{{- end }}
diff --git a/controllers/resources/deploy/charts/resources-controller/values.yaml b/controllers/resources/deploy/charts/resources-controller/values.yaml
new file mode 100644
index 000000000000..18d84d125952
--- /dev/null
+++ b/controllers/resources/deploy/charts/resources-controller/values.yaml
@@ -0,0 +1,111 @@
+# Default values for resources controller helm chart.
+
+replicaCount: 1
+
+image: ghcr.io/labring/sealos-resources-controller:latest
+imagePullPolicy: Always
+
+imagePullSecrets: []
+nameOverride: ""
+fullnameOverride: "resources"
+
+serviceAccount:
+ create: true
+ automount: true
+ annotations: {}
+ name: "resources-controller-manager"
+
+podAnnotations: {}
+podLabels: {}
+
+podSecurityContext:
+ runAsNonRoot: true
+
+securityContext:
+ allowPrivilegeEscalation: false
+ runAsNonRoot: true
+
+service:
+ metrics:
+ port: 8443
+
+resources:
+ limits:
+ cpu: 1000m
+ memory: 1280Mi
+ requests:
+ cpu: 10m
+ memory: 64Mi
+
+# ============================================================================
+# Auto-configured values (from sealos-system ConfigMaps)
+# ============================================================================
+# The following configmap values are automatically fetched from ConfigMaps
+# by the entrypoint script and will override any values set here.
+# These are provided as reference defaults.
+#
+# To override these auto-configured values, use HELM_OPTIONS or modify the
+# ConfigMaps directly.
+# ============================================================================
+# Sources:
+# - sealos-system/sealos-config ConfigMap: databaseMongodbURI
+# - sealos-system/nm-agent-config ConfigMap: MONGO_URI (for trafficMongoURI)
+# - sealos-system/objectstorage-config ConfigMap: MINIO_ROOT_USER, MINIO_ROOT_PASSWORD
+# ============================================================================
+
+configmap:
+ name: "resources-config"
+ # Database URIs (auto-configured from sealos-config)
+ mongoURI: "mongodb://mongo:27017/resources" # Auto-fetched from sealos-config.databaseMongodbURI
+ trafficMongoURI: "mongodb://mongo:27017/traffic" # Auto-fetched from nm-agent-config.MONGO_URI or sealos-config
+ trafficsServiceConnectAddress: ""
+
+ # MinIO configuration (auto-configured from objectstorage-config)
+ minioEndpoint: "object-storage.objectstorage-system.svc:80"
+ minioAK: "" # Auto-fetched from objectstorage-config.MINIO_ROOT_USER
+ minioSK: "" # Auto-fetched from objectstorage-config.MINIO_ROOT_PASSWORD
+ minioMetricsAddr: "object-storage.objectstorage-system.svc:80"
+ minioMetricsSecure: "false"
+
+ # Monitoring configuration
+ promURL: "http://vmselect-vm-stack-victoria-metrics-k8s-stack.vm.svc:8481/select/0/prometheus/"
+ objectStorageInstance: "object-storage.objectstorage-system.svc:80"
+
+ # Resource quota configuration
+ enableAutoResourceQuota: "false"
+ concurrentLimit: "1000"
+ ephemeralStorageChargeThreshold: "10Gi"
+ limitQuotaExpansionCycle: "24h"
+
+# End of auto-configured values
+# ============================================================================
+
+extraEnv: []
+
+livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 8081
+ initialDelaySeconds: 15
+ periodSeconds: 20
+
+readinessProbe:
+ httpGet:
+ path: /readyz
+ port: 8081
+ initialDelaySeconds: 5
+ periodSeconds: 10
+
+metrics:
+ enabled: false
+ certPath: /tmp/k8s-metrics-server/metrics-certs
+ secretName: metrics-server-cert
+
+volumes: []
+volumeMounts: []
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
diff --git a/controllers/resources/deploy/resources-controller-entrypoint.sh b/controllers/resources/deploy/resources-controller-entrypoint.sh
new file mode 100644
index 000000000000..562b1a611384
--- /dev/null
+++ b/controllers/resources/deploy/resources-controller-entrypoint.sh
@@ -0,0 +1,153 @@
+#!/bin/bash
+set -e
+
+HELM_OPTS=${HELM_OPTS:-""}
+RELEASE_NAME=${RELEASE_NAME:-"resources-controller"}
+RELEASE_NAMESPACE=${RELEASE_NAMESPACE:-"resources-system"}
+CHART_PATH=${CHART_PATH:-"./charts/resources-controller"}
+RESOURCES_ENV_MERGE_STRATEGY=${RESOURCES_ENV_MERGE_STRATEGY:-"overwrite"}
+RESOURCES_BACKUP_ENABLED=${RESOURCES_BACKUP_ENABLED:-"true"}
+RESOURCES_BACKUP_DIR=${RESOURCES_BACKUP_DIR:-"/tmp/sealos-backup/resources-controller"}
+
+adopt_namespaced_resource() {
+ local kind="$1"
+ local name="$2"
+ if kubectl -n "${RELEASE_NAMESPACE}" get "${kind}" "${name}" >/dev/null 2>&1; then
+ kubectl -n "${RELEASE_NAMESPACE}" label "${kind}" "${name}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true
+ kubectl -n "${RELEASE_NAMESPACE}" annotate "${kind}" "${name}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true
+ fi
+}
+
+adopt_cluster_resource() {
+ local kind="$1"
+ local name="$2"
+ if kubectl get "${kind}" "${name}" >/dev/null 2>&1; then
+ kubectl label "${kind}" "${name}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true
+ kubectl annotate "${kind}" "${name}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true
+ fi
+}
+
+get_cm_value() {
+ local namespace="$1"
+ local name="$2"
+ local key="$3"
+ kubectl get configmap "${name}" -n "${namespace}" -o "jsonpath={.data.${key}}" 2>/dev/null || true
+}
+
+backup_ns_resource() {
+ local kind="$1"
+ local name="$2"
+ if kubectl -n "${RELEASE_NAMESPACE}" get "${kind}" "${name}" >/dev/null 2>&1; then
+ kubectl -n "${RELEASE_NAMESPACE}" get "${kind}" "${name}" -o yaml >> "${RESOURCES_BACKUP_FILE}"
+ printf "\n---\n" >> "${RESOURCES_BACKUP_FILE}"
+ fi
+}
+
+backup_cluster_resource() {
+ local kind="$1"
+ local name="$2"
+ if kubectl get "${kind}" "${name}" >/dev/null 2>&1; then
+ kubectl get "${kind}" "${name}" -o yaml >> "${RESOURCES_BACKUP_FILE}"
+ printf "\n---\n" >> "${RESOURCES_BACKUP_FILE}"
+ fi
+}
+
+backup_resources_resources() {
+ if [ "${RESOURCES_BACKUP_ENABLED}" != "true" ]; then
+ return
+ fi
+ local ts
+ ts=$(date +%Y%m%d%H%M%S)
+ mkdir -p "${RESOURCES_BACKUP_DIR}"
+ RESOURCES_BACKUP_FILE="${RESOURCES_BACKUP_DIR}/update-${ts}.yaml"
+ : > "${RESOURCES_BACKUP_FILE}"
+
+ backup_cluster_resource clusterrole resources-manager-role
+ backup_cluster_resource clusterrole resources-metrics-reader
+ backup_cluster_resource clusterrole resources-proxy-role
+ backup_cluster_resource clusterrolebinding resources-manager-rolebinding
+ backup_cluster_resource clusterrolebinding resources-proxy-rolebinding
+
+ if kubectl get namespace "${RELEASE_NAMESPACE}" >/dev/null 2>&1; then
+ kubectl get namespace "${RELEASE_NAMESPACE}" -o yaml >> "${RESOURCES_BACKUP_FILE}"
+ printf "\n---\n" >> "${RESOURCES_BACKUP_FILE}"
+ fi
+ backup_ns_resource configmap resources-manager-config
+ backup_ns_resource configmap resources-config
+ backup_ns_resource service resources-controller-manager-metrics-service
+ backup_ns_resource deployment resources-controller-manager
+ backup_ns_resource serviceaccount resources-controller-manager
+ backup_ns_resource role resources-leader-election-role
+ backup_ns_resource rolebinding resources-leader-election-rolebinding
+ backup_ns_resource issuer selfsigned-issuer
+ backup_ns_resource certificate metrics-certs
+}
+
+# 执行备份
+backup_resources_resources
+
+HELM_SET_ARGS=()
+
+AUTO_CONFIG_HELM_OPTS=""
+
+MONGODB_URI=$(get_cm_value sealos-system sealos-config databaseMongodbURI)
+MINIO_USER=$(get_cm_value sealos-system objectstorage-config MINIO_ROOT_USER)
+MINIO_PASSWORD=$(get_cm_value sealos-system objectstorage-config MINIO_ROOT_PASSWORD)
+
+TRAFFIC_MONGO=$(get_cm_value sealos-system nm-agent-config MONGO_URI)
+if [ -z "${TRAFFIC_MONGO}" ] && [ -n "${MONGODB_URI}" ]; then
+ TRAFFIC_MONGO="${MONGODB_URI}"
+fi
+
+[ -n "${MONGODB_URI}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string configmap.mongoURI=${MONGODB_URI}"
+[ -n "${TRAFFIC_MONGO}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string configmap.trafficMongoURI=${TRAFFIC_MONGO}"
+[ -n "${MINIO_USER}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string configmap.minioAK=${MINIO_USER}"
+[ -n "${MINIO_PASSWORD}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string configmap.minioSK=${MINIO_PASSWORD}"
+
+if ! helm status "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" >/dev/null 2>&1; then
+ if kubectl get namespace "${RELEASE_NAMESPACE}" >/dev/null 2>&1; then
+ kubectl label namespace "${RELEASE_NAMESPACE}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true
+ kubectl annotate namespace "${RELEASE_NAMESPACE}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true
+ fi
+
+ adopt_namespaced_resource configmap resources-manager-config
+ adopt_namespaced_resource configmap resources-config
+ adopt_namespaced_resource service resources-controller-manager-metrics-service
+ adopt_namespaced_resource deployment resources-controller-manager
+ adopt_namespaced_resource serviceaccount resources-controller-manager
+ adopt_namespaced_resource role resources-leader-election-role
+ adopt_namespaced_resource rolebinding resources-leader-election-rolebinding
+ adopt_namespaced_resource issuer selfsigned-issuer
+ adopt_namespaced_resource certificate metrics-certs
+
+ adopt_cluster_resource clusterrole resources-manager-role
+ adopt_cluster_resource clusterrole resources-metrics-reader
+ adopt_cluster_resource clusterrole resources-proxy-role
+ adopt_cluster_resource clusterrolebinding resources-manager-rolebinding
+ adopt_cluster_resource clusterrolebinding resources-proxy-rolebinding
+fi
+
+if [ -n "${RESOURCES_ENV_MERGE_STRATEGY}" ]; then
+ HELM_SET_ARGS+=(--set-string "configmapMergeStrategy=${RESOURCES_ENV_MERGE_STRATEGY}")
+fi
+
+# Prepare values files
+SERVICE_NAME="resources-controller"
+USER_VALUES_PATH="/root/.sealos/cloud/values/core/${SERVICE_NAME}-values.yaml"
+
+# Copy user values template if not exists
+if [ ! -f "${USER_VALUES_PATH}" ]; then
+ mkdir -p "$(dirname "${USER_VALUES_PATH}")"
+ cp "./charts/${SERVICE_NAME}/${SERVICE_NAME}-values.yaml" "${USER_VALUES_PATH}"
+fi
+
+# merge all helm_opts
+# 1. AUTO_CONFIG_HELM_OPTS (Configuration automatically obtained from ConfigMap)
+# 2. HELM_SET_ARGS (parameters set internally in the script)
+# 3. HELM_OPTS (the parameter passed by the user via --env, with the highest priority, can override the previous configuration)
+helm upgrade -i "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" --create-namespace "${CHART_PATH}" \
+ -f "./charts/${SERVICE_NAME}/values.yaml" \
+ -f "${USER_VALUES_PATH}" \
+ ${AUTO_CONFIG_HELM_OPTS} \
+ "${HELM_SET_ARGS[@]}" \
+ ${HELM_OPTS}
diff --git a/controllers/resources/go.mod b/controllers/resources/go.mod
new file mode 100644
index 000000000000..ccae1e073ac7
--- /dev/null
+++ b/controllers/resources/go.mod
@@ -0,0 +1,118 @@
+module github.com/labring/sealos/controllers/resources
+
+go 1.24.0
+
+replace (
+ k8s.io/api => k8s.io/api v0.32.3
+ k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.32.1
+ k8s.io/apimachinery => k8s.io/apimachinery v0.32.1
+ k8s.io/client-go => k8s.io/client-go v0.32.1
+ sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.20.4
+)
+
+require (
+ github.com/apecloud/kubeblocks v0.9.3
+ github.com/go-logr/logr v1.4.2
+ github.com/labring/sealos/controllers/app v0.0.0-20240807070504-eb838607f089
+ github.com/labring/sealos/controllers/pkg v0.0.0-20240715064441-d1193f70675b
+ github.com/labring/sealos/controllers/user v0.0.0
+ github.com/minio/minio-go/v7 v7.0.64
+ github.com/onsi/ginkgo v1.16.5
+ github.com/onsi/gomega v1.36.1
+ golang.org/x/sync v0.8.0
+ k8s.io/api v0.32.1
+ k8s.io/apimachinery v0.32.3
+ k8s.io/client-go v12.0.0+incompatible
+ sigs.k8s.io/controller-runtime v0.20.4
+)
+
+require (
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/emicklei/go-restful/v3 v3.11.0 // indirect
+ github.com/evanphx/json-patch/v5 v5.9.11 // indirect
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/fxamacker/cbor/v2 v2.7.0 // indirect
+ github.com/go-logr/zapr v1.3.0 // indirect
+ github.com/go-openapi/jsonpointer v0.21.0 // indirect
+ github.com/go-openapi/jsonreference v0.20.2 // indirect
+ github.com/go-openapi/swag v0.23.0 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/golang/snappy v0.0.4 // indirect
+ github.com/google/btree v1.1.3 // indirect
+ github.com/google/gnostic-models v0.6.8 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
+ github.com/google/gofuzz v1.2.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+ github.com/jackc/pgx/v5 v5.5.4 // indirect
+ github.com/jackc/puddle/v2 v2.2.1 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.17.8 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.7 // indirect
+ github.com/lib/pq v1.10.9 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/matoous/go-nanoid/v2 v2.0.0 // indirect
+ github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
+ github.com/minio/md5-simd v1.1.2 // indirect
+ github.com/minio/sha256-simd v1.0.1 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/montanaflynn/stats v0.6.6 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/nxadm/tail v1.4.8 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/prometheus/client_golang v1.19.1 // indirect
+ github.com/prometheus/client_model v0.6.1 // indirect
+ github.com/prometheus/common v0.55.0 // indirect
+ github.com/prometheus/procfs v0.15.1 // indirect
+ github.com/prometheus/prom2json v1.3.3 // indirect
+ github.com/rs/xid v1.5.0 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
+ github.com/xdg-go/pbkdf2 v1.0.0 // indirect
+ github.com/xdg-go/scram v1.1.2 // indirect
+ github.com/xdg-go/stringprep v1.0.4 // indirect
+ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
+ go.mongodb.org/mongo-driver v1.12.1 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ go.uber.org/zap v1.27.0 // indirect
+ golang.org/x/crypto v0.28.0 // indirect
+ golang.org/x/net v0.30.0 // indirect
+ golang.org/x/oauth2 v0.23.0 // indirect
+ golang.org/x/sys v0.26.0 // indirect
+ golang.org/x/term v0.25.0 // indirect
+ golang.org/x/text v0.19.0 // indirect
+ golang.org/x/time v0.7.0 // indirect
+ gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
+ google.golang.org/protobuf v1.35.1 // indirect
+ gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+ gopkg.in/inf.v0 v0.9.1 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ gorm.io/driver/postgres v1.5.4 // indirect
+ gorm.io/gorm v1.25.5 // indirect
+ k8s.io/apiextensions-apiserver v0.32.1 // indirect
+ k8s.io/klog/v2 v2.130.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
+ k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
+ sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
+ sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
+ sigs.k8s.io/yaml v1.4.0 // indirect
+)
+
+replace (
+ github.com/labring/sealos/controllers/pkg => ../pkg
+ github.com/labring/sealos/controllers/user => ../user
+)
diff --git a/controllers/resources/go.sum b/controllers/resources/go.sum
new file mode 100644
index 000000000000..e199fc05e8ad
--- /dev/null
+++ b/controllers/resources/go.sum
@@ -0,0 +1,340 @@
+github.com/apecloud/kubeblocks v0.9.3 h1:06hUB4oVZdHfkFg/wez6LJahiJCE+vp5DDyrcXHqCZc=
+github.com/apecloud/kubeblocks v0.9.3/go.mod h1:uC7CHg8mTEEhYeJSyiZI1tX2Ep7rpq5qumcJyTIqlCg=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
+github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
+github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
+github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=
+github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
+github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/labring/sealos/controllers/app v0.0.0-20240807070504-eb838607f089 h1:UVeQQ61npBKmrT7r6GrLLmCMUSgAY3c5/fFEAjVN+II=
+github.com/labring/sealos/controllers/app v0.0.0-20240807070504-eb838607f089/go.mod h1:F/fdFEzWKs0mDmXWDxcaAVPv2SKfl/DXH4X4pimApHE=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
+github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
+github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
+github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
+github.com/minio/minio-go/v7 v7.0.64 h1:Zdza8HwOzkld0ZG/og50w56fKi6AAyfqfifmasD9n2Q=
+github.com/minio/minio-go/v7 v7.0.64/go.mod h1:R4WVUR6ZTedlCcGwZRauLMIKjgyaWxhs4Mqi/OMPmEc=
+github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
+github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ=
+github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
+github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
+github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo=
+github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
+github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
+github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
+github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
+github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
+github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE=
+go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
+golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
+golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
+gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
+gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
+gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
+gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
+k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
+k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw=
+k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto=
+k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs=
+k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU=
+k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
+k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
+k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
+sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/controllers/resources/hack/boilerplate.go.txt b/controllers/resources/hack/boilerplate.go.txt
new file mode 100644
index 000000000000..4b31f4ca244e
--- /dev/null
+++ b/controllers/resources/hack/boilerplate.go.txt
@@ -0,0 +1,15 @@
+/*
+Copyright 2023 sealos.
+
+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.
+*/
\ No newline at end of file
diff --git a/controllers/resources/main.go b/controllers/resources/main.go
new file mode 100644
index 000000000000..40cd9712cc02
--- /dev/null
+++ b/controllers/resources/main.go
@@ -0,0 +1,295 @@
+/*
+Copyright 2023 sealos.
+
+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 main
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
+ appv1 "github.com/labring/sealos/controllers/app/api/v1"
+ "github.com/labring/sealos/controllers/pkg/database"
+ "github.com/labring/sealos/controllers/pkg/database/mongo"
+ "github.com/labring/sealos/controllers/pkg/objectstorage"
+ "github.com/labring/sealos/controllers/pkg/resources"
+ "github.com/labring/sealos/controllers/pkg/utils/env"
+ "github.com/labring/sealos/controllers/resources/controllers"
+ resourcecache "github.com/labring/sealos/controllers/resources/controllers/cache"
+ "k8s.io/apimachinery/pkg/runtime"
+ utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+ _ "k8s.io/client-go/plugin/pkg/client/auth"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/healthz"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+ metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+)
+
+var (
+ scheme = runtime.NewScheme()
+ setupLog = ctrl.Log.WithName("setup")
+)
+
+func init() {
+ utilruntime.Must(clientgoscheme.AddToScheme(scheme))
+ utilruntime.Must(appv1.AddToScheme(scheme))
+ utilruntime.Must(v1alpha1.AddToScheme(scheme))
+ //+kubebuilder:scaffold:scheme
+}
+
+func main() {
+ var metricsAddr string
+ var enableLeaderElection bool
+ var probeAddr string
+ flag.StringVar(
+ &metricsAddr,
+ "metrics-bind-address",
+ ":8080",
+ "The address the metric endpoint binds to.",
+ )
+ flag.StringVar(
+ &probeAddr,
+ "health-probe-bind-address",
+ ":8081",
+ "The address the probe endpoint binds to.",
+ )
+ flag.BoolVar(&enableLeaderElection, "leader-elect", false,
+ "Enable leader election for controller manager. "+
+ "Enabling this will ensure there is only one active controller manager.")
+ opts := zap.Options{
+ Development: true,
+ }
+ opts.BindFlags(flag.CommandLine)
+ flag.Parse()
+
+ ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
+
+ mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
+ Scheme: scheme,
+ Cache: resourcecache.Options(),
+ Client: client.Options{Cache: &client.CacheOptions{
+ DisableFor: resourcecache.UncachedObjects(),
+ }},
+ Metrics: metricsserver.Options{
+ BindAddress: metricsAddr,
+ },
+ HealthProbeBindAddress: probeAddr,
+ LeaderElection: enableLeaderElection,
+ LeaderElectionID: "a63686c3.sealos.io",
+ })
+ if err != nil {
+ setupLog.Error(err, "unable to start manager")
+ os.Exit(1)
+ }
+
+ //+kubebuilder:scaffold:builder
+
+ if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
+ setupLog.Error(err, "unable to set up health check")
+ os.Exit(1)
+ }
+ if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
+ setupLog.Error(err, "unable to set up ready check")
+ os.Exit(1)
+ }
+
+ setupLog.Info("starting manager")
+
+ if err = resourcecache.SetupInformers(mgr); err != nil {
+ setupLog.Error(err, "failed to set up resource cache informers")
+ os.Exit(1)
+ }
+ err = controllers.InitIndexField(mgr)
+ if err != nil {
+ setupLog.Error(err, "failed to init index field")
+ os.Exit(1)
+ }
+ // if env.GetBoolWithDefault("ENABLE_AUTO_RESOURCE_QUOTA", false) {
+ // if err = (&controllers.NamespaceQuotaReconciler{
+ // Client: mgr.GetClient(),
+ // Scheme: mgr.GetScheme(),
+ // Recorder: mgr.GetEventRecorderFor("namespace-quota-controller"),
+ // }).SetupWithManager(mgr); err != nil {
+ // setupLog.Error(err, "unable to create controller", "controller", "NamespaceQuota")
+ // os.Exit(1)
+ // }
+ //}
+ // if err = (&controllers.NetworkReconciler{}).SetupWithManager(mgr); err != nil {
+ // setupLog.Error(err, "unable to create controller", "controller", "Network")
+ // os.Exit(1)
+ //}
+
+ managerCtx := ctrl.SetupSignalHandler()
+ go func() {
+ if err := mgr.Start(managerCtx); err != nil {
+ setupLog.Error(err, "problem running manager")
+ os.Exit(1)
+ }
+ }()
+ // ReaderFailOnMissingInformer skips the per-read sync wait, so synchronize all
+ // explicitly registered informers before the monitor performs its first read.
+ if !mgr.GetCache().WaitForCacheSync(managerCtx) {
+ setupLog.Error(
+ errors.New("resource cache sync did not complete"),
+ "unable to start monitor",
+ )
+ os.Exit(1)
+ }
+
+ reconciler, err := controllers.NewMonitorReconciler(mgr)
+ if err != nil {
+ setupLog.Error(err, "failed to init monitor reconciler")
+ os.Exit(1)
+ }
+ reconciler.DBClient, err = mongo.NewMongoInterface(
+ context.Background(),
+ os.Getenv(database.MongoURI),
+ )
+ if err != nil {
+ setupLog.Error(err, "failed to init db client")
+ os.Exit(1)
+ }
+ defer func() {
+ if err := reconciler.DBClient.Disconnect(context.Background()); err != nil {
+ setupLog.Error(err, "failed to disconnect db client")
+ }
+ }()
+ if trafficURI := os.Getenv(database.TrafficMongoURI); trafficURI != "" {
+ reconciler.TrafficClient, err = mongo.NewMongoInterface(context.Background(), trafficURI)
+ if err != nil {
+ setupLog.Error(err, "failed to init traffic db client")
+ os.Exit(1)
+ }
+ defer func() {
+ if err := reconciler.TrafficClient.Disconnect(context.Background()); err != nil {
+ setupLog.Error(err, "failed to disconnect traffic db client")
+ }
+ }()
+ } else {
+ setupLog.Info("traffic mongo uri not found, please check env: TRAFFIC_MONGO_URI")
+ }
+
+ err = reconciler.DBClient.InitDefaultPropertyTypeLSWithDefaults()
+ if err != nil {
+ setupLog.Error(err, "failed to init property type with defaults")
+ os.Exit(1)
+ }
+ reconciler.Properties = resources.DefaultPropertyTypeLS
+ const (
+ MinioEndpoint = "MINIO_ENDPOINT"
+ MinioAk = "MINIO_AK"
+ MinioSk = "MINIO_SK"
+ PromURL = "PROM_URL"
+ MinioMetricsAddr = "MINIO_METRICS_ADDR"
+ MinioMetricsAddrSecure = "MINIO_METRICS_SECURE"
+ )
+ if endpoint, ak, sk, mAddr := os.Getenv(
+ MinioEndpoint,
+ ), os.Getenv(
+ MinioAk,
+ ), os.Getenv(
+ MinioSk,
+ ), os.Getenv(
+ MinioMetricsAddr,
+ ); endpoint != "" &&
+ ak != "" &&
+ sk != "" &&
+ mAddr != "" {
+ reconciler.Info("init minio client")
+ if reconciler.ObjStorageClient, err = objectstorage.NewOSClient(
+ endpoint,
+ ak,
+ sk,
+ ); err != nil {
+ reconciler.Error(err, "failed to new minio client")
+ os.Exit(1)
+ }
+ _, err := reconciler.ObjStorageClient.ListBuckets(context.Background())
+ if err != nil {
+ reconciler.Error(err, "failed to list minio buckets")
+ os.Exit(1)
+ }
+ if reconciler.PromURL = os.Getenv(PromURL); reconciler.PromURL == "" {
+ reconciler.Info("prometheus url not found, please check env: PROM_URL")
+ }
+ secure := env.GetBoolWithDefault(MinioMetricsAddrSecure, false)
+ reconciler.ObjStorageMetricsClient, err = objectstorage.NewMetricsClient(
+ mAddr,
+ ak,
+ sk,
+ secure,
+ )
+ if err != nil {
+ reconciler.Error(err, "failed to new minio metrics client")
+ os.Exit(1)
+ }
+ reconciler.Info(
+ fmt.Sprintf(
+ "init minio client with info (endpoint %s, metrics addr %s, metrics addr secure %v) success",
+ endpoint,
+ mAddr,
+ secure,
+ ),
+ )
+ } else {
+ reconciler.Info(
+ "minio info not found, please check env: MINIO_ENDPOINT, MINIO_AK, MINIO_SK, MINIO_METRICS_ADDR",
+ )
+ }
+ err = reconciler.DBClient.CreateTTLTrafficTimeSeries()
+ if err != nil {
+ reconciler.Error(err, "failed to create ttl traffic time series")
+ }
+ // timer creates tomorrow's timing table in advance to ensure that tomorrow's table exists
+ // Execute immediately and then every 24 hours.
+ time.AfterFunc(time.Until(getNextMidnight()), func() {
+ ticker := time.NewTicker(24 * time.Hour)
+ defer ticker.Stop()
+ for {
+ err := reconciler.DBClient.CreateMonitorTimeSeriesIfNotExist(
+ time.Now().UTC().Add(24 * time.Hour),
+ )
+ if err != nil {
+ reconciler.Error(err, "failed to create monitor time series")
+ }
+ if err := reconciler.DropMonitorCollectionOlder(); err != nil {
+ reconciler.Error(err, "failed to drop monitor collection")
+ }
+ <-ticker.C
+ }
+ })
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ if err := reconciler.StartReconciler(ctx); err != nil {
+ setupLog.Error(err, "failed to start monitor reconciler")
+ os.Exit(1)
+ }
+}
+
+// getNextMidnight returns the next midnight time from now
+func getNextMidnight() time.Time {
+ now := time.Now().UTC()
+ midnight := time.Date(now.Year(), now.Month(), now.Day(), 23, 0, 0, 0, time.UTC)
+ return midnight
+}
diff --git a/controllers/user/.dockerignore b/controllers/user/.dockerignore
new file mode 100644
index 000000000000..8e6fac709b69
--- /dev/null
+++ b/controllers/user/.dockerignore
@@ -0,0 +1,3 @@
+# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
+# Ignore build and test binaries.
+testbin/
diff --git a/controllers/user/.gitignore b/controllers/user/.gitignore
new file mode 100644
index 000000000000..3ac36b37de4f
--- /dev/null
+++ b/controllers/user/.gitignore
@@ -0,0 +1,27 @@
+
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+bin
+testbin/*
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+deploy/registry
+
+# Kubernetes Generated files - skip generated files, except for vendored files
+
+!vendor/**/zz_generated.*
+
+# editor and IDE paraphernalia
+.idea
+*.swp
+*.swo
+*~
+user.env
diff --git a/controllers/user/Dockerfile b/controllers/user/Dockerfile
new file mode 100644
index 000000000000..7faf10354053
--- /dev/null
+++ b/controllers/user/Dockerfile
@@ -0,0 +1,22 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+FROM gcr.io/distroless/static:nonroot
+ARG TARGETARCH
+
+WORKDIR /
+USER 65532:65532
+
+COPY bin/controller-user-$TARGETARCH /manager
+ENTRYPOINT ["/manager"]
diff --git a/controllers/user/Makefile b/controllers/user/Makefile
new file mode 100644
index 000000000000..cbb35dac05ee
--- /dev/null
+++ b/controllers/user/Makefile
@@ -0,0 +1,150 @@
+
+# Image URL to use all building/pushing image targets
+IMG ?= ghcr.io/labring/sealos-user-controller:latest
+TARGETARCH ?= amd64
+LICENSE_KEY ?=
+CONTROLLER_LICENSE_PKG=github.com/labring/sealos/controllers/pkg/license
+
+# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
+ENVTEST_K8S_VERSION = 1.25.0
+
+# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
+ifeq (,$(shell go env GOBIN))
+GOBIN=$(shell go env GOPATH)/bin
+else
+GOBIN=$(shell go env GOBIN)
+endif
+
+# Setting SHELL to bash allows bash commands to be executed by recipes.
+# This is a requirement for 'setup-envtest.sh' in the test target.
+# Options are set to exit when a recipe line exits non-zero or a piped command fails.
+SHELL = /usr/bin/env bash -o pipefail
+.SHELLFLAGS = -ec
+
+.PHONY: all
+all: build
+
+##@ General
+
+# The help target prints out all targets with their descriptions organized
+# beneath their categories. The categories are represented by '##@' and the
+# target descriptions by '##'. The awk commands is responsible for reading the
+# entire set of makefiles included in this invocation, looking for lines of the
+# file as xyz: ## something, and then pretty-format the target and help. Then,
+# if there's a line with ##@ something, that gets pretty-printed as a category.
+# More info on the usage of ANSI control characters for terminal formatting:
+# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
+# More info on the awk command:
+# http://linuxcommand.org/lc3_adv_awk.php
+
+.PHONY: help
+help: ## Display this help.
+ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
+
+##@ Development
+
+.PHONY: manifests
+manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
+ $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
+ cp -r config/crd/bases/* deploy/charts/user/crds/
+
+.PHONY: generate
+generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
+ $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
+
+.PHONY: fmt
+fmt: ## Run go fmt against code.
+ go fmt ./...
+
+.PHONY: vet
+vet: ## Run go vet against code.
+ go vet ./...
+
+.PHONY: test
+test: envtest ## Run tests.
+ @LD_FLAGS=""; \
+ [ -n "$(LICENSE_KEY)" ] && LD_FLAGS="-X ${CONTROLLER_LICENSE_PKG}.encryptionKey=${LICENSE_KEY}"; \
+ KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -race -ldflags "$${LD_FLAGS}" ./... -count=1
+
+##@ Build
+
+.PHONY: build
+build: ## Build manager binary.
+ LD_FLAGS="-s -w"; \
+ [ -n "$(LICENSE_KEY)" ] && LD_FLAGS+=" -X ${CONTROLLER_LICENSE_PKG}.encryptionKey=${LICENSE_KEY}"; \
+ CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go
+
+.PHONY: run
+run: manifests generate fmt vet ## Run a controller from your host.
+ go run ./main.go
+
+.PHONY: docker-build
+docker-build: build ## Build docker image with the manager.
+ mv bin/manager bin/controller-user-${TARGETARCH}
+ chmod +x bin/controller-user-${TARGETARCH}
+ docker build -t ${IMG} . --build-arg TARGETARCH=${TARGETARCH}
+
+.PHONY: docker-push
+docker-push: ## Push docker image with the manager.
+ docker push ${IMG}
+
+##@ Deployment
+
+ifndef ignore-not-found
+ ignore-not-found = false
+endif
+
+.PHONY: install
+install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
+ $(KUSTOMIZE) build config/crd | kubectl apply -f -
+
+.PHONY: uninstall
+uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
+ $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
+
+.PHONY: deploy
+deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
+ cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
+ $(KUSTOMIZE) build config/default | kubectl apply -f -
+
+.PHONY: pre-deploy
+pre-deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
+ cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
+ $(KUSTOMIZE) build config/default > deploy/manifests/deploy.yaml
+
+
+.PHONY: undeploy
+undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
+ $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
+
+##@ Build Dependencies
+
+## Location to install dependencies to
+LOCALBIN ?= $(shell pwd)/bin
+$(LOCALBIN):
+ mkdir -p $(LOCALBIN)
+
+## Tool Binaries
+KUSTOMIZE ?= $(LOCALBIN)/kustomize
+CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
+ENVTEST ?= $(LOCALBIN)/setup-envtest
+
+## Tool Versions
+KUSTOMIZE_VERSION ?= v5.3.0
+CONTROLLER_TOOLS_VERSION ?= v0.14.0
+
+KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
+.PHONY: kustomize
+kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
+$(KUSTOMIZE): $(LOCALBIN)
+ curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN)
+
+.PHONY: controller-gen
+controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
+$(CONTROLLER_GEN): $(LOCALBIN)
+ GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
+
+.PHONY: envtest
+envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
+$(ENVTEST): $(LOCALBIN)
+ GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
diff --git a/controllers/user/PROJECT b/controllers/user/PROJECT
new file mode 100644
index 000000000000..add8ee105376
--- /dev/null
+++ b/controllers/user/PROJECT
@@ -0,0 +1,45 @@
+# Code generated by tool. DO NOT EDIT.
+# This file is used to track the info used to scaffold your project
+# and allow the plugins properly work.
+# More info: https://book.kubebuilder.io/reference/project-config.html
+domain: sealos.io
+layout:
+- go.kubebuilder.io/v3
+projectName: user
+repo: github.com/labring/sealos/controllers/user
+resources:
+- api:
+ crdVersion: v1
+ controller: true
+ domain: sealos.io
+ group: user
+ kind: User
+ path: github.com/labring/sealos/controllers/user/api/v1
+ version: v1
+ webhooks:
+ defaulting: true
+ validation: true
+ webhookVersion: v1
+- api:
+ crdVersion: v1
+ namespaced: true
+ controller: true
+ domain: sealos.io
+ group: user
+ kind: Operationrequest
+ path: github.com/labring/sealos/controllers/user/api/v1
+ version: v1
+ webhooks:
+ defaulting: true
+ validation: true
+ webhookVersion: v1
+- api:
+ crdVersion: v1
+ namespaced: true
+ controller: true
+ domain: sealos.io
+ group: user
+ kind: DeleteRequest
+ path: github.com/labring/sealos/controllers/user/api/v1
+ version: v1
+version: "3"
diff --git a/controllers/user/README.md b/controllers/user/README.md
new file mode 100644
index 000000000000..37a31b23e63e
--- /dev/null
+++ b/controllers/user/README.md
@@ -0,0 +1,475 @@
+# add user
+
+create user:
+
+```yaml
+apiVersion: user.sealos.io/v1
+kind: User
+metadata:
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+spec:
+ displayName: cuisongliu
+ csrExpirationSeconds: 1000000000
+ kubeConfigRotateAt: "2026-01-19T14:55:00Z"
+
+```
+
+show all resources:
+
+```yaml
+apiVersion: v1
+items:
+- apiVersion: user.sealos.io/v1
+ kind: User
+ metadata:
+ creationTimestamp: "2022-09-07T14:06:56Z"
+ finalizers:
+ - sealos.io/user.finalizers
+ generation: 1
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ resourceVersion: "81068"
+ uid: dc25db9a-3c07-411a-9d0f-d77a943cfd07
+ spec:
+ csrExpirationSeconds: 1000000000
+ displayName: cuisongliu
+- apiVersion: user.sealos.io/v1
+ kind: UserGroup
+ metadata:
+ annotations:
+ user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d
+ creationTimestamp: "2022-09-07T14:06:56Z"
+ finalizers:
+ - sealos.io/user.group.finalizers
+ generation: 1
+ name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+ ownerReferences:
+ - apiVersion: user.sealos.io/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: User
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ uid: dc25db9a-3c07-411a-9d0f-d77a943cfd07
+ resourceVersion: "81052"
+ uid: 2e2a0e63-8510-4527-9cd5-b36085760ed9
+- apiVersion: user.sealos.io/v1
+ kind: UserGroupBinding
+ metadata:
+ annotations:
+ user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d
+ creationTimestamp: "2022-09-07T14:06:56Z"
+ finalizers:
+ - sealos.io/user.group.binding.finalizers
+ generation: 1
+ name: ugn-f8699ded-58d3-432b-a9ff-56568b57a38d
+ ownerReferences:
+ - apiVersion: user.sealos.io/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: User
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ uid: dc25db9a-3c07-411a-9d0f-d77a943cfd07
+ resourceVersion: "81057"
+ uid: 4d0bf140-f31f-4413-a276-4810566eb873
+ roleRef: user
+ subject:
+ kind: Namespace
+ name: ns-f8699ded-58d3-432b-a9ff-56568b57a38d
+ userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+- apiVersion: user.sealos.io/v1
+ kind: UserGroupBinding
+ metadata:
+ annotations:
+ user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d
+ creationTimestamp: "2022-09-07T14:06:56Z"
+ finalizers:
+ - sealos.io/user.group.binding.finalizers
+ generation: 1
+ name: ugu-f8699ded-58d3-432b-a9ff-56568b57a38d
+ ownerReferences:
+ - apiVersion: user.sealos.io/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: UserGroup
+ name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+ uid: 2e2a0e63-8510-4527-9cd5-b36085760ed9
+ resourceVersion: "82412"
+ uid: be7d630b-5ecc-4fc8-8dff-9b25a5ea9538
+ roleRef: user
+ subject:
+ apiGroup: user.sealos.io
+ kind: User
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+kind: List
+metadata:
+ resourceVersion: ""
+```
+
+# add userGroup
+```yaml
+apiVersion: user.sealos.io/v1
+kind: UserGroup
+metadata:
+ annotations:
+ user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d
+ creationTimestamp: "2022-09-07T14:06:56Z"
+ finalizers:
+ - sealos.io/user.group.finalizers
+ generation: 1
+ name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+ ownerReferences:
+ - apiVersion: user.sealos.io/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: User
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ uid: dc25db9a-3c07-411a-9d0f-d77a943cfd07
+ resourceVersion: "81052"
+ uid: 2e2a0e63-8510-4527-9cd5-b36085760ed9
+```
+# add user to userGroup
+```yaml
+apiVersion: user.sealos.io/v1
+kind: UserGroupBinding
+metadata:
+ annotations:
+ user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d
+ creationTimestamp: "2022-09-07T14:06:56Z"
+ finalizers:
+ - sealos.io/user.group.binding.finalizers
+ generation: 1
+ name: ugu-f8699ded-58d3-432b-a9ff-56568b57a38d
+ ownerReferences:
+ - apiVersion: user.sealos.io/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: UserGroup
+ name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+ uid: 2e2a0e63-8510-4527-9cd5-b36085760ed9
+ resourceVersion: "82412"
+ uid: be7d630b-5ecc-4fc8-8dff-9b25a5ea9538
+roleRef: user
+subject:
+ apiGroup: user.sealos.io
+ kind: User
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+```
+# add namespace to userGroup
+
+```yaml
+apiVersion: user.sealos.io/v1
+kind: UserGroupBinding
+metadata:
+ annotations:
+ user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d
+ creationTimestamp: "2022-09-07T14:06:56Z"
+ finalizers:
+ - sealos.io/user.group.binding.finalizers
+ generation: 1
+ name: ugn-f8699ded-58d3-432b-a9ff-56568b57a38d
+ ownerReferences:
+ - apiVersion: user.sealos.io/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: User
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ uid: dc25db9a-3c07-411a-9d0f-d77a943cfd07
+ resourceVersion: "81057"
+ uid: 4d0bf140-f31f-4413-a276-4810566eb873
+roleRef: user
+subject:
+ kind: Namespace
+ name: ns-f8699ded-58d3-432b-a9ff-56568b57a38d
+userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+```
+
+# add manager to userGroup
+
+```yaml
+apiVersion: user.sealos.io/v1
+kind: UserGroupBinding
+metadata:
+ annotations:
+ user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d
+ creationTimestamp: "2022-09-07T14:06:56Z"
+ finalizers:
+ - sealos.io/user.group.binding.finalizers
+ generation: 1
+ name: ugu-f8699ded-58d3-432b-a9ff-56568b57a38d
+ ownerReferences:
+ - apiVersion: user.sealos.io/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: UserGroup
+ name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+ uid: 2e2a0e63-8510-4527-9cd5-b36085760ed9
+ resourceVersion: "82412"
+ uid: be7d630b-5ecc-4fc8-8dff-9b25a5ea9538
+roleRef: manager
+subject:
+ apiGroup: user.sealos.io
+ kind: User
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+```
+
+
+# add webhook
+
+```go
+
+const (
+ UserAnnotationOwnerKey = "user.sealos.io/creator"
+ UserAnnotationDisplayKey = "user.sealos.io/display-name"
+)
+
+const (
+ UgNameLabelKey = "user.sealos.io/usergroup.name"
+ UgRoleLabelKey = "user.sealos.io/usergroup.role"
+ UgBindingKindLabelKey = "user.sealos.io/usergroupbinding.kind"
+ UgBindingNameLabelKey = "user.sealos.io/usergroupbinding.name"
+)
+```
+
+```yaml
+apiVersion: v1
+items:
+- apiVersion: user.sealos.io/v1
+ kind: User
+ metadata:
+ annotations:
+ kubectl.kubernetes.io/last-applied-configuration: |
+ {"apiVersion":"user.sealos.io/v1","kind":"User","metadata":{"annotations":{},"name":"f8699ded-58d3-432b-a9ff-56568b57a38d"},"spec":{"csrExpirationSeconds":1000000000}}
+ user.sealos.io/display-name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ creationTimestamp: "2022-09-14T14:52:02Z"
+ finalizers:
+ - sealos.io/user.finalizers
+ generation: 1
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ resourceVersion: "416042"
+ uid: 48a7d9e6-5d15-4b26-923c-667d7b8c429f
+ spec:
+ csrExpirationSeconds: 1000000000
+ status:
+ conditions:
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: user has been initialized
+ reason: Initialized
+ status: "True"
+ type: Initialized
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: sync kube config successfully
+ reason: Ready
+ status: "True"
+ type: KubeConfigSyncReady
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: sync owner ug successfully
+ reason: Ready
+ status: "True"
+ type: OwnerUGSyncReady
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: sync owner ug namespace binding successfully
+ reason: Ready
+ status: "True"
+ type: OwnerUGNamespaceBindingSyncReady
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: User is available now
+ reason: Ready
+ status: "True"
+ type: Ready
+ kubeConfig: |
+ apiVersion: v1
+ clusters:
+ - cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM2VENDQWRHZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQ0FYRFRJeU1Ea3hOREExTlRRMU5Wb1lEekl4TWpJd09ESXhNRFUxTkRVMVdqQVZNUk13RVFZRApWUVFERXdwcmRXSmxjbTVsZEdWek1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCjBMSndEbndvYzV2YittMkpadVplMnB3dkhWZHpyY21SRkdIaUVmQzg4L2hscjVDWllMSkZkNVc5WjRjUFU1VjMKY1NVNG8wL1J1bmNiclNpNmZrdTNFZ0lya1Z0UUkxUm5Ub3ExTUNFeXk1Q3ZvRlpETkMwSW5wRTY2Nng2S0xpagpRZTFyRXYrYmxZc3B4MnE4Tmhtek1zZWR3ZzZod1ErM2RoQ0U5ck9Vb3UwRHZwQ2VKVjAwazkweVRmZGNTN1c0CnJCbFZ1dUhtNVlVU1M3SEZubTBFMUZQd0RONXhNaG9vZmkyVGE3eTZPNzlBZTl5cDZDZlV4cFRra28vOGNwZUIKKzI3NzRNelYyZmJ4Yys0Ri9McWpRTms2VVpIOTlTT3gvTUZJLzFCZWVqeTA1OGdtQ2U3QUlkbEpCdnFQOWRXbwp0cEtlb1ZyN3Fkc0owSEhTeUhmaVNRSURBUUFCbzBJd1FEQU9CZ05WSFE4QkFmOEVCQU1DQXFRd0R3WURWUjBUCkFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVlOW9SQmQ2UVBjNk1VNjcyQjAwOU9nR2VtVjB3RFFZSktvWkkKaHZjTkFRRUxCUUFEZ2dFQkFEYkVDMk9oWkpiUzZldUZmTFNEdG11VlNoc3dFbDRlMFBsWWhnYTBKRHlQSDBUZQpCNmdPWVhZSndrZWxhTWxTUTFFY3ZVa3FHWlhqVUw4Qnl4YzJ0K0pxUDYzQ3ZtZHVzY0NHcWQzTDZTY21UQmZvCjhhbmpENmtoSDZBaHNPVnVsODFYcU9iV2Zxd0diTytGZFgvQi81TDB4bUQ0WUtnNE9iMzdlMWxXb2xJbEJZWmYKMEhqZGNMNjFyYytyWEloeGFPMmlzWHJiSmxzQ0hwTERoVkVKM2o0cjJWdkJFbjRMYWxlZmlYdVdrU3M4RmF0Nwo3N1hwSmhIblA5SHlma0Z1UERsRmpyN1FpcE9wRnJKUXBaeHhmSmREVEhFWUpRV3V0ZytqZjhVZzQweGg5ZS9GCnBNdDlNcTVKSGt3YWVPOEhpSmhaN2dxNjJBQTdseVp4TzBDbnhUYz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
+ server: https://192.168.64.29:6443
+ name: sealos
+ contexts:
+ - context:
+ cluster: sealos
+ user: f8699ded-58d3-432b-a9ff-56568b57a38d
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d@sealos
+ current-context: f8699ded-58d3-432b-a9ff-56568b57a38d@sealos
+ kind: Config
+ preferences: {}
+ users:
+ - name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ user:
+ client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lSQU9SY1E5Nnc5UHh6MjBTQXNOKzVoa1V3RFFZSktvWklodmNOQVFFTEJRQXcKRlRFVE1CRUdBMVVFQXhNS2EzVmlaWEp1WlhSbGN6QWdGdzB5TWpBNU1UUXhORFEzTURKYUdBOHlNRFUwTURVeQpNekUyTXpNME1sb3dMekV0TUNzR0ExVUVBeE1rWmpnMk9UbGtaV1F0TlRoa015MDBNekppTFdFNVptWXROVFkxCk5qaGlOVGRoTXpoa01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnB4VHVwNHUKR0xzeVQ1a1RBeldrZGlTSUtnY09OTTkxU3FLV25CM2t1ektaKzUvWkdiT1MrWElGV3IrbEdtbkZnMUY0Sng5Ywpvb3I0K09KTnBSWHdpNElkejgvRVllbGU0cmU1bjE3enlvUHFLWFBGemdjM1pOc2d0Ni9WQm5ENVBYaUlvc3pECnI0dC9RQWh3SFA0VjVyTkIrTUJHbjdtMzJaY1UxNTlFak5sOVJjQWhNZHJWKzAvRVZNN1ZGVThVVnhzZWV1c1QKQlBvTDJOanZwek1NY0VTeFpsY3lVS1lpNG5TamtXR3ZOSnZja1BDaTFHaUhUc1lZZE1qKzRFQTZmZnRKRC9IYwprMkVCWVVVQmlWeXRtVUNRSEZIWm5rU0Eva1FwUFpHVll0Y1VTaDB1bmY4cWwyM2k1RUVsL3M1eTRjSmI0SVdmCmY3anpaWk9uTmpBVlVRSURBUUFCbzFZd1ZEQU9CZ05WSFE4QkFmOEVCQU1DQmFBd0V3WURWUjBsQkF3d0NnWUkKS3dZQkJRVUhBd0l3REFZRFZSMFRBUUgvQkFJd0FEQWZCZ05WSFNNRUdEQVdnQlI3MmhFRjNwQTl6b3hUcnZZSApUVDA2QVo2WlhUQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFGdHNUOFlaS1dNelEydUIzZVZqeVVaRXM5dEY4Cms3MXk4YVZIV2owcEVFSTgzUlpSUjNMbzg5V3l4a3p6c0N2NjZYSkdKVHRzWFlYdTVjL1FpZ3NHemhGRTB0WlAKeGRIc3Y4SzRVelZablhSc2Y5U3BWcmNTQnR6WHEveEw5bXJ4VCtLcklBTEEzYUxQTUpsRGs2bnNuNnlRc0J2egpvWHBJbWlkblVnQWNydmxMSFFSMTRaSHU3dEFaVmh5blFJTGNJRjhaUnpvYnhpclNPM0ZVUkhpMFZPaHRWMUpaCi93NzhwNVhWZ3NYT2ozUmliWm9qUkdUT2w1b0xlUHBaT0xueHZoYkNaMjBmV2FBaHliMlhNSzZiQmR6ckZUbWUKR2VPdGFUWEt2UTE3cXJZTjVKVTVKQ0JhaWFRbjEyZjA1c3E2U0NtUVJVRWxpVmQ1VERiNS9ydVpZUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
+ client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBMnB4VHVwNHVHTHN5VDVrVEF6V2tkaVNJS2djT05NOTFTcUtXbkIza3V6S1orNS9aCkdiT1MrWElGV3IrbEdtbkZnMUY0Sng5Y29vcjQrT0pOcFJYd2k0SWR6OC9FWWVsZTRyZTVuMTd6eW9QcUtYUEYKemdjM1pOc2d0Ni9WQm5ENVBYaUlvc3pEcjR0L1FBaHdIUDRWNXJOQitNQkduN20zMlpjVTE1OUVqTmw5UmNBaApNZHJWKzAvRVZNN1ZGVThVVnhzZWV1c1RCUG9MMk5qdnB6TU1jRVN4WmxjeVVLWWk0blNqa1dHdk5KdmNrUENpCjFHaUhUc1lZZE1qKzRFQTZmZnRKRC9IY2syRUJZVVVCaVZ5dG1VQ1FIRkhabmtTQS9rUXBQWkdWWXRjVVNoMHUKbmY4cWwyM2k1RUVsL3M1eTRjSmI0SVdmZjdqelpaT25OakFWVVFJREFRQUJBb0lCQUVGYkpTa1FxMFZYaWNZRQpSSXZjR0t2OVpmdnltZ3V0emlvZkM1bm0wZ0FwTTZIbmV4ZFUrV2E3OE4vZGxiV2MwNkRiMVdrVTFqUCtDa3NXCkZSSjZ5YXhiblNBSXhIUmxPYW5jTlpGdzZMK3R5bTdVSHRubWpHZ0pudkZyanV0YkVSaVFsWURlYU01dGpRQ08KeFQzVnRLU0JDUU1lTUw4RjNmSWVXZXZRd01TK24wUU5TVnQ4R1lLblptVDVJaUQyU2FtcUFLQW1vRW9kc09GRwpDR3JoUm5jL2MydVJGQzhwdms0aFZCWURSTEV1VGhNU3gvY1JRbjhTdTJKaktCNWRndnZXVHVxL3NJSjQzOWwrCncxWUpSZTMreWlXVTF5Y3lCNm5aOG5hT0RRc1RpTnVudkpQd2tvYUF3U3RxZzVYTGtXTXBiUVFKVWo5ZktzaHgKVURpdThVRUNnWUVBOW4xTkE1d0M0clE1RnVQMHZxMXdZKzNsaitSSzZBT0ZVd085VkN0S2FXUGZqT1JsaWxQNwpmSTFpUUc2V2MxbG9kTHVDVjFQZm9icmFyZ3p0M0hJV213dlpnMVRLTldZTVNwTGMzMUhnN0FlMkU2bHZGNnloCmFweU9saDdIRU1NV3Aybm55d0FaSWdWM1EyUWdUNHVIY1lYU3ZpbCtqQmVWZUQ4NzlJM1dXVmtDZ1lFQTR3dW4KUk80QmwyTjByNXJhRXJoTVRjT2QwYXFxV29lYWpEMHhIaXVTRWlRTytqYU1HU2theWNURzQ4UGc3Vk1LaTVPNQpiN1dydVJ3NFVNVFRaK3ZGRTRURjdDR0lBcmJ2bEhPRExxaDRJeWQyY1BJcEtQc2xpZ1F3VDhKUkt0VmtoVXNoCnpPRTgreHgvVXBzQ1NpVU14S3Z0aGZFcW5IK0ZhR3NiYW85aEpMa0NnWUVBcDA2OXE2bUZ1K09nckl6bVgxYlMKWnNIZmhCL3RTRGE3bEhHQlhWUlZHZHdVclVtS01xNTJrVUJXWkcycjZYZlNrd21EbTBydkRKUU1Rdis3cEZvOApSZGx4TnRlaXVVUWZLNjhzQk5sSzFtc1ZoNXNHTVFlWU9Ra3pMMnFNckMvL2ZIdENQbVErcXYwU3lzZDN0d1o2ClZQQUU3OWF5Y3R5L00vT1grcE1iMzJFQ2dZQkRhcjVzVlUvYnFBaUo3QzA4eWNTK2dvdG1Lb3NTL3BrQmMxb1cKWDFHc1dWQ3BNM0UrTjFwZzl6RW5pSTVOYWc4RGl3WFJDZHpIeG4xaVhrOTMyQ1pZdVdBYjBZa1ZaVVNXakFZego0SXByeGRnQmlhWjNLTHZvMDMyVThWb3dvZFNMVDlmbGdpd2RWRVRxUG1UYnAweTNTV1hxRThaWFdmWS9IeTZ3CjRZb3k0UUtCZ0ZvYmJHdVBhQ0QwT044dmk3TnhGZStzbGNEWlBOTWgydXRZRWFwQnpsTkNDNFB2eXFuTjlTdDgKLzhJSmRVc0M3MVNJYU4zZWJWQmUzKzdieFk0MzVwN0hEVXlGdkRQbTJnOHUvelVPbHU5SzNjZXVQRExJcDVoOAp1MG9oaHFFZUN5cHRFcDFBM1hTNGFRaVlsb3NUTkxrenlKRXM5cnh0aWdHT3ZoOXZKVjRtCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
+ observedCSRExpirationSeconds: 1000000000
+ observedGeneration: 1
+ phase: Active
+- apiVersion: user.sealos.io/v1
+ kind: UserGroup
+ metadata:
+ annotations:
+ user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d
+ user.sealos.io/display-name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+ creationTimestamp: "2022-09-14T14:52:02Z"
+ finalizers:
+ - sealos.io/user.group.finalizers
+ generation: 1
+ name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+ ownerReferences:
+ - apiVersion: user.sealos.io/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: User
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ uid: 48a7d9e6-5d15-4b26-923c-667d7b8c429f
+ resourceVersion: "416046"
+ uid: c0b1faf8-3cc4-4817-bd28-e508eb3ab0f4
+ status:
+ conditions:
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: user group has been initialized
+ reason: Initialized
+ status: "True"
+ type: Initialized
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: sync owner ug user binding successfully
+ reason: Ready
+ status: "True"
+ type: OwnerUGUserBindingSyncReady
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: UserGroup is available now
+ reason: Ready
+ status: "True"
+ type: Ready
+ observedGeneration: 1
+ phase: Active
+- apiVersion: user.sealos.io/v1
+ kind: UserGroupBinding
+ metadata:
+ annotations:
+ user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d
+ creationTimestamp: "2022-09-14T14:52:02Z"
+ finalizers:
+ - sealos.io/user.group.binding.finalizers
+ generation: 1
+ labels:
+ user.sealos.io/usergroup.name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+ user.sealos.io/usergroup.role: user
+ user.sealos.io/usergroupbinding.kind: namespace
+ user.sealos.io/usergroupbinding.name: ns-f8699ded-58d3-432b-a9ff-56568b57a38d
+ name: ugn-f8699ded-58d3-432b-a9ff-56568b57a38d
+ ownerReferences:
+ - apiVersion: user.sealos.io/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: User
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ uid: 48a7d9e6-5d15-4b26-923c-667d7b8c429f
+ resourceVersion: "422287"
+ uid: 3ac62863-d9a1-4159-bc45-fe4c71d691de
+ roleRef: user
+ status:
+ conditions:
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: user group binding has been initialized
+ reason: Initialized
+ status: "True"
+ type: Initialized
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: sync ug namespace successfully
+ reason: Ready
+ status: "True"
+ type: UGNamespaceSyncReady
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: sync ug namespace binding successfully
+ reason: Ready
+ status: "True"
+ type: UGNamespaceBindingSyncReady
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: UserGroupBinding is available now
+ reason: Ready
+ status: "True"
+ type: Ready
+ observedGeneration: 1
+ phase: Active
+ subject:
+ kind: Namespace
+ name: ns-f8699ded-58d3-432b-a9ff-56568b57a38d
+ userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+- apiVersion: user.sealos.io/v1
+ kind: UserGroupBinding
+ metadata:
+ annotations:
+ user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d
+ creationTimestamp: "2022-09-14T14:52:02Z"
+ finalizers:
+ - sealos.io/user.group.binding.finalizers
+ generation: 1
+ labels:
+ user.sealos.io/usergroup.name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+ user.sealos.io/usergroup.role: user
+ user.sealos.io/usergroupbinding.kind: user
+ user.sealos.io/usergroupbinding.name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ name: ugu-f8699ded-58d3-432b-a9ff-56568b57a38d
+ ownerReferences:
+ - apiVersion: user.sealos.io/v1
+ blockOwnerDeletion: true
+ controller: true
+ kind: UserGroup
+ name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+ uid: c0b1faf8-3cc4-4817-bd28-e508eb3ab0f4
+ resourceVersion: "419386"
+ uid: b6cbea8a-a248-4bb1-a9ae-30ba39db072f
+ roleRef: user
+ status:
+ conditions:
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: user group binding has been initialized
+ reason: Initialized
+ status: "True"
+ type: Initialized
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: sync ug user binding by owner successfully
+ reason: Ready
+ status: "True"
+ type: UGUserBindingSyncReadyByOwner
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: sync ug user binding successfully
+ reason: Ready
+ status: "True"
+ type: UGUserBindingSyncReady
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: sync ug namespace binding successfully
+ reason: Ready
+ status: "True"
+ type: UGNamespaceBindingSyncReady
+ - lastHeartbeatTime: "2022-09-14T14:52:00Z"
+ lastTransitionTime: "2022-09-14T14:52:00Z"
+ message: UserGroupBinding is available now
+ reason: Ready
+ status: "True"
+ type: Ready
+ observedGeneration: 1
+ phase: Active
+ subject:
+ apiGroup: user.sealos.io
+ kind: User
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d
+kind: List
+metadata:
+ resourceVersion: ""
+```
diff --git a/controllers/user/api/v1/deleterequest_types.go b/controllers/user/api/v1/deleterequest_types.go
new file mode 100644
index 000000000000..7b4ca3dd1fb2
--- /dev/null
+++ b/controllers/user/api/v1/deleterequest_types.go
@@ -0,0 +1,61 @@
+/*
+Copyright 2022 labring.
+
+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 v1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// DeleteRequestSpec defines the desired state of DeleteRequest
+type DeleteRequestSpec struct {
+ User string `json:"user,omitempty"`
+}
+
+// DeleteRequestStatus defines the observed state of DeleteRequest
+type DeleteRequestStatus struct {
+ //+kubebuilder:validation:Enum=Pending;Processing;Completed;Failed
+ Phase RequestPhase `json:"phase,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:resource:scope=Cluster
+//+kubebuilder:printcolumn:name="User",type="string",JSONPath=".spec.user"
+//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
+//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
+
+// DeleteRequest is the Schema for the deleterequests API
+type DeleteRequest struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec DeleteRequestSpec `json:"spec,omitempty"`
+ Status DeleteRequestStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// DeleteRequestList contains a list of DeleteRequest
+type DeleteRequestList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []DeleteRequest `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&DeleteRequest{}, &DeleteRequestList{})
+}
diff --git a/controllers/user/api/v1/groupversion_info.go b/controllers/user/api/v1/groupversion_info.go
new file mode 100644
index 000000000000..cfdf8b013608
--- /dev/null
+++ b/controllers/user/api/v1/groupversion_info.go
@@ -0,0 +1,72 @@
+/*
+Copyright 2022 labring.
+
+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 v1 contains API Schema definitions for the user v1 API group
+// +kubebuilder:object:generate=true
+// +groupName=user.sealos.io
+package v1
+
+import (
+ "fmt"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "sigs.k8s.io/controller-runtime/pkg/scheme"
+)
+
+var (
+ // GroupVersion is group version used to register these objects
+ GroupVersion = schema.GroupVersion{Group: "user.sealos.io", Version: "v1"}
+
+ // SchemeBuilder is used to add go types to the GroupVersionKind scheme
+ SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
+
+ // AddToScheme adds the types in this group-version to the given scheme.
+ AddToScheme = SchemeBuilder.AddToScheme
+)
+
+const (
+ // UserAnnotationCreatorKey refers to the user who created the resource
+ UserAnnotationCreatorKey = "user.sealos.io/creator"
+ // UserAnnotationOwnerKey refers to the user who owns the resource
+ UserAnnotationOwnerKey = "user.sealos.io/owner"
+ UserLabelOwnerKey = "user.sealos.io/owner"
+ UserAnnotationDisplayKey = "user.sealos.io/display-name"
+)
+
+const (
+ UgNameLabelKey = "user.sealos.io/usergroup.name"
+ UgRoleLabelKey = "user.sealos.io/usergroup.role"
+ UgBindingKindLabelKey = "user.sealos.io/usergroupbinding.kind"
+ UgBindingNameLabelKey = "user.sealos.io/usergroupbinding.name"
+)
+
+func validateAnnotationKeyNotEmpty(meta metav1.ObjectMeta, key string) error {
+ if meta.Annotations[key] == "" {
+ return fmt.Errorf("annotation %s not allow empty", key)
+ }
+ return nil
+}
+
+func initAnnotationAndLabels(meta metav1.ObjectMeta) metav1.ObjectMeta {
+ if meta.Annotations == nil {
+ meta.Annotations = make(map[string]string, 0)
+ }
+ if meta.Labels == nil {
+ meta.Labels = make(map[string]string, 0)
+ }
+ return meta
+}
diff --git a/controllers/user/api/v1/operationrequest_types.go b/controllers/user/api/v1/operationrequest_types.go
new file mode 100644
index 000000000000..8b0d2adf2940
--- /dev/null
+++ b/controllers/user/api/v1/operationrequest_types.go
@@ -0,0 +1,89 @@
+/*
+Copyright 2022 labring.
+
+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 v1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// OperationrequestSpec defines the desired state of Operationrequest
+type OperationrequestSpec struct {
+ // Namespace is the workspace that needs to be operated.
+ Namespace string `json:"namespace,omitempty"`
+ User string `json:"user,omitempty"`
+ // +kubebuilder:validation:Enum=Owner;Manager;Developer
+ Role RoleType `json:"role,omitempty"`
+ // +kubebuilder:validation:Enum=Grant;Update;Deprive
+ Action ActionType `json:"action,omitempty"`
+}
+
+type ActionType string
+
+const (
+ Grant ActionType = "Grant"
+ Update ActionType = "Update"
+ Deprive ActionType = "Deprive"
+)
+
+// OperationrequestStatus defines the observed state of Operationrequest
+type OperationrequestStatus struct {
+ // Phase is the recently observed lifecycle phase of operationrequest.
+ //+kubebuilder:default:=Pending
+ //+kubebuilder:validation:Enum=Pending;Processing;Completed;Failed
+ Phase RequestPhase `json:"phase,omitempty"`
+}
+
+type RequestPhase string
+
+// These are the valid phases of node.
+const (
+ RequestPending RequestPhase = "Pending"
+ RequestProcessing RequestPhase = "Processing"
+ RequestCompleted RequestPhase = "Completed"
+ RequestFailed RequestPhase = "Failed"
+)
+
+//+kubebuilder:printcolumn:name="Action",type="string",JSONPath=".spec.action"
+//+kubebuilder:printcolumn:name="Namespace",type="string",JSONPath=".spec.namespace"
+//+kubebuilder:printcolumn:name="User",type="string",JSONPath=".spec.user"
+//+kubebuilder:printcolumn:name="Role",type="string",JSONPath=".spec.role"
+//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
+//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
+//+kubebuilder:object:root=true
+//+kubebuilder:subresource:status
+
+// Operationrequest is the Schema for the operation requests API
+type Operationrequest struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec OperationrequestSpec `json:"spec,omitempty"`
+ Status OperationrequestStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// OperationrequestList contains a list of Operationrequest
+type OperationrequestList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []Operationrequest `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&Operationrequest{}, &OperationrequestList{})
+}
diff --git a/controllers/user/api/v1/operationrequest_webhook.go b/controllers/user/api/v1/operationrequest_webhook.go
new file mode 100644
index 000000000000..dff8210dde4a
--- /dev/null
+++ b/controllers/user/api/v1/operationrequest_webhook.go
@@ -0,0 +1,137 @@
+/*
+Copyright 2022 labring.
+
+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 v1
+
+import (
+ "context"
+ "errors"
+
+ "k8s.io/apimachinery/pkg/runtime"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+)
+
+// log is for logging in this package.
+var operationrequestlog = logf.Log.WithName("operationrequest-resource")
+
+func (r *Operationrequest) SetupWebhookWithManager(mgr ctrl.Manager) error {
+ m := &ReqMutator{Client: mgr.GetClient()}
+ v := &ReqValidator{Client: mgr.GetClient()}
+ return ctrl.NewWebhookManagedBy(mgr).
+ For(r).
+ WithDefaulter(m).
+ WithValidator(v).
+ Complete()
+}
+
+// +kubebuilder:webhook:path=/mutate-user-sealos-io-v1-operationrequest,mutating=true,failurePolicy=fail,sideEffects=None,groups=user.sealos.io,resources=operationrequests,verbs=create;update,versions=v1,name=moperationrequest.kb.io,admissionReviewVersions=v1
+//+kubebuilder:object:generate=false
+
+type ReqMutator struct {
+ client.Client
+}
+
+func (r ReqMutator) Default(_ context.Context, obj runtime.Object) error {
+ req, ok := obj.(*Operationrequest)
+ if !ok {
+ return errors.New("obj convert Operationrequest is error")
+ }
+ // mutate the request with an owner label
+ operationrequestlog.Info("mutate", "name", req.Name)
+ req.ObjectMeta = initAnnotationAndLabels(req.ObjectMeta)
+ req.Labels[UserLabelOwnerKey] = req.Spec.User
+ return nil
+}
+
+//+kubebuilder:webhook:path=/validate-user-sealos-io-v1-operationrequest,mutating=false,failurePolicy=fail,sideEffects=None,groups=user.sealos.io,resources=operationrequests,verbs=create;update,versions=v1,name=voperationrequest.kb.io,admissionReviewVersions=v1
+//+kubebuilder:object:generate=false
+
+type ReqValidator struct {
+ client.Client
+}
+
+func (r ReqValidator) ValidateCreate(
+ ctx context.Context,
+ obj runtime.Object,
+) (admission.Warnings, error) {
+ req, ok := obj.(*Operationrequest)
+ if !ok {
+ message := "obj convert Operationrequest is error"
+ return admission.Warnings{message}, errors.New(message)
+ }
+
+ // todo check request, _ := admission.RequestFromContext(ctx), request.UserInfo.Username if legal
+
+ // list all requests in the same namespace with a same owner
+ var reqList OperationrequestList
+ err := r.List(
+ ctx,
+ &reqList,
+ client.InNamespace(req.Namespace),
+ client.MatchingLabels{UserLabelOwnerKey: req.Spec.User},
+ )
+ if client.IgnoreNotFound(err) != nil {
+ operationrequestlog.Error(err, "list operationrequest error")
+ return admission.Warnings{"list operationrequest error"}, err
+ }
+
+ for _, item := range reqList.Items {
+ if item.Status.Phase != RequestCompleted {
+ operationrequestlog.Info(
+ "there is a request not completed, can not create new request",
+ "name",
+ item.Name,
+ "phase",
+ item.Status.Phase,
+ )
+ message := "there is a request not completed, can not create new request"
+ return admission.Warnings{message}, errors.New(message)
+ }
+ }
+ return admission.Warnings{}, nil
+}
+
+func (r ReqValidator) ValidateUpdate(
+ _ context.Context,
+ oldObj, newObj runtime.Object,
+) (admission.Warnings, error) {
+ // todo check request, _ := admission.RequestFromContext(ctx), request.UserInfo.Username if legal
+ oldReq, ok := oldObj.(*Operationrequest)
+ if !ok {
+ message := "obj convert Operationrequest error"
+ return admission.Warnings{message}, errors.New(message)
+ }
+ newReq, ok := newObj.(*Operationrequest)
+ if !ok {
+ message := "obj convert Operationrequest error"
+ return admission.Warnings{message}, errors.New(message)
+ }
+ if oldReq.Spec != newReq.Spec {
+ message := "operation request spec do not support update"
+ return admission.Warnings{message}, errors.New(message)
+ }
+ return admission.Warnings{}, nil
+}
+
+func (r ReqValidator) ValidateDelete(
+ _ context.Context,
+ _ runtime.Object,
+) (admission.Warnings, error) {
+ return admission.Warnings{}, nil
+}
diff --git a/controllers/user/api/v1/user_types.go b/controllers/user/api/v1/user_types.go
new file mode 100644
index 000000000000..78de9bc71899
--- /dev/null
+++ b/controllers/user/api/v1/user_types.go
@@ -0,0 +1,158 @@
+/*
+Copyright 2022 labring.
+
+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 v1
+
+import (
+ "errors"
+
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const DefaultCSRExpirationSeconds int32 = 1_000_000_000
+
+// NormalizeCSRExpirationSeconds returns the effective expiration duration
+// used when issuing credentials. Values below the minimum are preserved by
+// the API object and only raised at the point where they are consumed.
+func NormalizeCSRExpirationSeconds(value int32) int32 {
+ if value < DefaultCSRExpirationSeconds {
+ return DefaultCSRExpirationSeconds
+ }
+ return value
+}
+
+// UserSpec defines the desired state of User
+type UserSpec struct {
+ // expirationSeconds is the requested duration of validity of the issued
+ // kubeconfig credential. The issuer may issue a credential with a different
+ // validity duration so a client must check the issued credential to determine
+ // the actual duration.
+ //
+ // The minimum effective value for expirationSeconds is 1_000_000_000 seconds.
+ //
+ // +optional
+ //+kubebuilder:default:=1000000000
+ CSRExpirationSeconds int32 `json:"csrExpirationSeconds,omitempty"`
+ // kubeConfigRotateAt 用于手动触发 kubeconfig 轮转。
+ // 当字段被设置或更新时,controller 会重新请求 token 并重建 kubeconfig。
+ // +optional
+ KubeConfigRotateAt *metav1.Time `json:"kubeConfigRotateAt,omitempty"`
+}
+type RoleType string
+
+const (
+ OwnerRoleType RoleType = "Owner"
+ ManagerRoleType RoleType = "Manager"
+ DeveloperRoleType RoleType = "Developer"
+)
+
+type UserPhase string
+
+// These are the valid phases of node.
+const (
+ UserPending UserPhase = "Pending"
+ UserUnknown UserPhase = "Unknown"
+ UserActive UserPhase = "Active"
+)
+
+// UserStatus defines the observed state of User
+type UserStatus struct {
+ // Phase is the recently observed lifecycle phase of user
+ //+kubebuilder:default:=Unknown
+ Phase UserPhase `json:"phase,omitempty"`
+ KubeConfig string `json:"kubeConfig"`
+ //+kubebuilder:default:=1000000000
+ ObservedCSRExpirationSeconds int32 `json:"observedCSRExpirationSeconds,omitempty"`
+ // ObservedKubeConfigRotateAt 记录已处理的轮转请求时间戳。
+ // +optional
+ ObservedKubeConfigRotateAt *metav1.Time `json:"observedKubeConfigRotateAt,omitempty"`
+ // KubeConfigRefreshAt is the next time the controller should refresh the
+ // kubeconfig token. It is persisted so a controller restart keeps the
+ // token refresh schedule without retaining the kubeconfig payload in cache.
+ // +optional
+ KubeConfigRefreshAt *metav1.Time `json:"kubeConfigRefreshAt,omitempty"`
+ // ObservedKubeConfigSecretUID identifies the Secret bound to the current
+ // kubeconfig token and detects same-name Secret recreation.
+ // +optional
+ ObservedKubeConfigSecretUID string `json:"observedKubeConfigSecretUID,omitempty"`
+ // The generation observed by the user controller.
+ // +optional
+ ObservedGeneration int64 `json:"observedGeneration,omitempty"`
+ // Conditions contains the different condition statuses for this user.
+ Conditions []Condition `json:"conditions,omitempty"`
+}
+
+type ConditionType string
+
+const (
+ Initialized ConditionType = "Initialized"
+ Ready ConditionType = "Ready"
+)
+
+type Condition struct {
+ Type ConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=ConditionType"`
+ // Status is the status of the condition. One of True, False, Unknown.
+ Status v1.ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=ConditionStatus"`
+ // LastHeartbeatTime is the last time this condition was updated.
+ // +optional
+ LastHeartbeatTime metav1.Time `json:"lastHeartbeatTime,omitempty" protobuf:"bytes,3,opt,name=lastHeartbeatTime"`
+ // LastTransitionTime is the last time the condition changed from one status to another.
+ // +optional
+ LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,4,opt,name=lastTransitionTime"`
+ // Reason is a (brief) reason for the condition's last status change.
+ // +optional
+ Reason string `json:"reason,omitempty" protobuf:"bytes,5,opt,name=reason"`
+ // Message is a human-readable message indicating details about the last status change.
+ // +optional
+ Message string `json:"message,omitempty" protobuf:"bytes,6,opt,name=message"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:resource:scope=Cluster
+// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
+// +kubebuilder:printcolumn:name="ExpirationSeconds",type="integer",JSONPath=".status.observedCSRExpirationSeconds"
+// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
+
+// User is the Schema for the users API
+type User struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec UserSpec `json:"spec,omitempty"`
+ Status UserStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// UserList contains a list of User
+type UserList struct {
+ metav1.TypeMeta ` json:",inline"`
+ metav1.ListMeta ` json:"metadata,omitempty"`
+ Items []User `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&User{}, &UserList{})
+}
+
+func (r *User) validateCSRExpirationSeconds() error {
+ if r.Spec.CSRExpirationSeconds == 0 {
+ return errors.New("csrExpirationSeconds is not allowed to be 0")
+ }
+ return nil
+}
diff --git a/controllers/user/api/v1/user_webhook.go b/controllers/user/api/v1/user_webhook.go
new file mode 100644
index 000000000000..44563ce21a32
--- /dev/null
+++ b/controllers/user/api/v1/user_webhook.go
@@ -0,0 +1,162 @@
+/*
+Copyright 2022 labring.
+
+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 v1
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/labring/sealos/controllers/user/pkg/licensegate"
+ "github.com/labring/sealos/controllers/user/pkg/usercount"
+ "k8s.io/apimachinery/pkg/runtime"
+ ctrl "sigs.k8s.io/controller-runtime"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/webhook"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+)
+
+// log is for logging in this package.
+var (
+ userlog = logf.Log.WithName("user-webhook")
+ userWebhookCount *usercount.Counter
+)
+
+const (
+ licenseLimitErrorCode = 40301
+ userCountLimitErrorCode = 40302
+)
+
+func buildLicenseLimitErrorMessage() string {
+ return fmt.Sprintf(
+ "{\"code\":%d,\"message\":\"license inactive: user limit reached\"}",
+ licenseLimitErrorCode,
+ )
+}
+
+func buildUserCountLimitErrorMessage() string {
+ return fmt.Sprintf(
+ "{\"code\":%d,\"message\":\"license active: user limit reached\"}",
+ userCountLimitErrorCode,
+ )
+}
+
+func (r *User) SetupWebhookWithManager(
+ mgr ctrl.Manager,
+ userCounter *usercount.Counter,
+) error {
+ if userCounter == nil {
+ return errors.New("user webhook count cache is not initialized")
+ }
+ userWebhookCount = userCounter
+ return ctrl.NewWebhookManagedBy(mgr).
+ For(r).
+ WithDefaulter(r).
+ WithValidator(r).
+ Complete()
+}
+
+// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
+
+//+kubebuilder:webhook:path=/mutate-user-sealos-io-v1-user,mutating=true,failurePolicy=fail,sideEffects=None,groups=user.sealos.io,resources=users,verbs=create;update,versions=v1,name=muser.kb.io,admissionReviewVersions=v1
+
+var _ webhook.CustomDefaulter = &User{}
+
+// Default implements webhook.Defaulter so a webhook will be registered for the type
+func (r *User) Default(ctx context.Context, obj runtime.Object) error {
+ user, ok := obj.(*User)
+ if !ok {
+ return errors.New("obj convert User is error")
+ }
+ userlog.Info("default", "name", user.Name)
+ user.ObjectMeta = initAnnotationAndLabels(user.ObjectMeta)
+ if user.Spec.CSRExpirationSeconds == 0 {
+ user.Spec.CSRExpirationSeconds = DefaultCSRExpirationSeconds
+ }
+ if user.Annotations[UserAnnotationDisplayKey] == "" {
+ user.Annotations[UserAnnotationDisplayKey] = user.Name
+ }
+ if user.Annotations[UserAnnotationOwnerKey] == "" {
+ user.Annotations[UserAnnotationOwnerKey] = user.Name
+ }
+ return nil
+}
+
+// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
+//+kubebuilder:webhook:path=/validate-user-sealos-io-v1-user,mutating=false,failurePolicy=fail,sideEffects=None,groups=user.sealos.io,resources=users,verbs=create;update,versions=v1,name=vuser.kb.io,admissionReviewVersions=v1,timeoutSeconds=30
+
+var _ webhook.CustomValidator = &User{}
+
+// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
+func (r *User) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+ user, ok := obj.(*User)
+ if !ok {
+ return admission.Warnings{}, errors.New("obj convert User is error")
+ }
+ userlog.Info("validate create", "name", user.Name)
+ if err := user.validateCSRExpirationSeconds(); err != nil {
+ return admission.Warnings{}, err
+ }
+ if userWebhookCount == nil || !userWebhookCount.Initialized() {
+ return admission.Warnings{}, errors.New("user count cache is not initialized")
+ }
+ currentCount := userWebhookCount.Count()
+ if !licensegate.AllowNewUser(currentCount) {
+ message := buildLicenseLimitErrorMessage()
+ if licensegate.HasActiveLicense() {
+ message = buildUserCountLimitErrorMessage()
+ }
+ warnings := admission.Warnings{message}
+ return warnings, errors.New(message)
+ }
+ return admission.Warnings{}, validateAnnotationKeyNotEmpty(
+ user.ObjectMeta,
+ UserAnnotationDisplayKey,
+ )
+}
+
+// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
+func (r *User) ValidateUpdate(
+ ctx context.Context,
+ oldObj, newObj runtime.Object,
+) (admission.Warnings, error) {
+ user, ok := newObj.(*User)
+ if !ok {
+ return admission.Warnings{}, errors.New("obj convert User is error")
+ }
+ userlog.Info("validate update", "name", user.Name)
+ if err := user.validateCSRExpirationSeconds(); err != nil {
+ return admission.Warnings{}, err
+ }
+ return admission.Warnings{}, validateAnnotationKeyNotEmpty(
+ user.ObjectMeta,
+ UserAnnotationDisplayKey,
+ )
+}
+
+// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
+func (r *User) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+ user, ok := obj.(*User)
+ if !ok {
+ return admission.Warnings{}, errors.New("obj convert User is error")
+ }
+ userlog.Info("validate delete", "name", user.Name)
+ return admission.Warnings{}, validateAnnotationKeyNotEmpty(
+ user.ObjectMeta,
+ UserAnnotationDisplayKey,
+ )
+}
diff --git a/controllers/user/api/v1/webhook_suite_test.go b/controllers/user/api/v1/webhook_suite_test.go
new file mode 100644
index 000000000000..5d84f556c78f
--- /dev/null
+++ b/controllers/user/api/v1/webhook_suite_test.go
@@ -0,0 +1,153 @@
+/*
+Copyright 2022 labring.
+
+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 v1
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "net"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/labring/sealos/controllers/user/pkg/usercount"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ admissionv1beta1 "k8s.io/api/admission/v1beta1"
+ //+kubebuilder:scaffold:imports
+ "k8s.io/apimachinery/pkg/runtime"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/envtest"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+ "sigs.k8s.io/controller-runtime/pkg/webhook"
+)
+
+// These tests use Ginkgo (BDD-style Go testing framework). Refer to
+// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
+
+var (
+ k8sClient client.Client
+ testEnv *envtest.Environment
+ ctx context.Context
+ cancel context.CancelFunc
+)
+
+func TestAPIs(t *testing.T) {
+ RegisterFailHandler(Fail)
+
+ RunSpecsWithDefaultAndCustomReporters(t,
+ "Webhook Suite",
+ []Reporter{})
+}
+
+var _ = BeforeSuite(func() {
+ logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+ //nolint:fatcontext
+ ctx, cancel = context.WithCancel(context.TODO())
+
+ By("bootstrapping test environment")
+ testEnv = &envtest.Environment{
+ CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
+ ErrorIfCRDPathMissing: false,
+ WebhookInstallOptions: envtest.WebhookInstallOptions{
+ Paths: []string{filepath.Join("..", "..", "config", "webhook")},
+ },
+ }
+
+ cfg, err := testEnv.Start()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cfg).NotTo(BeNil())
+
+ scheme := runtime.NewScheme()
+ err = AddToScheme(scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ err = admissionv1beta1.AddToScheme(scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ err = admissionv1beta1.AddToScheme(scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ err = admissionv1beta1.AddToScheme(scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ //+kubebuilder:scaffold:scheme
+
+ k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient).NotTo(BeNil())
+ // start webhook server using Manager
+ webhookInstallOptions := &testEnv.WebhookInstallOptions
+ mgr, err := ctrl.NewManager(cfg, ctrl.Options{
+ Scheme: scheme,
+ LeaderElection: false,
+ WebhookServer: webhook.NewServer(webhook.Options{
+ Host: webhookInstallOptions.LocalServingHost,
+ Port: webhookInstallOptions.LocalServingPort,
+ CertDir: webhookInstallOptions.LocalServingCertDir,
+ }),
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ userCounter := usercount.NewCounter()
+ userCounter.MarkInitialized()
+ err = (&User{}).SetupWebhookWithManager(mgr, userCounter)
+ Expect(err).NotTo(HaveOccurred())
+
+ err = (&Operationrequest{}).SetupWebhookWithManager(mgr)
+ Expect(err).NotTo(HaveOccurred())
+
+ //+kubebuilder:scaffold:webhook
+
+ go func() {
+ defer GinkgoRecover()
+ err = mgr.Start(ctx)
+ Expect(err).NotTo(HaveOccurred())
+ }()
+
+ // wait for the webhook server to get ready
+ dialer := &net.Dialer{Timeout: time.Second}
+ addrPort := fmt.Sprintf(
+ "%s:%d",
+ webhookInstallOptions.LocalServingHost,
+ webhookInstallOptions.LocalServingPort,
+ )
+ Eventually(func() error {
+ conn, err := tls.DialWithDialer(
+ dialer,
+ "tcp",
+ addrPort,
+ &tls.Config{InsecureSkipVerify: true},
+ )
+ if err != nil {
+ return err
+ }
+ conn.Close()
+ return nil
+ }).Should(Succeed())
+}, 60)
+
+var _ = AfterSuite(func() {
+ cancel()
+ By("tearing down the test environment")
+ err := testEnv.Stop()
+ Expect(err).NotTo(HaveOccurred())
+})
diff --git a/controllers/user/api/v1/zz_generated.deepcopy.go b/controllers/user/api/v1/zz_generated.deepcopy.go
new file mode 100644
index 000000000000..bbc894530615
--- /dev/null
+++ b/controllers/user/api/v1/zz_generated.deepcopy.go
@@ -0,0 +1,329 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright 2022 labring.
+
+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.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1
+
+import (
+ "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Condition) DeepCopyInto(out *Condition) {
+ *out = *in
+ in.LastHeartbeatTime.DeepCopyInto(&out.LastHeartbeatTime)
+ in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition.
+func (in *Condition) DeepCopy() *Condition {
+ if in == nil {
+ return nil
+ }
+ out := new(Condition)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DeleteRequest) DeepCopyInto(out *DeleteRequest) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ out.Spec = in.Spec
+ out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeleteRequest.
+func (in *DeleteRequest) DeepCopy() *DeleteRequest {
+ if in == nil {
+ return nil
+ }
+ out := new(DeleteRequest)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *DeleteRequest) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DeleteRequestList) DeepCopyInto(out *DeleteRequestList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]DeleteRequest, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeleteRequestList.
+func (in *DeleteRequestList) DeepCopy() *DeleteRequestList {
+ if in == nil {
+ return nil
+ }
+ out := new(DeleteRequestList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *DeleteRequestList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DeleteRequestSpec) DeepCopyInto(out *DeleteRequestSpec) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeleteRequestSpec.
+func (in *DeleteRequestSpec) DeepCopy() *DeleteRequestSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(DeleteRequestSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DeleteRequestStatus) DeepCopyInto(out *DeleteRequestStatus) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeleteRequestStatus.
+func (in *DeleteRequestStatus) DeepCopy() *DeleteRequestStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(DeleteRequestStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Operationrequest) DeepCopyInto(out *Operationrequest) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ out.Spec = in.Spec
+ out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Operationrequest.
+func (in *Operationrequest) DeepCopy() *Operationrequest {
+ if in == nil {
+ return nil
+ }
+ out := new(Operationrequest)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Operationrequest) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OperationrequestList) DeepCopyInto(out *OperationrequestList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]Operationrequest, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperationrequestList.
+func (in *OperationrequestList) DeepCopy() *OperationrequestList {
+ if in == nil {
+ return nil
+ }
+ out := new(OperationrequestList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *OperationrequestList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OperationrequestSpec) DeepCopyInto(out *OperationrequestSpec) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperationrequestSpec.
+func (in *OperationrequestSpec) DeepCopy() *OperationrequestSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(OperationrequestSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OperationrequestStatus) DeepCopyInto(out *OperationrequestStatus) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperationrequestStatus.
+func (in *OperationrequestStatus) DeepCopy() *OperationrequestStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(OperationrequestStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *User) DeepCopyInto(out *User) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ out.Spec = in.Spec
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new User.
+func (in *User) DeepCopy() *User {
+ if in == nil {
+ return nil
+ }
+ out := new(User)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *User) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UserList) DeepCopyInto(out *UserList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]User, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserList.
+func (in *UserList) DeepCopy() *UserList {
+ if in == nil {
+ return nil
+ }
+ out := new(UserList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *UserList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UserSpec) DeepCopyInto(out *UserSpec) {
+ *out = *in
+ if in.KubeConfigRotateAt != nil {
+ in, out := &in.KubeConfigRotateAt, &out.KubeConfigRotateAt
+ *out = (*in).DeepCopy()
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserSpec.
+func (in *UserSpec) DeepCopy() *UserSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(UserSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UserStatus) DeepCopyInto(out *UserStatus) {
+ *out = *in
+ if in.KubeConfigRefreshAt != nil {
+ in, out := &in.KubeConfigRefreshAt, &out.KubeConfigRefreshAt
+ *out = (*in).DeepCopy()
+ }
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.ObservedKubeConfigRotateAt != nil {
+ in, out := &in.ObservedKubeConfigRotateAt, &out.ObservedKubeConfigRotateAt
+ *out = (*in).DeepCopy()
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserStatus.
+func (in *UserStatus) DeepCopy() *UserStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(UserStatus)
+ in.DeepCopyInto(out)
+ return out
+}
diff --git a/controllers/user/config/certmanager/certificate.yaml b/controllers/user/config/certmanager/certificate.yaml
new file mode 100644
index 000000000000..54c78b15e6b3
--- /dev/null
+++ b/controllers/user/config/certmanager/certificate.yaml
@@ -0,0 +1,39 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# The following manifests contain a self-signed issuer CR and a certificate CR.
+# More document can be found at https://docs.cert-manager.io
+# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes.
+apiVersion: cert-manager.io/v1
+kind: Issuer
+metadata:
+ name: selfsigned-issuer
+ namespace: system
+spec:
+ selfSigned: {}
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+ name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml
+ namespace: system
+spec:
+ # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize
+ dnsNames:
+ - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc
+ - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local
+ issuerRef:
+ kind: Issuer
+ name: selfsigned-issuer
+ secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize
diff --git a/controllers/user/config/certmanager/kustomization.yaml b/controllers/user/config/certmanager/kustomization.yaml
new file mode 100644
index 000000000000..27b9b3c07457
--- /dev/null
+++ b/controllers/user/config/certmanager/kustomization.yaml
@@ -0,0 +1,19 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+resources:
+- certificate.yaml
+
+configurations:
+- kustomizeconfig.yaml
diff --git a/controllers/user/config/certmanager/kustomizeconfig.yaml b/controllers/user/config/certmanager/kustomizeconfig.yaml
new file mode 100644
index 000000000000..a6f7e14c39fc
--- /dev/null
+++ b/controllers/user/config/certmanager/kustomizeconfig.yaml
@@ -0,0 +1,30 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# This configuration is for teaching kustomize how to update name ref and var substitution
+nameReference:
+- kind: Issuer
+ group: cert-manager.io
+ fieldSpecs:
+ - kind: Certificate
+ group: cert-manager.io
+ path: spec/issuerRef/name
+
+varReference:
+- kind: Certificate
+ group: cert-manager.io
+ path: spec/commonName
+- kind: Certificate
+ group: cert-manager.io
+ path: spec/dnsNames
diff --git a/controllers/user/config/crd/bases/user.sealos.io_deleterequests.yaml b/controllers/user/config/crd/bases/user.sealos.io_deleterequests.yaml
new file mode 100644
index 000000000000..575311e72193
--- /dev/null
+++ b/controllers/user/config/crd/bases/user.sealos.io_deleterequests.yaml
@@ -0,0 +1,84 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.14.0
+ name: deleterequests.user.sealos.io
+spec:
+ group: user.sealos.io
+ names:
+ kind: DeleteRequest
+ listKind: DeleteRequestList
+ plural: deleterequests
+ singular: deleterequest
+ scope: Cluster
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .spec.user
+ name: User
+ type: string
+ - jsonPath: .status.phase
+ name: Phase
+ type: string
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v1
+ schema:
+ openAPIV3Schema:
+ description: DeleteRequest is the Schema for the deleterequests API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: DeleteRequestSpec defines the desired state of DeleteRequest
+ properties:
+ user:
+ type: string
+ type: object
+ status:
+ description: DeleteRequestStatus defines the observed state of DeleteRequest
+ properties:
+ phase:
+ enum:
+ - Pending
+ - Processing
+ - Completed
+ - Failed
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/controllers/user/config/crd/bases/user.sealos.io_operationrequests.yaml b/controllers/user/config/crd/bases/user.sealos.io_operationrequests.yaml
new file mode 100644
index 000000000000..cfabdc44c850
--- /dev/null
+++ b/controllers/user/config/crd/bases/user.sealos.io_operationrequests.yaml
@@ -0,0 +1,110 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.14.0
+ name: operationrequests.user.sealos.io
+spec:
+ group: user.sealos.io
+ names:
+ kind: Operationrequest
+ listKind: OperationrequestList
+ plural: operationrequests
+ singular: operationrequest
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .spec.action
+ name: Action
+ type: string
+ - jsonPath: .spec.namespace
+ name: Namespace
+ type: string
+ - jsonPath: .spec.user
+ name: User
+ type: string
+ - jsonPath: .spec.role
+ name: Role
+ type: string
+ - jsonPath: .status.phase
+ name: Phase
+ type: string
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v1
+ schema:
+ openAPIV3Schema:
+ description: Operationrequest is the Schema for the operation requests API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: OperationrequestSpec defines the desired state of Operationrequest
+ properties:
+ action:
+ enum:
+ - Grant
+ - Update
+ - Deprive
+ type: string
+ namespace:
+ description: Namespace is the workspace that needs to be operated.
+ type: string
+ role:
+ enum:
+ - Owner
+ - Manager
+ - Developer
+ type: string
+ user:
+ type: string
+ type: object
+ status:
+ description: OperationrequestStatus defines the observed state of Operationrequest
+ properties:
+ phase:
+ default: Pending
+ description: Phase is the recently observed lifecycle phase of operationrequest.
+ enum:
+ - Pending
+ - Processing
+ - Completed
+ - Failed
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/controllers/user/config/crd/bases/user.sealos.io_users.yaml b/controllers/user/config/crd/bases/user.sealos.io_users.yaml
new file mode 100644
index 000000000000..9699f7cac89e
--- /dev/null
+++ b/controllers/user/config/crd/bases/user.sealos.io_users.yaml
@@ -0,0 +1,157 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.14.0
+ name: users.user.sealos.io
+spec:
+ group: user.sealos.io
+ names:
+ kind: User
+ listKind: UserList
+ plural: users
+ singular: user
+ scope: Cluster
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .status.phase
+ name: Phase
+ type: string
+ - jsonPath: .status.observedCSRExpirationSeconds
+ name: ExpirationSeconds
+ type: integer
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v1
+ schema:
+ openAPIV3Schema:
+ description: User is the Schema for the users API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: UserSpec defines the desired state of User
+ properties:
+ csrExpirationSeconds:
+ default: 1000000000
+ description: |-
+ expirationSeconds is the requested duration of validity of the issued
+ kubeconfig credential. The issuer may issue a credential with a different
+ validity duration so a client must check the issued credential to determine
+ the actual duration.
+
+
+ The minimum effective value for expirationSeconds is 1_000_000_000 seconds.
+ format: int32
+ type: integer
+ kubeConfigRotateAt:
+ description: kubeConfigRotateAt is a manual trigger for kubeconfig
+ rotation. When set/updated, controller will request a new token
+ and recreate kubeconfig.
+ format: date-time
+ type: string
+ type: object
+ status:
+ description: UserStatus defines the observed state of User
+ properties:
+ conditions:
+ description: Conditions contains the different condition statuses
+ for this user.
+ items:
+ properties:
+ lastHeartbeatTime:
+ description: LastHeartbeatTime is the last time this condition
+ was updated.
+ format: date-time
+ type: string
+ lastTransitionTime:
+ description: LastTransitionTime is the last time the condition
+ changed from one status to another.
+ format: date-time
+ type: string
+ message:
+ description: Message is a human-readable message indicating
+ details about the last status change.
+ type: string
+ reason:
+ description: Reason is a (brief) reason for the condition's
+ last status change.
+ type: string
+ status:
+ description: Status is the status of the condition. One of True,
+ False, Unknown.
+ type: string
+ type:
+ type: string
+ required:
+ - status
+ - type
+ type: object
+ type: array
+ kubeConfig:
+ type: string
+ observedCSRExpirationSeconds:
+ default: 1000000000
+ format: int32
+ type: integer
+ observedKubeConfigRotateAt:
+ description: ObservedKubeConfigRotateAt records the last processed
+ rotation request.
+ format: date-time
+ type: string
+ kubeConfigRefreshAt:
+ description: KubeConfigRefreshAt is the next time the controller
+ should refresh the kubeconfig token.
+ format: date-time
+ type: string
+ observedKubeConfigSecretUID:
+ description: ObservedKubeConfigSecretUID identifies the Secret bound
+ to the current kubeconfig token.
+ type: string
+ observedGeneration:
+ description: The generation observed by the user controller.
+ format: int64
+ type: integer
+ phase:
+ default: Unknown
+ description: Phase is the recently observed lifecycle phase of user
+ type: string
+ required:
+ - kubeConfig
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/controllers/user/config/crd/kustomization.yaml b/controllers/user/config/crd/kustomization.yaml
new file mode 100644
index 000000000000..8f613bf61efa
--- /dev/null
+++ b/controllers/user/config/crd/kustomization.yaml
@@ -0,0 +1,51 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# This kustomization.yaml is not intended to be run by itself,
+# since it depends on service name and namespace that are out of this kustomize package.
+# It should be run by config/default
+resources:
+- bases/user.sealos.io_users.yaml
+- bases/user.sealos.io_operationrequests.yaml
+- bases/user.sealos.io_deleterequests.yaml
+#+kubebuilder:scaffold:crdkustomizeresource
+
+patchesStrategicMerge:
+# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
+# patches here are for enabling the conversion webhook for each CRD
+#- patches/webhook_in_users.yaml
+#- patches/webhook_in_groups.yaml
+#- patches/webhook_in_usergroups.yaml
+#- patches/webhook_in_usergroupuserbindings.yaml
+#- patches/webhook_in_usergroupnamespacebindings.yaml
+#- patches/webhook_in_usergroupbindings.yaml
+#- patches/webhook_in_operationrequests.yaml
+#- patches/webhook_in_deleterequests.yaml
+#+kubebuilder:scaffold:crdkustomizewebhookpatch
+
+# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
+# patches here are for enabling the CA injection for each CRD
+#- patches/cainjection_in_users.yaml
+#- patches/cainjection_in_groups.yaml
+#- patches/cainjection_in_usergroups.yaml
+#- patches/cainjection_in_usergroupuserbindings.yaml
+#- patches/cainjection_in_usergroupnamespacebindings.yaml
+#- patches/cainjection_in_usergroupbindings.yaml
+#- patches/cainjection_in_operationrequests.yaml
+#- patches/cainjection_in_deleterequests.yaml
+#+kubebuilder:scaffold:crdkustomizecainjectionpatch
+
+# the following config is for teaching kustomize how to do kustomization for CRDs.
+configurations:
+- kustomizeconfig.yaml
diff --git a/controllers/user/config/crd/kustomizeconfig.yaml b/controllers/user/config/crd/kustomizeconfig.yaml
new file mode 100644
index 000000000000..9c99fc52fb20
--- /dev/null
+++ b/controllers/user/config/crd/kustomizeconfig.yaml
@@ -0,0 +1,33 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# This file is for teaching kustomize how to substitute name and namespace reference in CRD
+nameReference:
+- kind: Service
+ version: v1
+ fieldSpecs:
+ - kind: CustomResourceDefinition
+ version: v1
+ group: apiextensions.k8s.io
+ path: spec/conversion/webhook/clientConfig/service/name
+
+namespace:
+- kind: CustomResourceDefinition
+ version: v1
+ group: apiextensions.k8s.io
+ path: spec/conversion/webhook/clientConfig/service/namespace
+ create: false
+
+varReference:
+- path: metadata/annotations
diff --git a/controllers/user/config/crd/patches/cainjection_in_deleterequests.yaml b/controllers/user/config/crd/patches/cainjection_in_deleterequests.yaml
new file mode 100644
index 000000000000..f44ebfb0d7ed
--- /dev/null
+++ b/controllers/user/config/crd/patches/cainjection_in_deleterequests.yaml
@@ -0,0 +1,21 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+ name: deleterequests.user.sealos.io
diff --git a/controllers/user/config/crd/patches/cainjection_in_operationrequests.yaml b/controllers/user/config/crd/patches/cainjection_in_operationrequests.yaml
new file mode 100644
index 000000000000..2ad5a01ae7e9
--- /dev/null
+++ b/controllers/user/config/crd/patches/cainjection_in_operationrequests.yaml
@@ -0,0 +1,21 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+ name: operationrequests.user.sealos.io
diff --git a/controllers/user/config/crd/patches/cainjection_in_users.yaml b/controllers/user/config/crd/patches/cainjection_in_users.yaml
new file mode 100644
index 000000000000..7151c43f1dab
--- /dev/null
+++ b/controllers/user/config/crd/patches/cainjection_in_users.yaml
@@ -0,0 +1,21 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+ name: users.user.sealos.io
diff --git a/controllers/user/config/crd/patches/webhook_in_deleterequests.yaml b/controllers/user/config/crd/patches/webhook_in_deleterequests.yaml
new file mode 100644
index 000000000000..38178d15cd83
--- /dev/null
+++ b/controllers/user/config/crd/patches/webhook_in_deleterequests.yaml
@@ -0,0 +1,30 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: deleterequests.user.sealos.io
+spec:
+ conversion:
+ strategy: Webhook
+ webhook:
+ clientConfig:
+ service:
+ namespace: system
+ name: webhook-service
+ path: /convert
+ conversionReviewVersions:
+ - v1
diff --git a/controllers/user/config/crd/patches/webhook_in_operationrequests.yaml b/controllers/user/config/crd/patches/webhook_in_operationrequests.yaml
new file mode 100644
index 000000000000..6c60e8191110
--- /dev/null
+++ b/controllers/user/config/crd/patches/webhook_in_operationrequests.yaml
@@ -0,0 +1,30 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: operationrequests.user.sealos.io
+spec:
+ conversion:
+ strategy: Webhook
+ webhook:
+ clientConfig:
+ service:
+ namespace: system
+ name: webhook-service
+ path: /convert
+ conversionReviewVersions:
+ - v1
diff --git a/controllers/user/config/crd/patches/webhook_in_users.yaml b/controllers/user/config/crd/patches/webhook_in_users.yaml
new file mode 100644
index 000000000000..78136cab36af
--- /dev/null
+++ b/controllers/user/config/crd/patches/webhook_in_users.yaml
@@ -0,0 +1,30 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: users.user.sealos.io
+spec:
+ conversion:
+ strategy: Webhook
+ webhook:
+ clientConfig:
+ service:
+ namespace: system
+ name: webhook-service
+ path: /convert
+ conversionReviewVersions:
+ - v1
diff --git a/controllers/user/config/default/kustomization.yaml b/controllers/user/config/default/kustomization.yaml
new file mode 100644
index 000000000000..e4a0af36b1c1
--- /dev/null
+++ b/controllers/user/config/default/kustomization.yaml
@@ -0,0 +1,88 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# Adds namespace to all resources.
+namespace: user-system
+
+# Value of this field is prepended to the
+# names of all resources, e.g. a deployment named
+# "wordpress" becomes "alices-wordpress".
+# Note that it should also match with the prefix (text before '-') of the namespace
+# field above.
+namePrefix: user-
+
+# Labels to add to all resources and selectors.
+#commonLabels:
+# someName: someValue
+
+bases:
+ - ../crd
+ - ../rbac
+ - ../manager
+ # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
+ # crd/kustomization.yaml
+ - ../webhook
+ # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
+ - ../certmanager
+# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
+#- ../prometheus
+
+patchesStrategicMerge:
+ # Protect the /metrics endpoint by putting it behind auth.
+ # If you want your controller-manager to expose the /metrics
+ # endpoint w/o any authn/z, please comment the following line.
+ - manager_auth_proxy_patch.yaml
+
+ # Mount the controller config file for loading manager configurations
+ # through a ComponentConfig type
+ #- manager_config_patch.yaml
+
+ # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
+ # crd/kustomization.yaml
+ - manager_webhook_patch.yaml
+
+ # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
+ # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
+ # 'CERTMANAGER' needs to be enabled to use ca injection
+ - webhookcainjection_patch.yaml
+
+# the following config is for teaching kustomize how to do var substitution
+vars:
+ # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
+ - name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
+ objref:
+ kind: Certificate
+ group: cert-manager.io
+ version: v1
+ name: serving-cert # this name should match the one in certificate.yaml
+ fieldref:
+ fieldpath: metadata.namespace
+ - name: CERTIFICATE_NAME
+ objref:
+ kind: Certificate
+ group: cert-manager.io
+ version: v1
+ name: serving-cert # this name should match the one in certificate.yaml
+ - name: SERVICE_NAMESPACE # namespace of the service
+ objref:
+ kind: Service
+ version: v1
+ name: webhook-service
+ fieldref:
+ fieldpath: metadata.namespace
+ - name: SERVICE_NAME
+ objref:
+ kind: Service
+ version: v1
+ name: webhook-service
diff --git a/controllers/user/config/default/manager_auth_proxy_patch.yaml b/controllers/user/config/default/manager_auth_proxy_patch.yaml
new file mode 100644
index 000000000000..56f9120ae7df
--- /dev/null
+++ b/controllers/user/config/default/manager_auth_proxy_patch.yaml
@@ -0,0 +1,63 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# This patch inject a sidecar container which is a HTTP proxy for the
+# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: controller-manager
+ namespace: system
+spec:
+ template:
+ spec:
+ containers:
+ - name: kube-rbac-proxy
+ securityContext:
+ runAsNonRoot: true
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - "ALL"
+ image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0
+ args:
+ - "--secure-listen-address=0.0.0.0:8443"
+ - "--upstream=http://127.0.0.1:8080/"
+ - "--logtostderr=true"
+ - "--v=0"
+ ports:
+ - containerPort: 8443
+ protocol: TCP
+ name: https
+ resources:
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ requests:
+ cpu: 5m
+ memory: 64Mi
+ - name: manager
+ imagePullPolicy: Always
+ args:
+ - "--health-probe-bind-address=:8081"
+ - "--metrics-bind-address=127.0.0.1:8080"
+ - "--leader-elect"
+ - "--kube-api-qps=5"
+ - "--kube-api-burst=10"
+ env:
+ - name: NAMESPACE_NAME
+ valueFrom:
+ fieldRef:
+ apiVersion: v1
+ fieldPath: metadata.namespace
diff --git a/controllers/user/config/default/manager_config_patch.yaml b/controllers/user/config/default/manager_config_patch.yaml
new file mode 100644
index 000000000000..ca4883fc5ce5
--- /dev/null
+++ b/controllers/user/config/default/manager_config_patch.yaml
@@ -0,0 +1,34 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: controller-manager
+ namespace: system
+spec:
+ template:
+ spec:
+ containers:
+ - name: manager
+ args:
+ - "--config=controller_manager_config.yaml"
+ volumeMounts:
+ - name: manager-config
+ mountPath: /controller_manager_config.yaml
+ subPath: controller_manager_config.yaml
+ volumes:
+ - name: manager-config
+ configMap:
+ name: manager-config
diff --git a/controllers/user/config/default/manager_webhook_patch.yaml b/controllers/user/config/default/manager_webhook_patch.yaml
new file mode 100644
index 000000000000..ade33aa9d5f0
--- /dev/null
+++ b/controllers/user/config/default/manager_webhook_patch.yaml
@@ -0,0 +1,37 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: controller-manager
+ namespace: system
+spec:
+ template:
+ spec:
+ containers:
+ - name: manager
+ ports:
+ - containerPort: 9443
+ name: webhook-server
+ protocol: TCP
+ volumeMounts:
+ - mountPath: /tmp/k8s-webhook-server/serving-certs
+ name: cert
+ readOnly: true
+ volumes:
+ - name: cert
+ secret:
+ defaultMode: 420
+ secretName: webhook-server-cert
diff --git a/controllers/user/config/default/webhookcainjection_patch.yaml b/controllers/user/config/default/webhookcainjection_patch.yaml
new file mode 100644
index 000000000000..b9ec7b20d96e
--- /dev/null
+++ b/controllers/user/config/default/webhookcainjection_patch.yaml
@@ -0,0 +1,29 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# This patch add annotation to admission webhook config and
+# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize.
+apiVersion: admissionregistration.k8s.io/v1
+kind: MutatingWebhookConfiguration
+metadata:
+ name: mutating-webhook-configuration
+ annotations:
+ cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+ name: validating-webhook-configuration
+ annotations:
+ cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
diff --git a/controllers/user/config/manager/controller_manager_config.yaml b/controllers/user/config/manager/controller_manager_config.yaml
new file mode 100644
index 000000000000..d9a78f0c1df6
--- /dev/null
+++ b/controllers/user/config/manager/controller_manager_config.yaml
@@ -0,0 +1,35 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: controller-runtime.sigs.k8s.io/v1alpha1
+kind: ControllerManagerConfig
+health:
+ healthProbeBindAddress: :8081
+metrics:
+ bindAddress: 127.0.0.1:8080
+webhook:
+ port: 9443
+leaderElection:
+ leaderElect: true
+ resourceName: 785548a1.sealos.io
+# leaderElectionReleaseOnCancel defines if the leader should step down volume
+# when the Manager ends. This requires the binary to immediately end when the
+# Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
+# speeds up voluntary leader transitions as the new leader don't have to wait
+# LeaseDuration time first.
+# In the default scaffold provided, the program ends immediately after
+# the manager stops, so would be fine to enable this option. However,
+# if you are doing or is intended to do any operation such as perform cleanups
+# after the manager stops then its usage might be unsafe.
+# leaderElectionReleaseOnCancel: true
diff --git a/controllers/user/config/manager/kustomization.yaml b/controllers/user/config/manager/kustomization.yaml
new file mode 100644
index 000000000000..ed9883f73056
--- /dev/null
+++ b/controllers/user/config/manager/kustomization.yaml
@@ -0,0 +1,28 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+resources:
+- manager.yaml
+generatorOptions:
+ disableNameSuffixHash: true
+configMapGenerator:
+- files:
+ - controller_manager_config.yaml
+ name: manager-config
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+images:
+- name: controller
+ newName: ghcr.io/labring/sealos-user-controller
+ newTag: latest
diff --git a/controllers/user/config/manager/manager.yaml b/controllers/user/config/manager/manager.yaml
new file mode 100644
index 000000000000..38eeb66fef83
--- /dev/null
+++ b/controllers/user/config/manager/manager.yaml
@@ -0,0 +1,86 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: v1
+kind: Namespace
+metadata:
+ labels:
+ control-plane: controller-manager
+ name: system
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: controller-manager
+ namespace: system
+ labels:
+ control-plane: controller-manager
+spec:
+ selector:
+ matchLabels:
+ control-plane: controller-manager
+ replicas: 1
+ template:
+ metadata:
+ annotations:
+ kubectl.kubernetes.io/default-container: manager
+ labels:
+ control-plane: controller-manager
+ spec:
+ securityContext:
+ runAsNonRoot: true
+ # TODO(user): For common cases that do not require escalating privileges
+ # it is recommended to ensure that all your Pods/Containers are restrictive.
+ # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
+ # Please uncomment the following code if your project does NOT have to work on old Kubernetes
+ # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ).
+ # seccompProfile:
+ # type: RuntimeDefault
+ containers:
+ - command:
+ - /manager
+ args:
+ - --leader-elect
+ image: controller:latest
+ imagePullPolicy: IfNotPresent
+ name: manager
+ securityContext:
+ runAsNonRoot: true
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - "ALL"
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 8081
+ initialDelaySeconds: 15
+ periodSeconds: 20
+ readinessProbe:
+ httpGet:
+ path: /readyz
+ port: 8081
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ # TODO(user): Configure the resources accordingly based on the project requirements.
+ # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
+ resources:
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ requests:
+ cpu: 10m
+ memory: 64Mi
+ serviceAccountName: controller-manager
+ terminationGracePeriodSeconds: 10
diff --git a/controllers/user/config/prometheus/kustomization.yaml b/controllers/user/config/prometheus/kustomization.yaml
new file mode 100644
index 000000000000..a32baf71b1b4
--- /dev/null
+++ b/controllers/user/config/prometheus/kustomization.yaml
@@ -0,0 +1,16 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+resources:
+- monitor.yaml
diff --git a/controllers/user/config/prometheus/monitor.yaml b/controllers/user/config/prometheus/monitor.yaml
new file mode 100644
index 000000000000..32401c3d7fdc
--- /dev/null
+++ b/controllers/user/config/prometheus/monitor.yaml
@@ -0,0 +1,34 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+
+# Prometheus Monitor Service (Metrics)
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+ labels:
+ control-plane: controller-manager
+ name: controller-manager-metrics-monitor
+ namespace: system
+spec:
+ endpoints:
+ - path: /metrics
+ port: https
+ scheme: https
+ bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
+ tlsConfig:
+ insecureSkipVerify: true
+ selector:
+ matchLabels:
+ control-plane: controller-manager
diff --git a/controllers/user/config/rbac/auth_proxy_client_clusterrole.yaml b/controllers/user/config/rbac/auth_proxy_client_clusterrole.yaml
new file mode 100644
index 000000000000..0e421adaba5d
--- /dev/null
+++ b/controllers/user/config/rbac/auth_proxy_client_clusterrole.yaml
@@ -0,0 +1,23 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: metrics-reader
+rules:
+- nonResourceURLs:
+ - "/metrics"
+ verbs:
+ - get
diff --git a/controllers/user/config/rbac/auth_proxy_role.yaml b/controllers/user/config/rbac/auth_proxy_role.yaml
new file mode 100644
index 000000000000..7539c6cd1e86
--- /dev/null
+++ b/controllers/user/config/rbac/auth_proxy_role.yaml
@@ -0,0 +1,31 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: proxy-role
+rules:
+- apiGroups:
+ - authentication.k8s.io
+ resources:
+ - tokenreviews
+ verbs:
+ - create
+- apiGroups:
+ - authorization.k8s.io
+ resources:
+ - subjectaccessreviews
+ verbs:
+ - create
diff --git a/controllers/user/config/rbac/auth_proxy_role_binding.yaml b/controllers/user/config/rbac/auth_proxy_role_binding.yaml
new file mode 100644
index 000000000000..84c16ebd8d6a
--- /dev/null
+++ b/controllers/user/config/rbac/auth_proxy_role_binding.yaml
@@ -0,0 +1,26 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: proxy-rolebinding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: proxy-role
+subjects:
+- kind: ServiceAccount
+ name: controller-manager
+ namespace: system
diff --git a/controllers/user/config/rbac/auth_proxy_service.yaml b/controllers/user/config/rbac/auth_proxy_service.yaml
new file mode 100644
index 000000000000..70e47bf67833
--- /dev/null
+++ b/controllers/user/config/rbac/auth_proxy_service.yaml
@@ -0,0 +1,29 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ control-plane: controller-manager
+ name: controller-manager-metrics-service
+ namespace: system
+spec:
+ ports:
+ - name: https
+ port: 8443
+ protocol: TCP
+ targetPort: https
+ selector:
+ control-plane: controller-manager
diff --git a/controllers/user/config/rbac/deleterequest_editor_role.yaml b/controllers/user/config/rbac/deleterequest_editor_role.yaml
new file mode 100644
index 000000000000..cdced0fa324a
--- /dev/null
+++ b/controllers/user/config/rbac/deleterequest_editor_role.yaml
@@ -0,0 +1,45 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions for end users to edit deleterequests.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: clusterrole
+ app.kubernetes.io/instance: deleterequest-editor-role
+ app.kubernetes.io/component: rbac
+ app.kubernetes.io/created-by: user
+ app.kubernetes.io/part-of: user
+ app.kubernetes.io/managed-by: kustomize
+ name: deleterequest-editor-role
+rules:
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - deleterequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - deleterequests/status
+ verbs:
+ - get
diff --git a/controllers/user/config/rbac/deleterequest_viewer_role.yaml b/controllers/user/config/rbac/deleterequest_viewer_role.yaml
new file mode 100644
index 000000000000..78e0cc001e38
--- /dev/null
+++ b/controllers/user/config/rbac/deleterequest_viewer_role.yaml
@@ -0,0 +1,41 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions for end users to view deleterequests.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: clusterrole
+ app.kubernetes.io/instance: deleterequest-viewer-role
+ app.kubernetes.io/component: rbac
+ app.kubernetes.io/created-by: user
+ app.kubernetes.io/part-of: user
+ app.kubernetes.io/managed-by: kustomize
+ name: deleterequest-viewer-role
+rules:
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - deleterequests
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - deleterequests/status
+ verbs:
+ - get
diff --git a/controllers/user/config/rbac/kustomization.yaml b/controllers/user/config/rbac/kustomization.yaml
new file mode 100644
index 000000000000..9ea536e0fd4f
--- /dev/null
+++ b/controllers/user/config/rbac/kustomization.yaml
@@ -0,0 +1,32 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+resources:
+# All RBAC will be applied under this service account in
+# the deployment namespace. You may comment out this resource
+# if your manager will use a service account that exists at
+# runtime. Be sure to update RoleBinding and ClusterRoleBinding
+# subjects if changing service account names.
+- service_account.yaml
+- role.yaml
+- role_binding.yaml
+- leader_election_role.yaml
+- leader_election_role_binding.yaml
+# Comment the following 4 lines if you want to disable
+# the auth proxy (https://github.com/brancz/kube-rbac-proxy)
+# which protects your /metrics endpoint.
+- auth_proxy_service.yaml
+- auth_proxy_role.yaml
+- auth_proxy_role_binding.yaml
+- auth_proxy_client_clusterrole.yaml
diff --git a/controllers/user/config/rbac/leader_election_role.yaml b/controllers/user/config/rbac/leader_election_role.yaml
new file mode 100644
index 000000000000..7ccba03c9765
--- /dev/null
+++ b/controllers/user/config/rbac/leader_election_role.yaml
@@ -0,0 +1,51 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions to do leader election.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: leader-election-role
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - configmaps
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+- apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
diff --git a/controllers/user/config/rbac/leader_election_role_binding.yaml b/controllers/user/config/rbac/leader_election_role_binding.yaml
new file mode 100644
index 000000000000..493b75ed3993
--- /dev/null
+++ b/controllers/user/config/rbac/leader_election_role_binding.yaml
@@ -0,0 +1,26 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: leader-election-rolebinding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: leader-election-role
+subjects:
+- kind: ServiceAccount
+ name: controller-manager
+ namespace: system
diff --git a/controllers/user/config/rbac/operationrequest_editor_role.yaml b/controllers/user/config/rbac/operationrequest_editor_role.yaml
new file mode 100644
index 000000000000..629a0aafb9a5
--- /dev/null
+++ b/controllers/user/config/rbac/operationrequest_editor_role.yaml
@@ -0,0 +1,45 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions for end users to edit operationrequests.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: clusterrole
+ app.kubernetes.io/instance: operationrequest-editor-role
+ app.kubernetes.io/component: rbac
+ app.kubernetes.io/created-by: user
+ app.kubernetes.io/part-of: user
+ app.kubernetes.io/managed-by: kustomize
+ name: operationrequest-editor-role
+rules:
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - operationrequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - operationrequests/status
+ verbs:
+ - get
diff --git a/controllers/user/config/rbac/operationrequest_viewer_role.yaml b/controllers/user/config/rbac/operationrequest_viewer_role.yaml
new file mode 100644
index 000000000000..1cd005063f39
--- /dev/null
+++ b/controllers/user/config/rbac/operationrequest_viewer_role.yaml
@@ -0,0 +1,41 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions for end users to view operationrequests.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: clusterrole
+ app.kubernetes.io/instance: operationrequest-viewer-role
+ app.kubernetes.io/component: rbac
+ app.kubernetes.io/created-by: user
+ app.kubernetes.io/part-of: user
+ app.kubernetes.io/managed-by: kustomize
+ name: operationrequest-viewer-role
+rules:
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - operationrequests
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - operationrequests/status
+ verbs:
+ - get
diff --git a/controllers/user/config/rbac/role.yaml b/controllers/user/config/rbac/role.yaml
new file mode 100644
index 000000000000..4ce169d6ed53
--- /dev/null
+++ b/controllers/user/config/rbac/role.yaml
@@ -0,0 +1,78 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: manager-role
+rules:
+- apiGroups:
+ - '*'
+ resources:
+ - '*'
+ verbs:
+ - '*'
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - deleterequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - deleterequests/finalizers
+ verbs:
+ - update
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - deleterequests/status
+ verbs:
+ - get
+ - patch
+ - update
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - operationrequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - operationrequests/finalizers
+ verbs:
+ - update
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - operationrequests/status
+ verbs:
+ - get
+ - patch
+ - update
diff --git a/controllers/user/config/rbac/role_binding.yaml b/controllers/user/config/rbac/role_binding.yaml
new file mode 100644
index 000000000000..749383ffe1ec
--- /dev/null
+++ b/controllers/user/config/rbac/role_binding.yaml
@@ -0,0 +1,26 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: manager-rolebinding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: manager-role
+subjects:
+- kind: ServiceAccount
+ name: controller-manager
+ namespace: system
diff --git a/controllers/user/config/rbac/service_account.yaml b/controllers/user/config/rbac/service_account.yaml
new file mode 100644
index 000000000000..b8ca78e010b4
--- /dev/null
+++ b/controllers/user/config/rbac/service_account.yaml
@@ -0,0 +1,19 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: controller-manager
+ namespace: system
diff --git a/controllers/user/config/rbac/user_editor_role.yaml b/controllers/user/config/rbac/user_editor_role.yaml
new file mode 100644
index 000000000000..987c1ae7b559
--- /dev/null
+++ b/controllers/user/config/rbac/user_editor_role.yaml
@@ -0,0 +1,38 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions for end users to edit users.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: user-editor-role
+rules:
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - users
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - users/status
+ verbs:
+ - get
diff --git a/controllers/user/config/rbac/user_viewer_role.yaml b/controllers/user/config/rbac/user_viewer_role.yaml
new file mode 100644
index 000000000000..35b2a879e5ab
--- /dev/null
+++ b/controllers/user/config/rbac/user_viewer_role.yaml
@@ -0,0 +1,34 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions for end users to view users.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: user-viewer-role
+rules:
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - users
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - users/status
+ verbs:
+ - get
diff --git a/controllers/user/config/rbac/usergroup_editor_role.yaml b/controllers/user/config/rbac/usergroup_editor_role.yaml
new file mode 100644
index 000000000000..3d2696717c0f
--- /dev/null
+++ b/controllers/user/config/rbac/usergroup_editor_role.yaml
@@ -0,0 +1,38 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions for end users to edit usergroups.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: usergroup-editor-role
+rules:
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - usergroups
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - usergroups/status
+ verbs:
+ - get
diff --git a/controllers/user/config/rbac/usergroup_viewer_role.yaml b/controllers/user/config/rbac/usergroup_viewer_role.yaml
new file mode 100644
index 000000000000..79eed45d8eec
--- /dev/null
+++ b/controllers/user/config/rbac/usergroup_viewer_role.yaml
@@ -0,0 +1,34 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions for end users to view usergroups.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: usergroup-viewer-role
+rules:
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - usergroups
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - usergroups/status
+ verbs:
+ - get
diff --git a/controllers/user/config/rbac/usergroupbinding_editor_role.yaml b/controllers/user/config/rbac/usergroupbinding_editor_role.yaml
new file mode 100644
index 000000000000..685ca3cd191d
--- /dev/null
+++ b/controllers/user/config/rbac/usergroupbinding_editor_role.yaml
@@ -0,0 +1,38 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions for end users to edit usergroupbindings.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: usergroupbinding-editor-role
+rules:
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - usergroupbindings
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - usergroupbindings/status
+ verbs:
+ - get
diff --git a/controllers/user/config/rbac/usergroupbinding_viewer_role.yaml b/controllers/user/config/rbac/usergroupbinding_viewer_role.yaml
new file mode 100644
index 000000000000..91d90a1a681f
--- /dev/null
+++ b/controllers/user/config/rbac/usergroupbinding_viewer_role.yaml
@@ -0,0 +1,34 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions for end users to view usergroupbindings.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: usergroupbinding-viewer-role
+rules:
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - usergroupbindings
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - usergroupbindings/status
+ verbs:
+ - get
diff --git a/controllers/user/config/samples/all.yaml b/controllers/user/config/samples/all.yaml
new file mode 100644
index 000000000000..ebb84b35eadb
--- /dev/null
+++ b/controllers/user/config/samples/all.yaml
@@ -0,0 +1,104 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: user.sealos.io/v1
+kind: User
+metadata:
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+spec:
+ displayName: cuisongliu
+---
+apiVersion: user.sealos.io/v1
+kind: UserGroup
+metadata:
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+---
+apiVersion: user.sealos.io/v1
+kind: UserGroupBinding
+metadata:
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+subject:
+ kind: User
+ name: "f8699ded-58d3-432b-a9ff-56568b57a38d" # Name is case sensitive
+ apiGroup: sealos.io/v1
+userGroupRef: f8699ded-58d3-432b-a9ff-56568b57a38d
+roleRefs:
+ kind: ClusterRole
+ name: sealos-user-admin-role
+ apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: user.sealos.io/v1
+kind: UserGroupBinding
+metadata:
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d
+subject:
+ kind: Namespace
+ name: "f8699ded-58d3-432b-a9ff-56568b57a38d"
+userGroupRef: f8699ded-58d3-432b-a9ff-56568b57a38d
+
+## 创建者 sealos-user-create-role权限
+## 管理员 不能创建管理员使用webhook校验操作 新增sealos-user-manager-role权限
+## 只有管理员和创建者才需要设置role其他不需要设置(默认给namespace设置cluster-admin的Role权限)
+## 创建User默认创建UserGroup 和 2个UserGroupBinding (对user(直接是创建人的role)和对namespace)
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: sealos-user-create-role
+rules:
+ - apiGroups:
+ - user.sealos.io
+ resources:
+ - 'usergroupbindings'
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - patch
+ - update
+ - apiGroups:
+ - user.sealos.io
+ resources:
+ - 'listusergroupsbyuser'
+ - 'listuserbyusergroup'
+ verbs:
+ - list
+ - get
+ - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: sealos-user-manager-role
+rules:
+ - apiGroups:
+ - user.sealos.io
+ resources:
+ - '*'
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - patch
+ - update
+ - apiGroups:
+ - user.sealos.io
+ resources:
+ - 'listusergroupsbyuser'
+ - 'listuserbyusergroup'
+ - 'listnamespacebyusergroup'
+ verbs:
+ - list
+ - get
+ - watch
diff --git a/controllers/user/config/samples/app.log b/controllers/user/config/samples/app.log
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/controllers/user/config/samples/user_v1_deleterequest.yaml b/controllers/user/config/samples/user_v1_deleterequest.yaml
new file mode 100644
index 000000000000..b538cb994932
--- /dev/null
+++ b/controllers/user/config/samples/user_v1_deleterequest.yaml
@@ -0,0 +1,20 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: user.sealos.io/v1
+kind: DeleteRequest
+metadata:
+ name: deleterequest-sample
+spec:
+ user: xxxxxxxx
diff --git a/controllers/user/config/samples/user_v1_operationrequest.yaml b/controllers/user/config/samples/user_v1_operationrequest.yaml
new file mode 100644
index 000000000000..048620d3ed79
--- /dev/null
+++ b/controllers/user/config/samples/user_v1_operationrequest.yaml
@@ -0,0 +1,33 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+apiVersion: user.sealos.io/v1
+kind: Operationrequest
+metadata:
+ name: request-grant-a-owns-b
+ namespace: ns-bbbb0001
+spec:
+ user: aaaa0001
+ role: Owner
+ action: Grant
+---
+apiVersion: user.sealos.io/v1
+kind: Operationrequest
+metadata:
+ name: request-deprive-a-owns-b
+ namespace: ns-bbbb0001
+spec:
+ user: aaaa0001
+ role: Owner
+ action: Deprive
\ No newline at end of file
diff --git a/controllers/user/config/samples/user_v1_user.yaml b/controllers/user/config/samples/user_v1_user.yaml
new file mode 100644
index 000000000000..7129fdbc4ab0
--- /dev/null
+++ b/controllers/user/config/samples/user_v1_user.yaml
@@ -0,0 +1,32 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# user
+apiVersion: user.sealos.io/v1
+kind: User
+metadata:
+ annotations:
+ user.sealos.io/owner: aaaa0001
+ name: aaaa0001
+spec:
+ csrExpirationSeconds: 1000000000
+ kubeConfigRotateAt: "2026-01-19T14:55:00Z"
+---
+# group
+apiVersion: user.sealos.io/v1
+kind: User
+metadata:
+ annotations:
+ user.sealos.io/owner: aaaa0001
+ name: bbbb0001
diff --git a/controllers/user/config/webhook/kustomization.yaml b/controllers/user/config/webhook/kustomization.yaml
new file mode 100644
index 000000000000..1bf9427c2424
--- /dev/null
+++ b/controllers/user/config/webhook/kustomization.yaml
@@ -0,0 +1,20 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+resources:
+- manifests.yaml
+- service.yaml
+
+configurations:
+- kustomizeconfig.yaml
diff --git a/controllers/user/config/webhook/kustomizeconfig.yaml b/controllers/user/config/webhook/kustomizeconfig.yaml
new file mode 100644
index 000000000000..594f180c57f7
--- /dev/null
+++ b/controllers/user/config/webhook/kustomizeconfig.yaml
@@ -0,0 +1,39 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# the following config is for teaching kustomize where to look at when substituting vars.
+# It requires kustomize v2.1.0 or newer to work properly.
+nameReference:
+- kind: Service
+ version: v1
+ fieldSpecs:
+ - kind: MutatingWebhookConfiguration
+ group: admissionregistration.k8s.io
+ path: webhooks/clientConfig/service/name
+ - kind: ValidatingWebhookConfiguration
+ group: admissionregistration.k8s.io
+ path: webhooks/clientConfig/service/name
+
+namespace:
+- kind: MutatingWebhookConfiguration
+ group: admissionregistration.k8s.io
+ path: webhooks/clientConfig/service/namespace
+ create: true
+- kind: ValidatingWebhookConfiguration
+ group: admissionregistration.k8s.io
+ path: webhooks/clientConfig/service/namespace
+ create: true
+
+varReference:
+- path: metadata/annotations
diff --git a/controllers/user/config/webhook/manifests.yaml b/controllers/user/config/webhook/manifests.yaml
new file mode 100644
index 000000000000..59d6e4ac5b24
--- /dev/null
+++ b/controllers/user/config/webhook/manifests.yaml
@@ -0,0 +1,107 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: MutatingWebhookConfiguration
+metadata:
+ name: mutating-webhook-configuration
+webhooks:
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /mutate-user-sealos-io-v1-operationrequest
+ failurePolicy: Fail
+ name: moperationrequest.kb.io
+ rules:
+ - apiGroups:
+ - user.sealos.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - operationrequests
+ sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /mutate-user-sealos-io-v1-user
+ failurePolicy: Fail
+ name: muser.kb.io
+ rules:
+ - apiGroups:
+ - user.sealos.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - users
+ sideEffects: None
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+ name: validating-webhook-configuration
+webhooks:
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /validate-user-sealos-io-v1-operationrequest
+ failurePolicy: Fail
+ name: voperationrequest.kb.io
+ rules:
+ - apiGroups:
+ - user.sealos.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - operationrequests
+ sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /validate-user-sealos-io-v1-user
+ timeoutSeconds: 30
+ failurePolicy: Fail
+ name: vuser.kb.io
+ rules:
+ - apiGroups:
+ - user.sealos.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - users
+ sideEffects: None
diff --git a/controllers/user/config/webhook/service.yaml b/controllers/user/config/webhook/service.yaml
new file mode 100644
index 000000000000..03198124dd55
--- /dev/null
+++ b/controllers/user/config/webhook/service.yaml
@@ -0,0 +1,27 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+
+apiVersion: v1
+kind: Service
+metadata:
+ name: webhook-service
+ namespace: system
+spec:
+ ports:
+ - port: 443
+ protocol: TCP
+ targetPort: 9443
+ selector:
+ control-plane: controller-manager
diff --git a/controllers/user/controllers/adapt_rolebinding_controller.go b/controllers/user/controllers/adapt_rolebinding_controller.go
new file mode 100644
index 000000000000..8a1934ed130a
--- /dev/null
+++ b/controllers/user/controllers/adapt_rolebinding_controller.go
@@ -0,0 +1,133 @@
+/*
+Copyright 2022 labring.
+
+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 controllers
+
+import (
+ "context"
+
+ "github.com/go-logr/logr"
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ v1 "k8s.io/api/rbac/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+)
+
+// TODO This controller is used to adapt the old RoleBinding. only need to deploy the logic once for conversion and delete the controller in the future
+
+// AdaptRoleBindingReconciler reconciles a RoleBinding object, Old Role bindings are backward compatible and will be deleted in the future
+type AdaptRoleBindingReconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+ Logger logr.Logger
+}
+
+func (r *AdaptRoleBindingReconciler) Reconcile(
+ ctx context.Context,
+ req ctrl.Request,
+) (ctrl.Result, error) {
+ rolebinding := &v1.RoleBinding{}
+ if err := r.Get(ctx, req.NamespacedName, rolebinding); err != nil {
+ return ctrl.Result{}, client.IgnoreNotFound(err)
+ }
+
+ // old rolebinding only has one subject
+ if len(rolebinding.Subjects) != 1 {
+ return ctrl.Result{}, nil
+ }
+
+ if rolebinding.Subjects[0].Namespace != config.GetUserSystemNamespace() {
+ userName := rolebinding.GetAnnotations()[userAnnotationOwnerKey]
+ user := &userv1.User{}
+ if err := r.Get(ctx, client.ObjectKey{Name: userName}, user); err != nil {
+ r.Logger.Error(err, "get user failed")
+ return ctrl.Result{}, err
+ }
+ appendSubject := rolebinding.Subjects[0].DeepCopy()
+ appendSubject.Namespace = config.GetUserSystemNamespace()
+ rolebinding.Subjects = append(rolebinding.Subjects, *appendSubject)
+ if err := r.Update(ctx, rolebinding); err != nil {
+ r.Logger.Error(err, "update rolebinding failed")
+ return ctrl.Result{}, err
+ }
+ if err := controllerutil.SetControllerReference(user, rolebinding, r.Scheme); err != nil {
+ r.Logger.Error(err, "set controller reference failed")
+ return ctrl.Result{}, err
+ }
+ }
+ return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *AdaptRoleBindingReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ const controllerName = "adapt_rolebinding_controller"
+ if r.Client == nil {
+ r.Client = mgr.GetClient()
+ }
+ r.Logger = ctrl.Log.WithName(controllerName)
+ r.Scheme = mgr.GetScheme()
+ r.Logger.V(1).Info("init reconcile AdaptRoleBinding controller")
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&v1.RoleBinding{}, builder.WithPredicates(WorkspacePredicate{})).
+ Complete(r)
+}
+
+type WorkspacePredicate struct{}
+
+func (WorkspacePredicate) Create(e event.CreateEvent) bool {
+ return isWorkspaceObject(e.Object)
+}
+
+func (WorkspacePredicate) Delete(_ event.DeleteEvent) bool {
+ return false
+}
+
+func (WorkspacePredicate) Update(_ event.UpdateEvent) bool {
+ return false
+}
+
+func (WorkspacePredicate) Generic(_ event.GenericEvent) bool {
+ return false
+}
+
+func isWorkspaceObject(obj client.Object) bool {
+ rolebinding, ok := obj.(*v1.RoleBinding)
+ if !ok {
+ return false
+ }
+ anno := obj.GetAnnotations()
+ if anno == nil {
+ return false
+ }
+ if anno["user.sealos.io/owner"] == "" {
+ return false
+ }
+ if len(obj.GetOwnerReferences()) > 0 {
+ return false
+ }
+
+ for _, sub := range rolebinding.Subjects {
+ if sub.Namespace == config.GetUserSystemNamespace() {
+ return false
+ }
+ }
+ return true
+}
diff --git a/controllers/user/controllers/cache/cache.go b/controllers/user/controllers/cache/cache.go
new file mode 100644
index 000000000000..f50a6fd512f5
--- /dev/null
+++ b/controllers/user/controllers/cache/cache.go
@@ -0,0 +1,566 @@
+// Copyright 2026 labring.
+//
+// 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 cache
+
+import (
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/json"
+ "encoding/pem"
+ "strconv"
+ "strings"
+ "time"
+
+ licensev1 "github.com/labring/sealos/controllers/license/api/v1"
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ "github.com/labring/sealos/controllers/user/controllers/helper/hash"
+ corev1 "k8s.io/api/core/v1"
+ rbacv1 "k8s.io/api/rbac/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/fields"
+ "k8s.io/client-go/tools/clientcmd"
+ ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// RoleBindingSpecHashAnnotation stores the in-memory hash for a RoleBinding
+// or ClusterRoleBinding projection. It is never written back to the API.
+const RoleBindingSpecHashAnnotation = "user.sealos.io/cache-role-binding-spec-hash"
+
+type roleBindingSpec struct {
+ RoleRef rbacv1.RoleRef
+ Subjects []rbacv1.Subject
+}
+
+// RoleBindingSpecHash returns the stable hash used by RBAC cache projections.
+func RoleBindingSpecHash(roleRef rbacv1.RoleRef, subjects []rbacv1.Subject) string {
+ return hash.HashToString(roleBindingSpec{RoleRef: roleRef, Subjects: subjects})
+}
+
+// Options keeps only explicitly registered informer data in memory. Large
+// fields are removed when controllers only need a smaller object projection.
+func Options(syncPeriod *time.Duration) ctrlcache.Options {
+ secretMetadata := &metav1.PartialObjectMetadata{}
+ secretMetadata.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Secret"))
+ return ctrlcache.Options{
+ SyncPeriod: syncPeriod,
+ ReaderFailOnMissingInformer: true,
+ DefaultTransform: ctrlcache.TransformStripManagedFields(),
+ ByObject: map[client.Object]ctrlcache.ByObject{
+ &corev1.Namespace{}: {
+ Transform: transformNamespace,
+ },
+ &rbacv1.ClusterRoleBinding{}: {
+ Field: fields.OneTermEqualSelector(
+ "metadata.name",
+ config.AdminClusterRoleBindingName,
+ ),
+ // The admin binding is checked from cache during restart, including
+ // its subjects and role reference.
+ Transform: transformClusterRoleBinding,
+ },
+ &licensev1.License{}: {
+ Transform: transformLicense,
+ },
+ &userv1.User{}: {
+ Transform: transformUser,
+ },
+ &userv1.DeleteRequest{}: {
+ Transform: transformDeleteRequest,
+ },
+ secretMetadata: {
+ Namespaces: map[string]ctrlcache.Config{
+ config.GetUserSystemNamespace(): {},
+ },
+ Transform: transformSecretMetadata,
+ },
+ &corev1.ServiceAccount{}: {
+ Namespaces: map[string]ctrlcache.Config{
+ config.GetUserSystemNamespace(): {},
+ },
+ Transform: transformServiceAccount,
+ },
+ &userv1.Operationrequest{}: {
+ Namespaces: map[string]ctrlcache.Config{
+ config.GetUserSystemNamespace(): {},
+ },
+ Transform: transformOperationrequest,
+ },
+ &rbacv1.Role{}: {
+ Transform: transformRole,
+ },
+ &rbacv1.RoleBinding{}: {
+ Transform: transformRoleBinding,
+ },
+ },
+ }
+}
+
+// UncachedObjects returns objects whose reads require complete, current API data.
+func UncachedObjects() []client.Object {
+ return []client.Object{
+ &licensev1.License{},
+ &userv1.DeleteRequest{},
+ &userv1.Operationrequest{},
+ &corev1.Namespace{},
+ &corev1.Secret{},
+ &corev1.ServiceAccount{},
+ &rbacv1.ClusterRoleBinding{},
+ &rbacv1.Role{},
+ &rbacv1.RoleBinding{},
+ }
+}
+
+func transformUser(obj any) (any, error) {
+ user, ok := obj.(*userv1.User)
+ if !ok {
+ return obj, nil
+ }
+
+ metadata := projectUserObjectMeta(user.ObjectMeta)
+ metadata.Finalizers = append([]string(nil), user.Finalizers...)
+ metadata.Annotations = copyMapValues(
+ user.Annotations,
+ userv1.UserAnnotationOwnerKey,
+ )
+ metadata.Labels = copyMapValues(
+ user.Labels,
+ "user.sealos.io/status",
+ "user.sealos.io/type",
+ )
+ status := projectUserStatus(user.Status)
+ if status.KubeConfigRefreshAt == nil {
+ status.KubeConfigRefreshAt = inferKubeConfigRefreshAt(user)
+ }
+ spec := user.Spec
+ if user.Spec.KubeConfigRotateAt != nil {
+ rotateAt := *user.Spec.KubeConfigRotateAt
+ spec.KubeConfigRotateAt = &rotateAt
+ }
+ return &userv1.User{
+ TypeMeta: user.TypeMeta,
+ ObjectMeta: metadata,
+ Spec: spec,
+ Status: status,
+ }, nil
+}
+
+// projectUserStatus copies the status fields used by reconciliation and drops
+// the persisted kubeconfig before allocating a cache object.
+func projectUserStatus(status userv1.UserStatus) userv1.UserStatus {
+ projected := userv1.UserStatus{
+ Phase: status.Phase,
+ ObservedCSRExpirationSeconds: status.ObservedCSRExpirationSeconds,
+ ObservedKubeConfigSecretUID: status.ObservedKubeConfigSecretUID,
+ ObservedGeneration: status.ObservedGeneration,
+ }
+ if status.ObservedKubeConfigRotateAt != nil {
+ rotateAt := *status.ObservedKubeConfigRotateAt
+ projected.ObservedKubeConfigRotateAt = &rotateAt
+ }
+ if status.KubeConfigRefreshAt != nil {
+ refreshAt := *status.KubeConfigRefreshAt
+ projected.KubeConfigRefreshAt = &refreshAt
+ }
+ if len(status.Conditions) > 0 {
+ projected.Conditions = append([]userv1.Condition(nil), status.Conditions...)
+ }
+ return projected
+}
+
+// inferKubeConfigRefreshAt derives the refresh point from legacy JWT-backed
+// kubeconfigs while projecting the object. The payload is parsed and dropped;
+// the cache never retains the credential itself.
+func inferKubeConfigRefreshAt(user *userv1.User) *metav1.Time {
+ if user == nil || user.Status.KubeConfig == "" {
+ return nil
+ }
+ config, err := clientcmd.Load([]byte(user.Status.KubeConfig))
+ if err != nil {
+ return nil
+ }
+ info, ok := config.AuthInfos[user.Name]
+ if !ok || info == nil {
+ return inferLegacyRefreshAt(user)
+ }
+ if info.Token != "" {
+ parts := strings.Split(info.Token, ".")
+ if len(parts) >= 2 {
+ if refreshAt := inferTokenRefreshAt(parts[1]); refreshAt != nil {
+ return refreshAt
+ }
+ }
+ }
+ if len(info.ClientCertificateData) == 0 {
+ return inferLegacyRefreshAt(user)
+ }
+ block, _ := pem.Decode(info.ClientCertificateData)
+ if block == nil {
+ return nil
+ }
+ certificate, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return nil
+ }
+ return refreshAtFromIssuedAt(certificate.NotBefore, certificate.NotAfter)
+}
+
+func inferLegacyRefreshAt(user *userv1.User) *metav1.Time {
+ if user == nil {
+ return nil
+ }
+ issuedAt := user.Status.ObservedKubeConfigRotateAt
+ if issuedAt == nil || issuedAt.IsZero() {
+ issuedAt = &user.CreationTimestamp
+ }
+ expirationSeconds := user.Status.ObservedCSRExpirationSeconds
+ if expirationSeconds == 0 {
+ expirationSeconds = user.Spec.CSRExpirationSeconds
+ }
+ if expirationSeconds == 0 {
+ // Older User objects may predate both expiration fields. The legacy
+ // kubeconfig issuer used this same default when the spec was zero.
+ expirationSeconds = userv1.DefaultCSRExpirationSeconds
+ }
+ if issuedAt == nil || issuedAt.IsZero() {
+ return nil
+ }
+ expirationSeconds = userv1.NormalizeCSRExpirationSeconds(expirationSeconds)
+ return refreshAtFromIssuedAt(
+ issuedAt.Time,
+ issuedAt.Add(time.Duration(expirationSeconds)*time.Second),
+ )
+}
+
+func inferTokenRefreshAt(encodedPayload string) *metav1.Time {
+ payload, err := base64.RawURLEncoding.DecodeString(encodedPayload)
+ if err != nil {
+ payload, err = base64.URLEncoding.DecodeString(encodedPayload)
+ if err != nil {
+ return nil
+ }
+ }
+ claims := map[string]json.RawMessage{}
+ if err := json.Unmarshal(payload, &claims); err != nil {
+ return nil
+ }
+ var expiration json.Number
+ if err := json.Unmarshal(claims["exp"], &expiration); err != nil {
+ return nil
+ }
+ expirationUnix, err := strconv.ParseInt(string(expiration), 10, 64)
+ if err != nil {
+ return nil
+ }
+ expirationTime := time.Unix(expirationUnix, 0)
+ var issuedAt time.Time
+ // Kubernetes service-account tokens normally carry iat and nbf. Use nbf
+ // as a fallback so the inferred deadline remains stable across cache events.
+ for _, claimName := range []string{"iat", "nbf"} {
+ issued, ok := claims[claimName]
+ if !ok {
+ continue
+ }
+ var issuedNumber json.Number
+ if err := json.Unmarshal(issued, &issuedNumber); err != nil {
+ continue
+ }
+ issuedAtUnix, err := strconv.ParseInt(string(issuedNumber), 10, 64)
+ if err == nil {
+ issuedAt = time.Unix(issuedAtUnix, 0)
+ break
+ }
+ }
+ if !issuedAt.IsZero() && expirationTime.After(issuedAt) {
+ return refreshAtFromIssuedAt(issuedAt, expirationTime)
+ }
+ return refreshAtFromExpiration(expirationTime)
+}
+
+func refreshAtFromExpiration(expiration time.Time) *metav1.Time {
+ refreshAt := time.Now().Add(time.Until(expiration) * 8 / 10)
+ return &metav1.Time{Time: refreshAt}
+}
+
+func refreshAtFromIssuedAt(issuedAt, expiration time.Time) *metav1.Time {
+ return &metav1.Time{Time: issuedAt.Add(expiration.Sub(issuedAt) * 8 / 10)}
+}
+
+func transformLicense(obj any) (any, error) {
+ metadata, ok := obj.(*metav1.PartialObjectMetadata)
+ if !ok {
+ return obj, nil
+ }
+ projected := projectEventObjectMeta(metadata.ObjectMeta)
+ projected.CreationTimestamp = metadata.CreationTimestamp
+ return &metav1.PartialObjectMetadata{
+ TypeMeta: metadata.TypeMeta,
+ ObjectMeta: projected,
+ }, nil
+}
+
+func transformDeleteRequest(obj any) (any, error) {
+ metadata, ok := obj.(*metav1.PartialObjectMetadata)
+ if !ok {
+ return obj, nil
+ }
+ return &metav1.PartialObjectMetadata{
+ TypeMeta: metadata.TypeMeta,
+ ObjectMeta: projectEventObjectMeta(metadata.ObjectMeta),
+ }, nil
+}
+
+func transformOperationrequest(obj any) (any, error) {
+ metadata, ok := obj.(*metav1.PartialObjectMetadata)
+ if !ok {
+ return obj, nil
+ }
+ return &metav1.PartialObjectMetadata{
+ TypeMeta: metadata.TypeMeta,
+ ObjectMeta: projectEventObjectMeta(metadata.ObjectMeta),
+ }, nil
+}
+
+func transformServiceAccount(obj any) (any, error) {
+ serviceAccount, ok := obj.(*corev1.ServiceAccount)
+ if !ok {
+ return obj, nil
+ }
+ projected := &corev1.ServiceAccount{
+ TypeMeta: serviceAccount.TypeMeta,
+ ObjectMeta: projectOwnerObjectMeta(serviceAccount.ObjectMeta),
+ }
+ if hasUserController(projected.OwnerReferences) && len(serviceAccount.Secrets) > 0 {
+ projected.Secrets = []corev1.ObjectReference{{Name: serviceAccount.Secrets[0].Name}}
+ }
+ return projected, nil
+}
+
+func transformRole(obj any) (any, error) {
+ role, ok := obj.(*rbacv1.Role)
+ if !ok {
+ return obj, nil
+ }
+ projected := &rbacv1.Role{
+ TypeMeta: role.TypeMeta,
+ ObjectMeta: projectOwnerObjectMeta(role.ObjectMeta),
+ }
+ if hasUserController(projected.OwnerReferences) {
+ if projected.Annotations == nil {
+ projected.Annotations = make(map[string]string, 1)
+ }
+ projected.Annotations[config.RoleRulesHashAnnotation] = hash.HashToString(role.Rules)
+ }
+ return projected, nil
+}
+
+func transformRoleBinding(obj any) (any, error) {
+ roleBinding, ok := obj.(*rbacv1.RoleBinding)
+ if !ok {
+ return obj, nil
+ }
+ projected := &rbacv1.RoleBinding{
+ TypeMeta: roleBinding.TypeMeta,
+ ObjectMeta: projectOwnerObjectMeta(roleBinding.ObjectMeta),
+ }
+ if hasUserController(projected.OwnerReferences) {
+ if projected.Annotations == nil {
+ projected.Annotations = make(map[string]string, 1)
+ }
+ projected.Annotations[RoleBindingSpecHashAnnotation] = RoleBindingSpecHash(
+ roleBinding.RoleRef,
+ roleBinding.Subjects,
+ )
+ } else {
+ // The legacy RoleBinding adapter uses the owner annotation to identify
+ // workspace bindings before it performs its uncached read.
+ projected.Annotations = copyMapValues(
+ roleBinding.Annotations,
+ userv1.UserAnnotationCreatorKey,
+ userv1.UserAnnotationOwnerKey,
+ )
+ }
+ return projected, nil
+}
+
+func transformClusterRoleBinding(obj any) (any, error) {
+ roleBinding, ok := obj.(*rbacv1.ClusterRoleBinding)
+ if !ok {
+ return obj, nil
+ }
+ projected := &rbacv1.ClusterRoleBinding{
+ TypeMeta: roleBinding.TypeMeta,
+ ObjectMeta: projectOwnerObjectMeta(roleBinding.ObjectMeta),
+ }
+ if hasUserController(projected.OwnerReferences) {
+ if projected.Annotations == nil {
+ projected.Annotations = make(map[string]string, 1)
+ }
+ projected.Annotations[RoleBindingSpecHashAnnotation] = RoleBindingSpecHash(
+ roleBinding.RoleRef,
+ roleBinding.Subjects,
+ )
+ }
+ return projected, nil
+}
+
+func hasUserController(references []metav1.OwnerReference) bool {
+ for _, reference := range references {
+ if reference.Controller != nil && *reference.Controller &&
+ reference.APIVersion == userv1.GroupVersion.String() && reference.Kind == "User" {
+ return true
+ }
+ }
+ return false
+}
+
+func transformNamespace(obj any) (any, error) {
+ metadata, ok := obj.(*metav1.PartialObjectMetadata)
+ if !ok {
+ return obj, nil
+ }
+
+ projected := projectEventObjectMeta(metadata.ObjectMeta)
+ if !isUserNamespaceName(metadata.Name) {
+ return &metav1.PartialObjectMetadata{
+ TypeMeta: metadata.TypeMeta,
+ ObjectMeta: projected,
+ }, nil
+ }
+ projected.Annotations = copyMapValues(
+ metadata.Annotations,
+ userv1.UserAnnotationCreatorKey,
+ userv1.UserAnnotationOwnerKey,
+ )
+ projected.Labels = copyMapValues(
+ metadata.Labels,
+ userv1.UserLabelOwnerKey,
+ )
+ for key, value := range metadata.Labels {
+ if config.IsPodSecurityLabel(key) {
+ if projected.Labels == nil {
+ projected.Labels = make(map[string]string)
+ }
+ projected.Labels[key] = value
+ }
+ }
+ projected.OwnerReferences = projectControllerOwnerReferences(metadata.OwnerReferences)
+ return &metav1.PartialObjectMetadata{
+ TypeMeta: metadata.TypeMeta,
+ ObjectMeta: projected,
+ }, nil
+}
+
+func projectOwnerObjectMeta(in metav1.ObjectMeta) metav1.ObjectMeta {
+ projected := projectEventObjectMeta(in)
+ projected.CreationTimestamp = in.CreationTimestamp
+ projected.OwnerReferences = projectControllerOwnerReferences(in.OwnerReferences)
+ if len(projected.OwnerReferences) == 0 {
+ return projected
+ }
+ projected.Annotations = copyMapValues(
+ in.Annotations,
+ userv1.UserAnnotationCreatorKey,
+ userv1.UserAnnotationOwnerKey,
+ )
+ return projected
+}
+
+func projectUserObjectMeta(in metav1.ObjectMeta) metav1.ObjectMeta {
+ out := metav1.ObjectMeta{
+ Name: in.Name,
+ Namespace: in.Namespace,
+ UID: in.UID,
+ ResourceVersion: in.ResourceVersion,
+ Generation: in.Generation,
+ CreationTimestamp: in.CreationTimestamp,
+ }
+ if in.DeletionTimestamp != nil {
+ out.DeletionTimestamp = in.DeletionTimestamp.DeepCopy()
+ }
+ if in.DeletionGracePeriodSeconds != nil {
+ gracePeriod := *in.DeletionGracePeriodSeconds
+ out.DeletionGracePeriodSeconds = &gracePeriod
+ }
+ return out
+}
+
+func projectEventObjectMeta(in metav1.ObjectMeta) metav1.ObjectMeta {
+ return metav1.ObjectMeta{
+ Name: in.Name,
+ Namespace: in.Namespace,
+ UID: in.UID,
+ ResourceVersion: in.ResourceVersion,
+ CreationTimestamp: in.CreationTimestamp,
+ }
+}
+
+func projectControllerOwnerReferences(in []metav1.OwnerReference) []metav1.OwnerReference {
+ for _, reference := range in {
+ if reference.Controller == nil || !*reference.Controller ||
+ reference.APIVersion != userv1.GroupVersion.String() || reference.Kind != "User" {
+ continue
+ }
+ controller := *reference.Controller
+ reference.Controller = &controller
+ if reference.BlockOwnerDeletion != nil {
+ blockOwnerDeletion := *reference.BlockOwnerDeletion
+ reference.BlockOwnerDeletion = &blockOwnerDeletion
+ }
+ return []metav1.OwnerReference{reference}
+ }
+ return nil
+}
+
+func isUserNamespaceName(name string) bool {
+ return strings.HasPrefix(name, "ns-") && len(name) > len("ns-")
+}
+
+func copyMapValues(source map[string]string, keys ...string) map[string]string {
+ count := 0
+ for _, key := range keys {
+ if _, ok := source[key]; ok {
+ count++
+ }
+ }
+ if count == 0 {
+ return nil
+ }
+ result := make(map[string]string, count)
+ for _, key := range keys {
+ if value, ok := source[key]; ok {
+ result[key] = value
+ }
+ }
+ return result
+}
+
+func transformSecretMetadata(obj any) (any, error) {
+ metadata, ok := obj.(*metav1.PartialObjectMetadata)
+ if !ok {
+ return obj, nil
+ }
+ projected := &metav1.PartialObjectMetadata{
+ TypeMeta: metadata.TypeMeta,
+ ObjectMeta: projectEventObjectMeta(metadata.ObjectMeta),
+ }
+ projected.CreationTimestamp = metadata.CreationTimestamp
+ projected.OwnerReferences = projectControllerOwnerReferences(metadata.OwnerReferences)
+ // Legacy token cleanup indexes all Secrets by this annotation, including
+ // Secrets that predate User owner references.
+ projected.Annotations = copyMapValues(metadata.Annotations, corev1.ServiceAccountNameKey)
+ return projected, nil
+}
diff --git a/controllers/user/controllers/cache/cache_test.go b/controllers/user/controllers/cache/cache_test.go
new file mode 100644
index 000000000000..7a0dc757e933
--- /dev/null
+++ b/controllers/user/controllers/cache/cache_test.go
@@ -0,0 +1,728 @@
+// Copyright 2026 labring.
+//
+// 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 cache
+
+import (
+ "encoding/base64"
+ "encoding/pem"
+ "reflect"
+ "strconv"
+ "testing"
+ "time"
+
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ "github.com/labring/sealos/controllers/user/controllers/helper/hash"
+ corev1 "k8s.io/api/core/v1"
+ rbacv1 "k8s.io/api/rbac/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/clientcmd"
+ clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
+ ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+func transformedAs[T any](t *testing.T, obj any) T {
+ t.Helper()
+ transformed, ok := obj.(T)
+ if !ok {
+ t.Fatalf("transformed object type = %T", obj)
+ }
+ return transformed
+}
+
+func TestOptionsLimitsSecretCache(t *testing.T) {
+ syncPeriod := time.Hour
+ options := Options(&syncPeriod)
+ if options.SyncPeriod != &syncPeriod {
+ t.Fatal("sync period was not retained")
+ }
+ if !options.ReaderFailOnMissingInformer {
+ t.Fatal("missing informer reads are allowed")
+ }
+ if options.DefaultTransform == nil {
+ t.Fatal("default managed fields transform is nil")
+ }
+
+ found := false
+ for obj, byObject := range options.ByObject {
+ if _, ok := obj.(*metav1.PartialObjectMetadata); !ok {
+ continue
+ }
+ found = true
+ if byObject.Transform == nil {
+ t.Fatal("secret transform is nil")
+ }
+ if len(byObject.Namespaces) != 1 {
+ t.Fatalf("secret cache namespaces = %d, want 1", len(byObject.Namespaces))
+ }
+ if _, ok := byObject.Namespaces[config.GetUserSystemNamespace()]; !ok {
+ t.Fatal("user system namespace is not cached for secrets")
+ }
+ }
+ if !found {
+ t.Fatal("secret cache options not found")
+ }
+}
+
+func TestOptionsLimitNamespacedMetadataCaches(t *testing.T) {
+ options := Options(nil)
+ for _, required := range []client.Object{
+ &corev1.ServiceAccount{},
+ &userv1.Operationrequest{},
+ } {
+ found := false
+ for obj, byObject := range options.ByObject {
+ if reflect.TypeOf(obj) != reflect.TypeOf(required) {
+ continue
+ }
+ found = true
+ if len(byObject.Namespaces) != 1 {
+ t.Fatalf("%T cache namespaces = %d, want 1", required, len(byObject.Namespaces))
+ }
+ if _, ok := byObject.Namespaces[config.GetUserSystemNamespace()]; !ok {
+ t.Fatalf("%T cache is not limited to the user system namespace", required)
+ }
+ }
+ if !found {
+ t.Fatalf("%T cache options not found", required)
+ }
+ }
+}
+
+func TestOptionsLimitsClusterRoleBindingCacheToAdminBinding(t *testing.T) {
+ options := Options(nil)
+ var byObject ctrlcache.ByObject
+ for obj, candidate := range options.ByObject {
+ if reflect.TypeOf(obj) == reflect.TypeFor[*rbacv1.ClusterRoleBinding]() {
+ byObject = candidate
+ break
+ }
+ }
+ if byObject.Field == nil {
+ t.Fatal("cluster role binding cache options not found")
+ }
+ if byObject.Field.String() != "metadata.name="+config.AdminClusterRoleBindingName {
+ t.Fatalf("cluster role binding field selector = %v", byObject.Field)
+ }
+}
+
+func TestTransformNamespaceKeepsSecurityAndOwnerMetadata(t *testing.T) {
+ namespace := &metav1.PartialObjectMetadata{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ns-user-a",
+ ResourceVersion: "42",
+ UID: "namespace-a",
+ CreationTimestamp: metav1.NewTime(time.Unix(100, 0)),
+ Labels: map[string]string{
+ config.PodSecurityLabelPrefix + "enforce": "baseline",
+ userv1.UserLabelOwnerKey: "owner-a",
+ "unused.example/label": "unused",
+ },
+ Annotations: map[string]string{
+ userv1.UserAnnotationCreatorKey: "user-a",
+ userv1.UserAnnotationOwnerKey: "owner-a",
+ "unused.example/annotation": "unused",
+ },
+ ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}},
+ },
+ }
+
+ transformed, err := transformNamespace(namespace)
+ if err != nil {
+ t.Fatalf("transform namespace: %v", err)
+ }
+ got, ok := transformed.(*metav1.PartialObjectMetadata)
+ if !ok {
+ t.Fatalf("transformed type = %T, want *metav1.PartialObjectMetadata", transformed)
+ }
+ wantLabels := map[string]string{
+ config.PodSecurityLabelPrefix + "enforce": "baseline",
+ userv1.UserLabelOwnerKey: "owner-a",
+ }
+ if !reflect.DeepEqual(got.Labels, wantLabels) {
+ t.Fatalf("labels = %#v, want %#v", got.Labels, wantLabels)
+ }
+ wantAnnotations := map[string]string{
+ userv1.UserAnnotationCreatorKey: "user-a",
+ userv1.UserAnnotationOwnerKey: "owner-a",
+ }
+ if !reflect.DeepEqual(got.Annotations, wantAnnotations) || len(got.ManagedFields) != 0 {
+ t.Fatalf("unused namespace metadata was retained: %#v", got.ObjectMeta)
+ }
+ if got.UID != namespace.UID ||
+ !got.CreationTimestamp.Equal(&namespace.CreationTimestamp) {
+ t.Fatalf(
+ "required namespace event metadata was not retained: uid=%q creation=%v",
+ got.UID,
+ got.CreationTimestamp,
+ )
+ }
+}
+
+func TestTransformSecretMetadataKeepsOnlyIndexMetadata(t *testing.T) {
+ controller := true
+ metadata := &metav1.PartialObjectMetadata{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "token-a",
+ Namespace: config.GetUserSystemNamespace(),
+ ResourceVersion: "42",
+ CreationTimestamp: metav1.NewTime(time.Unix(100, 0)),
+ Annotations: map[string]string{
+ corev1.ServiceAccountNameKey: "user-a",
+ "unused.example/key": "large-value",
+ },
+ OwnerReferences: []metav1.OwnerReference{{
+ APIVersion: userv1.GroupVersion.String(),
+ Kind: "User", Name: "user-a", UID: "user-a", Controller: &controller,
+ }},
+ ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}},
+ },
+ }
+ metadata.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Secret"))
+
+ transformed, err := transformSecretMetadata(metadata)
+ if err != nil {
+ t.Fatalf("transform secret: %v", err)
+ }
+ got, ok := transformed.(*metav1.PartialObjectMetadata)
+ if !ok {
+ t.Fatalf("transformed type = %T, want *metav1.PartialObjectMetadata", transformed)
+ }
+ if got.Name != metadata.Name ||
+ got.Namespace != metadata.Namespace ||
+ got.ResourceVersion != "42" ||
+ !got.CreationTimestamp.Equal(&metadata.CreationTimestamp) {
+ t.Fatalf("required metadata was not retained: %#v", got.ObjectMeta)
+ }
+ if got.Annotations[corev1.ServiceAccountNameKey] != "user-a" || len(got.Annotations) != 1 {
+ t.Fatalf("secret index annotations = %#v", got.Annotations)
+ }
+ if !reflect.DeepEqual(got.OwnerReferences, metadata.OwnerReferences) ||
+ len(got.Labels) != 0 || len(got.ManagedFields) != 0 {
+ t.Fatalf("secret metadata projection = %#v", got.ObjectMeta)
+ }
+}
+
+func TestTransformUserDropsOnlyLargeUnusedFields(t *testing.T) {
+ rotateAt := metav1.Now()
+ user := &userv1.User{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "user-a",
+ ResourceVersion: "42",
+ Annotations: map[string]string{
+ userv1.UserAnnotationOwnerKey: "owner-a",
+ "unused.example/annotation": "unused",
+ },
+ Labels: map[string]string{
+ "user.sealos.io/status": "active",
+ "user.sealos.io/type": "Group",
+ "unused.example/label": "unused",
+ },
+ Finalizers: []string{"sealos.io/user.finalizers"},
+ ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}},
+ },
+ Spec: userv1.UserSpec{
+ CSRExpirationSeconds: 600,
+ KubeConfigRotateAt: &rotateAt,
+ },
+ Status: userv1.UserStatus{
+ Phase: userv1.UserActive,
+ KubeConfig: "large-kubeconfig",
+ ObservedCSRExpirationSeconds: 600,
+ ObservedKubeConfigRotateAt: &rotateAt,
+ KubeConfigRefreshAt: &rotateAt,
+ ObservedKubeConfigSecretUID: "secret-a",
+ ObservedGeneration: 7,
+ Conditions: []userv1.Condition{{
+ Type: userv1.Ready,
+ Status: corev1.ConditionTrue,
+ }},
+ },
+ }
+
+ transformed, err := transformUser(user)
+ if err != nil {
+ t.Fatalf("transform user: %v", err)
+ }
+ got, ok := transformed.(*userv1.User)
+ if !ok {
+ t.Fatalf("transformed type = %T, want *v1.User", transformed)
+ }
+ if got.Status.KubeConfig != "" {
+ t.Fatal("kubeconfig was retained")
+ }
+ if len(got.ManagedFields) != 0 {
+ t.Fatal("managed fields were retained")
+ }
+ wantAnnotations := map[string]string{userv1.UserAnnotationOwnerKey: "owner-a"}
+ wantLabels := map[string]string{
+ "user.sealos.io/status": "active",
+ "user.sealos.io/type": "Group",
+ }
+ if got.Name != user.Name || got.ResourceVersion != user.ResourceVersion ||
+ !reflect.DeepEqual(got.Annotations, wantAnnotations) ||
+ !reflect.DeepEqual(got.Labels, wantLabels) ||
+ !reflect.DeepEqual(got.Finalizers, user.Finalizers) ||
+ !reflect.DeepEqual(got.Spec, user.Spec) {
+ t.Fatalf("required user fields were not retained: %#v", got)
+ }
+ wantStatus := user.Status.DeepCopy()
+ wantStatus.KubeConfig = ""
+ if !reflect.DeepEqual(&got.Status, wantStatus) {
+ t.Fatalf("status = %#v, want %#v", got.Status, *wantStatus)
+ }
+ if user.Status.KubeConfig == "" || len(user.ManagedFields) == 0 {
+ t.Fatal("transform mutated the source user")
+ }
+ got.Spec.KubeConfigRotateAt.Time = got.Spec.KubeConfigRotateAt.Add(time.Hour)
+ got.Status.Conditions[0].Message = "changed"
+ if user.Spec.KubeConfigRotateAt.Equal(got.Spec.KubeConfigRotateAt) {
+ t.Fatal("transform shared the kubeconfig rotate timestamp")
+ }
+ if user.Status.Conditions[0].Message == "changed" {
+ t.Fatal("transform shared the conditions slice")
+ }
+}
+
+func TestTransformMetadataKeepsOnlyEventFields(t *testing.T) {
+ metadata := &metav1.PartialObjectMetadata{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "role-a",
+ Namespace: "ns-a",
+ ResourceVersion: "42",
+ CreationTimestamp: metav1.NewTime(time.Unix(100, 0)),
+ Annotations: map[string]string{"unused.example/key": "unused"},
+ Labels: map[string]string{"unused.example/key": "unused"},
+ Finalizers: []string{"unused.example/finalizer"},
+ ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}},
+ OwnerReferences: []metav1.OwnerReference{{Name: "user-a"}},
+ },
+ }
+ metadata.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ServiceAccount"))
+
+ transformed, err := transformLicense(metadata)
+ if err != nil {
+ t.Fatalf("transform license metadata: %v", err)
+ }
+ got, ok := transformed.(*metav1.PartialObjectMetadata)
+ if !ok {
+ t.Fatalf("transformed type = %T, want *metav1.PartialObjectMetadata", transformed)
+ }
+ if got.GroupVersionKind() != metadata.GroupVersionKind() || got.Name != metadata.Name ||
+ got.Namespace != metadata.Namespace || got.ResourceVersion != metadata.ResourceVersion ||
+ !got.CreationTimestamp.Equal(&metadata.CreationTimestamp) {
+ t.Fatalf("required event metadata was not retained: %#v", got)
+ }
+ if len(got.OwnerReferences) != 0 ||
+ len(got.Annotations) != 0 ||
+ len(got.Labels) != 0 ||
+ len(got.Finalizers) != 0 ||
+ len(got.ManagedFields) != 0 {
+ t.Fatalf("unused metadata was retained: %#v", got.ObjectMeta)
+ }
+
+ for _, transform := range []func(any) (any, error){
+ transformDeleteRequest,
+ transformOperationrequest,
+ } {
+ keyOnly, err := transform(metadata)
+ if err != nil {
+ t.Fatalf("transform request metadata: %v", err)
+ }
+ gotKey, ok := keyOnly.(*metav1.PartialObjectMetadata)
+ if !ok {
+ t.Fatalf("key metadata type = %T, want *metav1.PartialObjectMetadata", keyOnly)
+ }
+ if len(gotKey.OwnerReferences) != 0 || len(gotKey.Annotations) != 0 {
+ t.Fatalf("key-only ownership metadata was retained: %#v", gotKey.ObjectMeta)
+ }
+ if !gotKey.CreationTimestamp.Equal(&metadata.CreationTimestamp) {
+ t.Fatalf("key-only event creation timestamp = %v", gotKey.CreationTimestamp)
+ }
+ }
+}
+
+func TestTransformOwnerObjectsKeepsReconcileFields(t *testing.T) {
+ controller := true
+ owner := metav1.OwnerReference{
+ APIVersion: userv1.GroupVersion.String(),
+ Kind: "User",
+ Name: "alice",
+ UID: "user-a",
+ Controller: &controller,
+ }
+ annotations := map[string]string{
+ userv1.UserAnnotationCreatorKey: "alice",
+ userv1.UserAnnotationOwnerKey: "owner-a",
+ "unused.example/key": "unused",
+ }
+
+ role, err := transformRole(&rbacv1.Role{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "Owner",
+ Namespace: "ns-alice",
+ ResourceVersion: "42",
+ Generation: 7,
+ CreationTimestamp: metav1.NewTime(
+ time.Unix(100, 0),
+ ),
+ Finalizers: []string{"unused.example/finalizer"},
+ Annotations: annotations,
+ OwnerReferences: []metav1.OwnerReference{owner},
+ },
+ Rules: []rbacv1.PolicyRule{{
+ APIGroups: []string{"*"},
+ Resources: []string{"pods"},
+ Verbs: []string{"get"},
+ }},
+ })
+ if err != nil {
+ t.Fatalf("transform role: %v", err)
+ }
+ gotRole := transformedAs[*rbacv1.Role](t, role)
+ wantRoleHash := hash.HashToString([]rbacv1.PolicyRule{{
+ APIGroups: []string{"*"}, Resources: []string{"pods"}, Verbs: []string{"get"},
+ }})
+ if len(gotRole.Rules) != 0 ||
+ gotRole.Annotations[config.RoleRulesHashAnnotation] != wantRoleHash ||
+ !reflect.DeepEqual(gotRole.OwnerReferences, []metav1.OwnerReference{owner}) ||
+ len(gotRole.Annotations) != 3 {
+ t.Fatalf("role reconcile fields were not retained: %#v", gotRole)
+ }
+ if len(gotRole.Labels) != 0 || len(gotRole.ManagedFields) != 0 {
+ t.Fatalf("role unused metadata was retained: %#v", gotRole.ObjectMeta)
+ }
+ if gotRole.ResourceVersion != "42" || gotRole.Generation != 0 ||
+ !gotRole.CreationTimestamp.Equal(&metav1.Time{Time: time.Unix(100, 0)}) ||
+ len(gotRole.Finalizers) != 0 {
+ t.Fatalf("role non-reconcile metadata was retained: %#v", gotRole.ObjectMeta)
+ }
+
+ roleBinding, err := transformRoleBinding(&rbacv1.RoleBinding{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: annotations,
+ OwnerReferences: []metav1.OwnerReference{owner},
+ },
+ RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "Role", Name: "Owner"},
+ Subjects: []rbacv1.Subject{
+ {Kind: "ServiceAccount", Name: "alice", Namespace: config.GetUserSystemNamespace()},
+ },
+ })
+ if err != nil {
+ t.Fatalf("transform role binding: %v", err)
+ }
+ gotRoleBinding := transformedAs[*rbacv1.RoleBinding](t, roleBinding)
+ if gotRoleBinding.RoleRef.Name != "" || len(gotRoleBinding.Subjects) != 0 ||
+ gotRoleBinding.Annotations[RoleBindingSpecHashAnnotation] != RoleBindingSpecHash(
+ rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "Role", Name: "Owner"},
+ []rbacv1.Subject{{
+ Kind: "ServiceAccount",
+ Name: "alice",
+ Namespace: config.GetUserSystemNamespace(),
+ }},
+ ) || !reflect.DeepEqual(gotRoleBinding.OwnerReferences, []metav1.OwnerReference{owner}) {
+ t.Fatalf("role binding projection was not compact: %#v", gotRoleBinding)
+ }
+
+ clusterRoleBinding, err := transformClusterRoleBinding(&rbacv1.ClusterRoleBinding{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: annotations,
+ OwnerReferences: []metav1.OwnerReference{owner},
+ },
+ RoleRef: rbacv1.RoleRef{
+ APIGroup: rbacv1.GroupName,
+ Kind: "ClusterRole",
+ Name: "cluster-admin",
+ },
+ Subjects: []rbacv1.Subject{
+ {Kind: "ServiceAccount", Name: "alice", Namespace: config.GetUserSystemNamespace()},
+ },
+ })
+ if err != nil {
+ t.Fatalf("transform cluster role binding: %v", err)
+ }
+ gotClusterRoleBinding := transformedAs[*rbacv1.ClusterRoleBinding](t, clusterRoleBinding)
+ if gotClusterRoleBinding.RoleRef.Name != "" || len(gotClusterRoleBinding.Subjects) != 0 ||
+ gotClusterRoleBinding.Annotations[RoleBindingSpecHashAnnotation] != RoleBindingSpecHash(
+ rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "cluster-admin"},
+ []rbacv1.Subject{{
+ Kind: "ServiceAccount",
+ Name: "alice",
+ Namespace: config.GetUserSystemNamespace(),
+ }},
+ ) {
+ t.Fatalf("cluster role binding projection was not compact: %#v", gotClusterRoleBinding)
+ }
+
+ serviceAccount, err := transformServiceAccount(&corev1.ServiceAccount{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: annotations,
+ OwnerReferences: []metav1.OwnerReference{owner},
+ },
+ Secrets: []corev1.ObjectReference{{Name: "token-alice"}},
+ })
+ if err != nil {
+ t.Fatalf("transform service account: %v", err)
+ }
+ gotServiceAccount := transformedAs[*corev1.ServiceAccount](t, serviceAccount)
+ if !reflect.DeepEqual(
+ gotServiceAccount.Secrets,
+ []corev1.ObjectReference{{Name: "token-alice"}},
+ ) {
+ t.Fatalf("service account secrets were not retained: %#v", gotServiceAccount.Secrets)
+ }
+}
+
+func TestTransformOwnerObjectsDropsSpecForUnownedObjects(t *testing.T) {
+ role, err := transformRole(&rbacv1.Role{
+ ObjectMeta: metav1.ObjectMeta{Name: "unrelated"},
+ Rules: []rbacv1.PolicyRule{
+ {APIGroups: []string{"*"}, Resources: []string{"secrets"}, Verbs: []string{"*"}},
+ },
+ })
+ if err != nil {
+ t.Fatalf("transform unrelated role: %v", err)
+ }
+ if got := transformedAs[*rbacv1.Role](t, role); len(got.Rules) != 0 {
+ t.Fatalf("unowned role rules were retained: %#v", got.Rules)
+ }
+
+ roleBinding, err := transformRoleBinding(&rbacv1.RoleBinding{
+ ObjectMeta: metav1.ObjectMeta{Name: "unrelated"},
+ RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "Role", Name: "admin"},
+ Subjects: []rbacv1.Subject{{Kind: "Group", Name: "admins"}},
+ })
+ if err != nil {
+ t.Fatalf("transform unrelated role binding: %v", err)
+ }
+ gotRoleBinding := transformedAs[*rbacv1.RoleBinding](t, roleBinding)
+ if gotRoleBinding.RoleRef.Name != "" || len(gotRoleBinding.Subjects) != 0 {
+ t.Fatalf("unowned role binding spec was retained: %#v", gotRoleBinding)
+ }
+
+ legacyRoleBinding, err := transformRoleBinding(&rbacv1.RoleBinding{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ userv1.UserAnnotationOwnerKey: "alice",
+ "unused.example/key": "unused",
+ },
+ },
+ Subjects: []rbacv1.Subject{{Kind: "User", Name: "alice"}},
+ })
+ if err != nil {
+ t.Fatalf("transform legacy role binding: %v", err)
+ }
+ gotLegacyRoleBinding := transformedAs[*rbacv1.RoleBinding](t, legacyRoleBinding)
+ if gotLegacyRoleBinding.Annotations[userv1.UserAnnotationOwnerKey] != "alice" ||
+ len(gotLegacyRoleBinding.Annotations) != 1 ||
+ len(gotLegacyRoleBinding.Subjects) != 0 {
+ t.Fatalf("legacy role binding projection lost adapter metadata: %#v", gotLegacyRoleBinding)
+ }
+}
+
+func TestTransformUserInfersLegacyKubeConfigRefresh(t *testing.T) {
+ expiresAt := time.Now().Add(time.Hour).Unix()
+ payload := base64.RawURLEncoding.EncodeToString(
+ []byte(`{"exp":` + strconv.FormatInt(expiresAt, 10) + `}`),
+ )
+ data, err := clientcmd.Write(clientcmdapi.Config{AuthInfos: map[string]*clientcmdapi.AuthInfo{
+ "alice": {Token: "header." + payload + ".signature"},
+ }})
+ if err != nil {
+ t.Fatalf("write legacy kubeconfig: %v", err)
+ }
+ user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}, Status: userv1.UserStatus{
+ KubeConfig: string(data),
+ }}
+ transformed, err := transformUser(user)
+ if err != nil {
+ t.Fatalf("transform legacy user: %v", err)
+ }
+ refreshAt := transformedAs[*userv1.User](t, transformed).Status.KubeConfigRefreshAt
+ if refreshAt == nil || refreshAt.Time.Before(time.Now().Add(40*time.Minute)) ||
+ refreshAt.After(time.Now().Add(50*time.Minute)) {
+ t.Fatalf("legacy refresh time = %v, want about 48 minutes from now", refreshAt)
+ }
+}
+
+func TestTransformUserUsesTokenNotBeforeForStableRefresh(t *testing.T) {
+ issuedAt := time.Now().Add(-10 * time.Minute).Truncate(time.Second)
+ expiresAt := issuedAt.Add(time.Hour)
+ payload := base64.RawURLEncoding.EncodeToString(
+ []byte(`{"exp":` + strconv.FormatInt(expiresAt.Unix(), 10) +
+ `,"nbf":` + strconv.FormatInt(issuedAt.Unix(), 10) + `}`),
+ )
+ data, err := clientcmd.Write(clientcmdapi.Config{AuthInfos: map[string]*clientcmdapi.AuthInfo{
+ "alice": {Token: "header." + payload + ".signature"},
+ }})
+ if err != nil {
+ t.Fatalf("write legacy kubeconfig: %v", err)
+ }
+ user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}, Status: userv1.UserStatus{
+ KubeConfig: string(data),
+ }}
+ transformed, err := transformUser(user)
+ if err != nil {
+ t.Fatalf("transform legacy user: %v", err)
+ }
+ refreshAt := transformedAs[*userv1.User](t, transformed).Status.KubeConfigRefreshAt
+ want := metav1.NewTime(issuedAt.Add(48 * time.Minute))
+ if refreshAt == nil || !refreshAt.Equal(&want) {
+ t.Fatalf("legacy refresh time = %v, want %v", refreshAt, want)
+ }
+}
+
+func TestTransformUserInfersLegacyRefreshFromObservedRotation(t *testing.T) {
+ rotation := metav1.NewTime(time.Now().Add(-time.Hour))
+ data, err := clientcmd.Write(clientcmdapi.Config{AuthInfos: map[string]*clientcmdapi.AuthInfo{
+ "alice": {Token: "legacy-token"},
+ }})
+ if err != nil {
+ t.Fatalf("write legacy kubeconfig: %v", err)
+ }
+ user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}, Spec: userv1.UserSpec{
+ CSRExpirationSeconds: 10 * 60,
+ }, Status: userv1.UserStatus{
+ KubeConfig: string(data),
+ ObservedCSRExpirationSeconds: 10 * 60,
+ ObservedKubeConfigRotateAt: &rotation,
+ }}
+ transformed, err := transformUser(user)
+ if err != nil {
+ t.Fatalf("transform legacy user: %v", err)
+ }
+ refreshAt := transformedAs[*userv1.User](t, transformed).Status.KubeConfigRefreshAt
+ want := metav1.NewTime(
+ rotation.Add(time.Duration(userv1.DefaultCSRExpirationSeconds) * time.Second * 8 / 10),
+ )
+ if refreshAt == nil || !refreshAt.Equal(&want) {
+ t.Fatalf("legacy fallback refresh time = %v, want %v", refreshAt, want)
+ }
+}
+
+func TestTransformUserInfersLegacyRefreshWithDefaultExpiration(t *testing.T) {
+ rotation := metav1.NewTime(time.Now().Add(-time.Hour))
+ data, err := clientcmd.Write(clientcmdapi.Config{AuthInfos: map[string]*clientcmdapi.AuthInfo{
+ "alice": {Token: "legacy-token"},
+ }})
+ if err != nil {
+ t.Fatalf("write legacy kubeconfig: %v", err)
+ }
+ user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}, Status: userv1.UserStatus{
+ KubeConfig: string(data),
+ ObservedKubeConfigRotateAt: &rotation,
+ }}
+ transformed, err := transformUser(user)
+ if err != nil {
+ t.Fatalf("transform legacy user: %v", err)
+ }
+ refreshAt := transformedAs[*userv1.User](t, transformed).Status.KubeConfigRefreshAt
+ want := metav1.NewTime(
+ rotation.Add(time.Duration(userv1.DefaultCSRExpirationSeconds) * time.Second * 8 / 10),
+ )
+ if refreshAt == nil || !refreshAt.Equal(&want) {
+ t.Fatalf("legacy refresh time = %v, want %v", refreshAt, want)
+ }
+}
+
+func TestTransformUserInfersLegacyRefreshWhenAuthInfoIsUnrecognized(t *testing.T) {
+ rotation := metav1.NewTime(time.Now().Add(-time.Hour))
+ data, err := clientcmd.Write(clientcmdapi.Config{AuthInfos: map[string]*clientcmdapi.AuthInfo{
+ "webhook-user": {},
+ }})
+ if err != nil {
+ t.Fatalf("write legacy kubeconfig: %v", err)
+ }
+ user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}, Spec: userv1.UserSpec{
+ CSRExpirationSeconds: 10 * 60,
+ }, Status: userv1.UserStatus{
+ KubeConfig: string(data),
+ ObservedCSRExpirationSeconds: 10 * 60,
+ ObservedKubeConfigRotateAt: &rotation,
+ }}
+ transformed, err := transformUser(user)
+ if err != nil {
+ t.Fatalf("transform legacy user: %v", err)
+ }
+ refreshAt := transformedAs[*userv1.User](t, transformed).Status.KubeConfigRefreshAt
+ want := metav1.NewTime(
+ rotation.Add(time.Duration(userv1.DefaultCSRExpirationSeconds) * time.Second * 8 / 10),
+ )
+ if refreshAt == nil || !refreshAt.Equal(&want) {
+ t.Fatalf("legacy refresh time = %v, want %v", refreshAt, want)
+ }
+}
+
+func TestTransformUserDoesNotInferRefreshFromMalformedKubeConfig(t *testing.T) {
+ rotation := metav1.NewTime(time.Now().Add(-time.Hour))
+ for _, kubeConfig := range []string{
+ "not: [valid",
+ string(mustWriteKubeConfig(t, &clientcmdapi.AuthInfo{
+ ClientCertificateData: []byte("not a certificate"),
+ })),
+ string(mustWriteKubeConfig(t, &clientcmdapi.AuthInfo{
+ ClientCertificateData: pem.EncodeToMemory(&pem.Block{
+ Type: "CERTIFICATE", Bytes: []byte("not certificate DER"),
+ }),
+ })),
+ } {
+ user := &userv1.User{
+ ObjectMeta: metav1.ObjectMeta{Name: "alice"},
+ Status: userv1.UserStatus{
+ KubeConfig: kubeConfig,
+ ObservedKubeConfigRotateAt: &rotation,
+ },
+ }
+ transformed, err := transformUser(user)
+ if err != nil {
+ t.Fatalf("transform malformed user: %v", err)
+ }
+ transformedUser := transformedAs[*userv1.User](t, transformed)
+ if refreshAt := transformedUser.Status.KubeConfigRefreshAt; refreshAt != nil {
+ t.Fatalf("malformed kubeconfig refresh time = %v, want nil", refreshAt)
+ }
+ }
+}
+
+func mustWriteKubeConfig(t *testing.T, authInfo *clientcmdapi.AuthInfo) []byte {
+ t.Helper()
+ data, err := clientcmd.Write(clientcmdapi.Config{AuthInfos: map[string]*clientcmdapi.AuthInfo{
+ "alice": authInfo,
+ }})
+ if err != nil {
+ t.Fatalf("write kubeconfig: %v", err)
+ }
+ return data
+}
+
+func TestUncachedObjects(t *testing.T) {
+ types := make(map[reflect.Type]struct{})
+ for _, obj := range UncachedObjects() {
+ types[reflect.TypeOf(obj)] = struct{}{}
+ }
+ for _, required := range []client.Object{
+ &corev1.Namespace{},
+ &corev1.Secret{},
+ &corev1.ServiceAccount{},
+ } {
+ if _, ok := types[reflect.TypeOf(required)]; !ok {
+ t.Fatalf("%T reads are still cache-backed", required)
+ }
+ }
+ if _, ok := types[reflect.TypeFor[*userv1.User]()]; ok {
+ t.Fatal("user reads bypass the projected cache")
+ }
+}
diff --git a/controllers/user/controllers/deleterequest_controller.go b/controllers/user/controllers/deleterequest_controller.go
new file mode 100644
index 000000000000..5b908cd23e29
--- /dev/null
+++ b/controllers/user/controllers/deleterequest_controller.go
@@ -0,0 +1,285 @@
+/*
+Copyright 2022 labring.
+
+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 controllers
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/go-logr/logr"
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/tools/record"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// DeleteRequestReconciler reconciles a DeleteRequest object
+type DeleteRequestReconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+ Logger logr.Logger
+ Recorder record.EventRecorder
+
+ // expirationTime is the time duration of the request is expired
+ expirationTime time.Duration
+ // retentionTime is the time duration of the request is retained after it is isCompleted
+ retentionTime time.Duration
+}
+
+const DeleteRequestRequeueDuration time.Duration = 30 * time.Second
+
+//+kubebuilder:rbac:groups=user.sealos.io,resources=deleterequests,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=user.sealos.io,resources=deleterequests/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=user.sealos.io,resources=deleterequests/finalizers,verbs=update
+
+func (r *DeleteRequestReconciler) Reconcile(
+ ctx context.Context,
+ req ctrl.Request,
+) (ctrl.Result, error) {
+ deleteRequest := &userv1.DeleteRequest{}
+ if err := r.Get(ctx, req.NamespacedName, deleteRequest); err != nil {
+ return ctrl.Result{}, client.IgnoreNotFound(err)
+ }
+ return r.reconcile(ctx, deleteRequest)
+}
+
+func (r *DeleteRequestReconciler) reconcile(
+ ctx context.Context,
+ request *userv1.DeleteRequest,
+) (ctrl.Result, error) {
+ r.Logger.V(1).
+ Info("start reconcile deleterequest", "name", request.Name, "user", request.Spec.User)
+ // count the time cost of handling the request
+ startTime := time.Now()
+ defer func() {
+ r.Logger.V(1).Info("complete request handling", "handling cost time", time.Since(startTime))
+ }()
+
+ // delete OperationRequest first if its status is isCompleted and exist for retention time
+ if r.isRetained(request) {
+ r.Logger.Info("delete request", "name", request.Name)
+ if err := r.deleteRequest(ctx, request); err != nil {
+ return ctrl.Result{}, err
+ }
+ return ctrl.Result{}, nil
+ }
+ // return early if its status is isCompleted and didn't exist for retention time
+ if r.isCompleted(request) {
+ r.Logger.Info("request is completed and requeue", "name", request.Name)
+ return ctrl.Result{RequeueAfter: DeleteRequestRequeueDuration}, nil
+ }
+ // change OperationRequest status to failed if it is expired
+ if r.isExpired(request) {
+ r.Logger.Info("request is expired, update status to failed", "name", request.Name)
+ if err := r.updateRequestStatus(ctx, request, userv1.RequestFailed); err != nil {
+ return ctrl.Result{}, err
+ }
+ return ctrl.Result{}, nil
+ }
+
+ // handle the request
+
+ // get user
+ user := userv1.User{}
+ if err := r.Get(ctx, client.ObjectKey{Name: request.Spec.User}, &user); err != nil {
+ r.Logger.Error(err, "get user error", "name", request.Spec.User)
+ r.Recorder.Eventf(
+ request,
+ corev1.EventTypeWarning,
+ "GetUserError",
+ "get user %s error: %s",
+ request.Spec.User,
+ err.Error(),
+ )
+ return ctrl.Result{}, err
+ }
+
+ // delete user if it is not labeled deleted
+ if !isUserDeleted(user) && !isGroupUser(user) {
+ r.Logger.Info("user is not deleted or not a group user", "name", user.Name)
+ r.Recorder.Eventf(
+ request,
+ corev1.EventTypeWarning,
+ "UserNotDeleted",
+ "user %s is not deleted or not a group user",
+ user.Name,
+ )
+ return ctrl.Result{RequeueAfter: DeleteRequestRequeueDuration}, nil
+ }
+
+ // delete user
+ if err := r.Delete(ctx, &user); err != nil {
+ r.Logger.Error(err, "delete user error", "name", user.Name)
+ r.Recorder.Eventf(
+ request,
+ corev1.EventTypeWarning,
+ "DeleteUserError",
+ "delete user %s error: %s",
+ user.Name,
+ err.Error(),
+ )
+ return ctrl.Result{}, err
+ }
+
+ // get namespace
+ ns := corev1.Namespace{}
+ if err := r.Get(
+ ctx,
+ client.ObjectKey{Name: config.GetUsersNamespace(user.Name)},
+ &ns,
+ ); err != nil {
+ r.Logger.Error(err, "get ns error", "name", ns.Name)
+ r.Recorder.Eventf(
+ request,
+ corev1.EventTypeWarning,
+ "GetNamespaceError",
+ "get namespace %s error: %s",
+ ns.Name,
+ err.Error(),
+ )
+ return ctrl.Result{}, err
+ }
+
+ // delete namespace
+ if err := r.Delete(ctx, &ns); err != nil {
+ r.Logger.Error(err, "delete ns error", "name", ns.Name)
+ r.Recorder.Eventf(
+ request,
+ corev1.EventTypeWarning,
+ "DeleteNamespaceError",
+ "delete namespace %s error: %s",
+ ns.Name,
+ err.Error(),
+ )
+ return ctrl.Result{}, err
+ }
+
+ // update Request status to completed
+ if err := r.updateRequestStatus(ctx, request, userv1.RequestCompleted); err != nil {
+ r.Logger.Error(err, "update request status error", "name", request.Name)
+ r.Recorder.Eventf(
+ request,
+ corev1.EventTypeWarning,
+ "UpdateRequestStatusError",
+ "update request %s status error: %s",
+ request.Name,
+ err.Error(),
+ )
+ return ctrl.Result{}, err
+ }
+ return ctrl.Result{}, nil
+}
+
+// isRetained returns true if the request is isCompleted and exist for retention time
+func (r *DeleteRequestReconciler) isRetained(request *userv1.DeleteRequest) bool {
+ if request.Status.Phase == userv1.RequestCompleted &&
+ request.CreationTimestamp.Add(r.retentionTime).Before(time.Now()) {
+ return true
+ }
+ return false
+}
+
+// isCompleted returns true if the request is isCompleted
+func (r *DeleteRequestReconciler) isCompleted(request *userv1.DeleteRequest) bool {
+ return request.Status.Phase == userv1.RequestCompleted
+}
+
+// isExpired returns true if the request is expired
+func (r *DeleteRequestReconciler) isExpired(request *userv1.DeleteRequest) bool {
+ if request.Status.Phase != userv1.RequestCompleted &&
+ request.CreationTimestamp.Add(r.expirationTime).Before(time.Now()) {
+ return true
+ }
+ return false
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *DeleteRequestReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ const controllerName = "deleterequest_controller"
+ if r.Client == nil {
+ r.Client = mgr.GetClient()
+ }
+ r.Logger = ctrl.Log.WithName(controllerName)
+ if r.Recorder == nil {
+ r.Recorder = mgr.GetEventRecorderFor(controllerName)
+ }
+ r.Scheme = mgr.GetScheme()
+ r.Logger.V(1).Info("init reconcile deleterequest controller")
+ r.expirationTime = time.Minute * 10
+ r.retentionTime = time.Minute * 30
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&userv1.DeleteRequest{}, builder.OnlyMetadata).
+ Complete(r)
+}
+
+func (r *DeleteRequestReconciler) deleteRequest(
+ ctx context.Context,
+ request *userv1.DeleteRequest,
+) error {
+ r.Logger.V(1).Info("deleting OperationRequest", "request", request)
+ if err := r.Delete(ctx, request); client.IgnoreNotFound(err) != nil {
+ r.Recorder.Eventf(
+ request,
+ corev1.EventTypeWarning,
+ "Failed to delete OperationRequest",
+ "Failed to delete OperationRequest %s/%s",
+ request.Namespace,
+ request.Name,
+ )
+ r.Logger.Error(err, "Failed to delete OperationRequest", "request", request)
+ return fmt.Errorf("failed to delete OperationRequest %s: %w", request.Name, err)
+ }
+ r.Logger.V(1).Info("delete OperationRequest success")
+ return nil
+}
+
+func (r *DeleteRequestReconciler) updateRequestStatus(
+ ctx context.Context,
+ request *userv1.DeleteRequest,
+ phase userv1.RequestPhase,
+) error {
+ request.Status.Phase = phase
+ if err := r.Status().Update(ctx, request); err != nil {
+ r.Recorder.Eventf(
+ request,
+ corev1.EventTypeWarning,
+ "Failed to update OperationRequest status",
+ "Failed to update OperationRequest status %s/%s",
+ request.Namespace,
+ request.Name,
+ )
+ r.Logger.V(1).Info("update OperationRequest status failed", "request", request)
+ return err
+ }
+ r.Logger.V(1).Info("update OperationRequest status success", "request", request)
+ return nil
+}
+
+// isUserDeleted returns true if the user is deleted
+func isUserDeleted(user userv1.User) bool {
+ return user.Labels["user.sealos.io/status"] == "Deleted"
+}
+
+// isGroupUser returns true if the user is a group user
+func isGroupUser(user userv1.User) bool {
+ return user.Labels["user.sealos.io/type"] == "Group"
+}
diff --git a/controllers/user/controllers/helper/config/config.go b/controllers/user/controllers/helper/config/config.go
new file mode 100644
index 000000000000..db56519164da
--- /dev/null
+++ b/controllers/user/controllers/helper/config/config.go
@@ -0,0 +1,47 @@
+// Copyright © 2024 sealos.
+//
+// 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 config
+
+import (
+ "os"
+
+ "gopkg.in/yaml.v3"
+)
+
+type Config struct {
+ Global `yaml:"global"`
+ Feature `yaml:"feature"`
+}
+
+type Global struct {
+ CloudAPIServerDomain string `yaml:"cloudAPIServerDomain"`
+ CloudAPIServerPort string `yaml:"cloudAPIServerPort"`
+}
+
+type Feature struct {
+ AdminSkip bool `yaml:"adminSkip"`
+}
+
+func LoadConfig(path string, target any) error {
+ configData, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ err = yaml.Unmarshal(configData, target)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/controllers/user/controllers/helper/config/pod_security.go b/controllers/user/controllers/helper/config/pod_security.go
new file mode 100644
index 000000000000..ace5b6d5fa26
--- /dev/null
+++ b/controllers/user/controllers/helper/config/pod_security.go
@@ -0,0 +1,38 @@
+/*
+Copyright 2022 cuisongliu@qq.com.
+
+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 config
+
+import "strings"
+
+const (
+ PodSecurityVersion = "v1.25"
+ PodSecurityLabelPrefix = "pod-security.kubernetes.io/"
+)
+
+func IsPodSecurityLabel(key string) bool {
+ return strings.HasPrefix(key, PodSecurityLabelPrefix)
+}
+
+func SetPodSecurity(labels map[string]string) map[string]string {
+ labels[PodSecurityLabelPrefix+"enforce"] = "baseline"
+ labels[PodSecurityLabelPrefix+"enforce-version"] = PodSecurityVersion
+ labels[PodSecurityLabelPrefix+"audit"] = "restricted"
+ labels[PodSecurityLabelPrefix+"audit-version"] = PodSecurityVersion
+ labels[PodSecurityLabelPrefix+"warn"] = "restricted"
+ labels[PodSecurityLabelPrefix+"warn-version"] = PodSecurityVersion
+ return labels
+}
diff --git a/controllers/user/controllers/helper/config/rbac.go b/controllers/user/controllers/helper/config/rbac.go
new file mode 100644
index 000000000000..69ad21f2a8b0
--- /dev/null
+++ b/controllers/user/controllers/helper/config/rbac.go
@@ -0,0 +1,92 @@
+/*
+Copyright 2023 cuisongliu@qq.com.
+
+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 config
+
+import (
+ "os"
+ "strings"
+
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ rbacv1 "k8s.io/api/rbac/v1"
+)
+
+// AdminClusterRoleBindingName is the legacy cluster-admin binding for admin.
+const AdminClusterRoleBindingName = "sealos-cloudadmin"
+
+// RoleRulesHashAnnotation is used only by the in-memory Role projection.
+const RoleRulesHashAnnotation = "user.sealos.io/cache-role-rules-hash"
+
+func GetUserSystemNamespace() string {
+ return "user-system"
+}
+
+func GetDefaultNamespace() string {
+ return os.Getenv("NAMESPACE_NAME")
+}
+
+func GetUsersSubject(user string) []rbacv1.Subject {
+ return []rbacv1.Subject{
+ {
+ Kind: "ServiceAccount",
+ Name: user,
+ Namespace: GetUserSystemNamespace(),
+ },
+ }
+}
+
+func GetUserNameByNamespace(namespace string) string {
+ return strings.TrimPrefix(namespace, "ns-")
+}
+
+func GetUsersNamespace(user string) string {
+ return "ns-" + user
+}
+
+func GetGroupRoleBindingName(user string) string {
+ return "rb-" + user
+}
+
+func GetUserRole(roleType userv1.RoleType) []rbacv1.PolicyRule {
+ switch roleType {
+ case userv1.OwnerRoleType:
+ return []rbacv1.PolicyRule{
+ {
+ APIGroups: []string{"*"},
+ Resources: []string{"*"},
+ Verbs: []string{"*"},
+ },
+ }
+ case userv1.ManagerRoleType:
+ return []rbacv1.PolicyRule{
+ {
+ APIGroups: []string{"*"},
+ Resources: []string{"*"},
+ Verbs: []string{"*"},
+ },
+ }
+ case userv1.DeveloperRoleType:
+ return []rbacv1.PolicyRule{
+ {
+ APIGroups: []string{"*"},
+ Resources: []string{"*"},
+ Verbs: []string{"list", "watch", "get"},
+ },
+ }
+ default:
+ return []rbacv1.PolicyRule{}
+ }
+}
diff --git a/controllers/user/controllers/helper/finalizer/finalizer.go b/controllers/user/controllers/helper/finalizer/finalizer.go
new file mode 100644
index 000000000000..8399824040f7
--- /dev/null
+++ b/controllers/user/controllers/helper/finalizer/finalizer.go
@@ -0,0 +1,129 @@
+/*
+Copyright 2022 cuisongliu@qq.com.
+
+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 finalizer
+
+import (
+ "context"
+
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/client-go/util/retry"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+type Finalizer struct {
+ client client.Client
+ reader client.Reader
+ finalizerName string
+}
+
+func (f *Finalizer) AddFinalizer(ctx context.Context, obj client.Object) (bool, error) {
+ var notDelete bool
+ if obj.GetDeletionTimestamp() == nil || obj.GetDeletionTimestamp().IsZero() {
+ // The object is not being deleted, so if it does not have our finalizer,
+ // then lets add the finalizer and update the object. This is equivalent
+ // registering our finalizer.
+ notDelete = true
+ if controllerutil.ContainsFinalizer(obj, f.finalizerName) {
+ return notDelete, nil
+ }
+ controllerutil.AddFinalizer(obj, f.finalizerName)
+ if err := f.updateFinalizers(
+ ctx,
+ client.ObjectKeyFromObject(obj),
+ obj,
+ obj.GetFinalizers(),
+ ); err != nil {
+ return notDelete, err
+ }
+ }
+ return notDelete, nil
+}
+
+func DefaultFunc(ctx context.Context, obj client.Object) error {
+ return nil
+}
+
+func NewFinalizer(client client.Client, finalizerName string) *Finalizer {
+ return &Finalizer{
+ client: client,
+ reader: client,
+ finalizerName: finalizerName,
+ }
+}
+
+func (f *Finalizer) WithReader(reader client.Reader) *Finalizer {
+ f.reader = reader
+ return f
+}
+
+func (f *Finalizer) RemoveFinalizer(
+ ctx context.Context,
+ obj client.Object,
+ fun func(ctx context.Context, obj client.Object) error,
+) (bool, error) {
+ var deleteBool bool
+ if obj.GetDeletionTimestamp() != nil && !obj.GetDeletionTimestamp().IsZero() {
+ deleteBool = true
+ if controllerutil.ContainsFinalizer(obj, f.finalizerName) {
+ // our finalizer is present, so lets handle any external dependency
+ if err := fun(ctx, obj); err != nil {
+ return deleteBool, err
+ }
+
+ controllerutil.RemoveFinalizer(obj, f.finalizerName)
+ if err := f.updateFinalizers(
+ ctx,
+ client.ObjectKeyFromObject(obj),
+ obj,
+ obj.GetFinalizers(),
+ ); err != nil {
+ return deleteBool, err
+ }
+ }
+ }
+ return deleteBool, nil
+}
+
+func (f *Finalizer) updateFinalizers(
+ ctx context.Context,
+ objectKey client.ObjectKey,
+ obj client.Object,
+ finalizers []string,
+) error {
+ gvk, err := f.client.GroupVersionKindFor(obj)
+ if err != nil {
+ return err
+ }
+ return retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ fetchObject := &unstructured.Unstructured{}
+ fetchObject.SetAPIVersion(gvk.GroupVersion().String())
+ fetchObject.SetKind(gvk.Kind)
+ err := f.reader.Get(ctx, objectKey, fetchObject)
+ if err != nil {
+ // We log this error, but we continue and try to set the ownerRefs on the other resources.
+ return err
+ }
+ fetchObject.SetFinalizers(finalizers)
+ err = f.client.Update(ctx, fetchObject)
+ if err != nil {
+ // We log this error, but we continue and try to set the ownerRefs on the other resources.
+ return err
+ }
+ return nil
+ })
+}
diff --git a/controllers/user/controllers/helper/finalizer/finalizer_test.go b/controllers/user/controllers/helper/finalizer/finalizer_test.go
new file mode 100644
index 000000000000..bcd259de8255
--- /dev/null
+++ b/controllers/user/controllers/helper/finalizer/finalizer_test.go
@@ -0,0 +1,114 @@
+// Copyright 2026 labring.
+//
+// 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 finalizer
+
+import (
+ "context"
+ "testing"
+
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+func TestAddFinalizerResolvesMissingGVK(t *testing.T) {
+ t.Parallel()
+
+ scheme := runtime.NewScheme()
+ if err := corev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add core scheme: %v", err)
+ }
+ stored := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
+ }
+ cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(stored).Build()
+
+ obj := &corev1.ConfigMap{}
+ key := client.ObjectKeyFromObject(stored)
+ if err := cli.Get(context.Background(), key, obj); err != nil {
+ t.Fatalf("get configmap: %v", err)
+ }
+ if !obj.GroupVersionKind().Empty() {
+ t.Fatalf("fetched object GVK = %s, want empty", obj.GroupVersionKind())
+ }
+
+ const finalizerName = "test.sealos.io/finalizer"
+ updated, err := NewFinalizer(cli, finalizerName).AddFinalizer(context.Background(), obj)
+ if err != nil {
+ t.Fatalf("add finalizer: %v", err)
+ }
+ if !updated {
+ t.Fatal("object was not handled")
+ }
+
+ got := &corev1.ConfigMap{}
+ if err := cli.Get(context.Background(), key, got); err != nil {
+ t.Fatalf("get updated configmap: %v", err)
+ }
+ if !controllerutil.ContainsFinalizer(got, finalizerName) {
+ t.Fatalf("finalizers = %v, want %q", got.Finalizers, finalizerName)
+ }
+}
+
+type updateCountingClient struct {
+ client.Client
+ updates int
+}
+
+func (c *updateCountingClient) Update(
+ ctx context.Context,
+ obj client.Object,
+ opts ...client.UpdateOption,
+) error {
+ c.updates++
+ return c.Client.Update(ctx, obj, opts...)
+}
+
+func TestAddFinalizerSkipsExistingFinalizerUpdate(t *testing.T) {
+ t.Parallel()
+
+ scheme := runtime.NewScheme()
+ if err := corev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add core scheme: %v", err)
+ }
+ const finalizerName = "test.sealos.io/finalizer"
+ stored := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ Namespace: "default",
+ Finalizers: []string{finalizerName},
+ },
+ }
+ baseClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(stored).Build()
+ cli := &updateCountingClient{Client: baseClient}
+
+ obj := &corev1.ConfigMap{}
+ if err := cli.Get(context.Background(), client.ObjectKeyFromObject(stored), obj); err != nil {
+ t.Fatalf("get configmap: %v", err)
+ }
+ handled, err := NewFinalizer(cli, finalizerName).AddFinalizer(context.Background(), obj)
+ if err != nil {
+ t.Fatalf("add finalizer: %v", err)
+ }
+ if !handled {
+ t.Fatal("object was not handled")
+ }
+ if cli.updates != 0 {
+ t.Fatalf("updates = %d, want 0", cli.updates)
+ }
+}
diff --git a/controllers/user/controllers/helper/hash/hash.go b/controllers/user/controllers/helper/hash/hash.go
new file mode 100644
index 000000000000..9a5235d2a449
--- /dev/null
+++ b/controllers/user/controllers/helper/hash/hash.go
@@ -0,0 +1,60 @@
+// Copyright © 2022 The sealos Authors.
+//
+// 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.
+
+/*
+import from "k8s.io/kubernetes/pkg/util/hash"
+*/
+
+package hash
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "hash"
+
+ "github.com/davecgh/go-spew/spew"
+)
+
+// DeepHashObject writes specified object to hash using the spew library
+// which follows pointers and prints actual values of the nested objects
+// ensuring the hash does not change when a pointer changes.
+func DeepHashObject(hasher hash.Hash, objectToWrite any) {
+ hasher.Reset()
+ printer := spew.ConfigState{
+ Indent: " ",
+ SortKeys: true,
+ DisableMethods: true,
+ SpewKeys: true,
+ }
+ printer.Fprintf(hasher, "%#v", objectToWrite)
+}
+
+// HashToString gen hash string base on actual values of the nested objects.
+func HashToString(obj any) string {
+ hasher := sha256.New()
+ DeepHashObject(hasher, obj)
+ return hex.EncodeToString(hasher.Sum(nil)[0:])
+}
+
+func Hash(data any) string {
+ dataByte, err := json.Marshal(data)
+ if err != nil {
+ hasher := sha256.New()
+ DeepHashObject(hasher, data)
+ return hex.EncodeToString(hasher.Sum(nil))
+ }
+ sum := sha256.Sum256(dataByte)
+ return hex.EncodeToString(sum[:])
+}
diff --git a/controllers/user/controllers/helper/helper.go b/controllers/user/controllers/helper/helper.go
new file mode 100644
index 000000000000..c907678c695e
--- /dev/null
+++ b/controllers/user/controllers/helper/helper.go
@@ -0,0 +1,109 @@
+/*
+Copyright 2022 cuisongliu@qq.com.
+
+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 helper
+
+import (
+ v1 "github.com/labring/sealos/controllers/user/api/v1"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func IsConditionTrue(conditions []v1.Condition, condition v1.Condition) bool {
+ for _, con := range conditions {
+ if con.Type == condition.Type && con.Status == condition.Status {
+ return true
+ }
+ }
+ return false
+}
+
+func IsConditionsTrue(conditions []v1.Condition) bool {
+ if len(conditions) == 0 {
+ return false
+ }
+ for _, condition := range conditions {
+ if condition.Type == v1.Ready {
+ continue
+ }
+ if condition.Status != corev1.ConditionTrue {
+ return false
+ }
+ }
+ return true
+}
+
+func GetCondition(conditions []v1.Condition, condition *v1.Condition) *v1.Condition {
+ for _, con := range conditions {
+ if con.Type == condition.Type {
+ return con.DeepCopy()
+ }
+ }
+ return condition.DeepCopy()
+}
+
+func DiffCondition(condition1, condition2 *v1.Condition) bool {
+ if condition1.Reason != condition2.Reason || condition1.Status != condition2.Status ||
+ condition1.Message != condition2.Message {
+ return true
+ }
+ return false
+}
+
+// UpdateCondition updates condition in cluster conditions using giving condition
+// adds condition if not existed
+func UpdateCondition(conditions []v1.Condition, condition v1.Condition) []v1.Condition {
+ if conditions == nil {
+ conditions = make([]v1.Condition, 0)
+ }
+ hasCondition := false
+ for i, cond := range conditions {
+ if cond.Type == condition.Type {
+ hasCondition = true
+ if DiffCondition(conditions[i].DeepCopy(), condition.DeepCopy()) {
+ conditions[i] = condition
+ }
+ }
+ }
+ if !hasCondition {
+ conditions = append(conditions, condition)
+ }
+ return conditions
+}
+
+func DeleteCondition(conditions []v1.Condition, conditionType v1.ConditionType) []v1.Condition {
+ for i, condition := range conditions {
+ if condition.Type != conditionType {
+ continue
+ }
+ filtered := make([]v1.Condition, 0, len(conditions)-1)
+ filtered = append(filtered, conditions[:i]...)
+ for _, remaining := range conditions[i+1:] {
+ if remaining.Type != conditionType {
+ filtered = append(filtered, remaining)
+ }
+ }
+ return filtered
+ }
+ return conditions
+}
+
+func SetConditionError(condition *v1.Condition, reason string, err error) {
+ condition.LastHeartbeatTime = metav1.Now()
+ condition.Status = corev1.ConditionFalse
+ condition.Reason = reason
+ condition.Message = err.Error()
+}
diff --git a/controllers/user/controllers/helper/helper_test.go b/controllers/user/controllers/helper/helper_test.go
new file mode 100644
index 000000000000..b9887ae0ed72
--- /dev/null
+++ b/controllers/user/controllers/helper/helper_test.go
@@ -0,0 +1,54 @@
+// Copyright 2026 labring.
+//
+// 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 helper
+
+import (
+ "testing"
+
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ corev1 "k8s.io/api/core/v1"
+)
+
+func TestGetConditionReturnsIndependentCopyWhenMissing(t *testing.T) {
+ t.Parallel()
+ desired := &userv1.Condition{Type: userv1.Ready, Status: corev1.ConditionTrue}
+ got := GetCondition(nil, desired)
+ got.Status = corev1.ConditionFalse
+ if desired.Status != corev1.ConditionTrue {
+ t.Fatal("missing condition result aliases the desired condition")
+ }
+}
+
+func TestDeleteConditionKeepsOriginalSliceWhenUnchanged(t *testing.T) {
+ t.Parallel()
+ conditions := []userv1.Condition{{Type: userv1.Ready}}
+ got := DeleteCondition(conditions, userv1.Initialized)
+ if len(got) != len(conditions) || &got[0] != &conditions[0] {
+ t.Fatal("DeleteCondition allocated or changed a slice without a matching condition")
+ }
+}
+
+func TestDeleteConditionRemovesAllMatchingConditions(t *testing.T) {
+ t.Parallel()
+ conditions := []userv1.Condition{
+ {Type: userv1.Ready},
+ {Type: userv1.Initialized},
+ {Type: userv1.Initialized},
+ }
+ got := DeleteCondition(conditions, userv1.Initialized)
+ if len(got) != 1 || got[0].Type != userv1.Ready {
+ t.Fatalf("remaining conditions = %#v, want only Ready", got)
+ }
+}
diff --git a/controllers/user/controllers/helper/kubeconfig/cert.go b/controllers/user/controllers/helper/kubeconfig/cert.go
new file mode 100644
index 000000000000..004e5a18c323
--- /dev/null
+++ b/controllers/user/controllers/helper/kubeconfig/cert.go
@@ -0,0 +1,169 @@
+/*
+Copyright 2022 cuisongliu@qq.com.
+
+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 kubeconfig
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "fmt"
+ "math"
+ "math/big"
+ "net"
+ "time"
+
+ confighelper "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd/api"
+ "k8s.io/client-go/util/cert"
+ "k8s.io/client-go/util/keyutil"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// tryLoadKeyFromDisk tries to load the key from the disk and validates that it is valid
+func tryLoadKeyFromDisk(pkiPath string) (crypto.Signer, error) {
+ // Parse the private key from a file
+ privKey, err := keyutil.PrivateKeyFromFile(pkiPath)
+ if err != nil {
+ return nil, fmt.Errorf("couldn't load the private key file %w", err)
+ }
+
+ // Allow RSA and ECDSA formats only
+ var key crypto.Signer
+ switch k := privKey.(type) {
+ case *rsa.PrivateKey:
+ key = k
+ case *ecdsa.PrivateKey:
+ key = k
+ default:
+ return nil, fmt.Errorf("couldn't convert the private key file %w", err)
+ }
+
+ return key, nil
+}
+
+// encodeCertPEM returns PEM-endcoded certificate data
+func encodeCertPEM(cert *x509.Certificate) []byte {
+ block := pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: cert.Raw,
+ }
+ return pem.EncodeToMemory(&block)
+}
+
+func (c *CertConfig) Apply(config *rest.Config, _ client.Client) (*api.Config, error) {
+ // make sure cadata is loaded into config under incluster mode
+ if err := rest.LoadTLSFiles(config); err != nil {
+ return nil, err
+ }
+ certs, err := cert.ParseCertsPEM(config.CAData)
+ if err != nil {
+ return nil, fmt.Errorf("error reading by config: %s", err.Error())
+ }
+ caCert := certs[0]
+ caKey, err := tryLoadKeyFromDisk(c.caKeyFile)
+ if err != nil {
+ return nil, fmt.Errorf("load ca key file failed %w", err)
+ }
+ clientCert, clientKey, err := newCertAndKey(
+ caCert,
+ caKey,
+ c.user,
+ c.groups,
+ c.dnsNames,
+ c.ipAddresses,
+ c.expirationSeconds,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("new client key failed %w", err)
+ }
+ encodedClientKey, err := keyutil.MarshalPrivateKeyToPEM(clientKey)
+ if err != nil {
+ return nil, fmt.Errorf("encode client key failed %w", err)
+ }
+ encodedClientCert := encodeCertPEM(clientCert)
+ ctx := fmt.Sprintf("%s@%s", c.user, c.clusterName)
+ return &api.Config{
+ Clusters: map[string]*api.Cluster{
+ c.clusterName: {
+ Server: GetKubernetesHost(config),
+ CertificateAuthorityData: encodeCertPEM(caCert),
+ },
+ },
+ Contexts: map[string]*api.Context{
+ ctx: {
+ Cluster: c.clusterName,
+ AuthInfo: c.user,
+ Namespace: confighelper.GetUsersNamespace(c.user),
+ },
+ },
+ AuthInfos: map[string]*api.AuthInfo{
+ c.user: {
+ ClientCertificateData: encodedClientCert,
+ ClientKeyData: encodedClientKey,
+ },
+ },
+ CurrentContext: ctx,
+ }, nil
+}
+
+func newCertAndKey(
+ caCert *x509.Certificate,
+ caKey crypto.Signer,
+ user string,
+ groups, dnsNames []string,
+ ipAddresses []net.IP,
+ expiration int32,
+) (*x509.Certificate, crypto.Signer, error) {
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return nil, nil, fmt.Errorf("generate client key error %w", err)
+ }
+ serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64))
+ if err != nil {
+ return nil, nil, fmt.Errorf("rand serial error %w", err)
+ }
+
+ noAfter := time.Now().Add(time.Second * time.Duration(expiration))
+
+ certTmpl := x509.Certificate{
+ Subject: pkix.Name{
+ CommonName: user,
+ Organization: groups,
+ },
+ DNSNames: dnsNames,
+ IPAddresses: ipAddresses,
+ SerialNumber: serial,
+ NotBefore: caCert.NotBefore,
+ NotAfter: noAfter,
+ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+ }
+ certDERBytes, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, key.Public(), caKey)
+ if err != nil {
+ return nil, nil, fmt.Errorf("create cert failed %w", err)
+ }
+ cert, err := x509.ParseCertificate(certDERBytes)
+ if err != nil {
+ return nil, nil, fmt.Errorf("parse cert failed %w", err)
+ }
+ return cert, key, nil
+}
diff --git a/controllers/user/controllers/helper/kubeconfig/csr.go b/controllers/user/controllers/helper/kubeconfig/csr.go
new file mode 100644
index 000000000000..c09cbcbabf33
--- /dev/null
+++ b/controllers/user/controllers/helper/kubeconfig/csr.go
@@ -0,0 +1,234 @@
+/*
+Copyright 2022 cuisongliu@qq.com.
+
+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 kubeconfig
+
+import (
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "fmt"
+ "net"
+ "time"
+
+ config2 "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ csrv1 "k8s.io/api/certificates/v1"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/errors"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/watch"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd/api"
+ "k8s.io/client-go/util/retry"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// newPrivateKey creates an RSA private key
+func newPrivateKey(keyType x509.PublicKeyAlgorithm) (crypto.Signer, error) {
+ if keyType == x509.ECDSA {
+ return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ }
+
+ return rsa.GenerateKey(rand.Reader, 2048)
+}
+
+func (csr *CsrConfig) newSignedToCsrKey() (csrData, keyPEM []byte, err error) {
+ key, err := newPrivateKey(x509.RSA)
+ if err != nil {
+ return nil, nil, fmt.Errorf("new signed private failed %w", err)
+ }
+ rsaKey, ok := key.(*rsa.PrivateKey)
+ if !ok {
+ return nil, nil, fmt.Errorf("new signed private failed unexpected key type %T", key)
+ }
+ pk := x509.MarshalPKCS1PrivateKey(rsaKey)
+ keyPEM = pem.EncodeToMemory(&pem.Block{
+ Type: "RSA PRIVATE KEY", Bytes: pk,
+ })
+ _, csrObj, err := csr.generateCSR(key)
+ if err != nil {
+ return nil, nil, fmt.Errorf("new signed csr failed %w", err)
+ }
+ csrData = pem.EncodeToMemory(&pem.Block{
+ Type: "CERTIFICATE REQUEST", Bytes: csrObj,
+ })
+ if err != nil {
+ return nil, nil, fmt.Errorf("new signed csr failed %w", err)
+ }
+
+ return csrData, keyPEM, err
+}
+
+func (csr *CsrConfig) Apply(config *rest.Config, client client.Client) (*api.Config, error) {
+ csrKey, key, err := csr.newSignedToCsrKey()
+ if err != nil {
+ return nil, err
+ }
+ // make sure cadata is loaded into config under incluster mode
+ if err = rest.LoadTLSFiles(config); err != nil {
+ return nil, err
+ }
+ ca := config.CAData
+ csr.ctxCAKey = ca
+ csr.ctxTLSKey = key
+ csr.ctxTLSCsr = csrKey
+ if err = csr.updateCsr(config, client); err != nil {
+ return nil, err
+ }
+ ctx := fmt.Sprintf("%s@%s", csr.user, csr.clusterName)
+ return &api.Config{
+ Clusters: map[string]*api.Cluster{
+ csr.clusterName: {
+ Server: GetKubernetesHost(config),
+ CertificateAuthorityData: ca,
+ },
+ },
+ Contexts: map[string]*api.Context{
+ ctx: {
+ Cluster: csr.clusterName,
+ AuthInfo: csr.user,
+ Namespace: config2.GetUsersNamespace(csr.user),
+ },
+ },
+ AuthInfos: map[string]*api.AuthInfo{
+ csr.user: {
+ ClientCertificateData: csr.ctxTLSCrt,
+ ClientKeyData: key,
+ },
+ },
+ CurrentContext: ctx,
+ }, nil
+}
+
+func (csr *CsrConfig) updateCsr(config *rest.Config, cli client.Client) error {
+ var csrResource *csrv1.CertificateSigningRequest
+ if csr.csr != nil {
+ csrResource = csr.csr.DeepCopy()
+ } else {
+ csrName := "sealos-generater-" + csr.user
+ csrResource = &csrv1.CertificateSigningRequest{}
+ csrResource.Name = csrName
+ if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ insertCSR := csrResource.DeepCopy()
+ insertCSR.ResourceVersion = "0"
+ insertCSR.Spec.Request = csr.ctxTLSCsr
+ insertCSR.Spec.SignerName = csrv1.KubeAPIServerClientSignerName
+ insertCSR.Spec.ExpirationSeconds = &csr.expirationSeconds
+ insertCSR.Spec.Groups = []string{"system:authenticated"}
+ insertCSR.Spec.Usages = []csrv1.KeyUsage{
+ "digital signature",
+ "key encipherment",
+ "client auth",
+ }
+
+ err := cli.Create(context.TODO(), insertCSR)
+ if err != nil {
+ if !errors.IsAlreadyExists(err) {
+ return err
+ }
+ }
+ csrResource = insertCSR.DeepCopy()
+ return nil
+ }); err != nil {
+ return err
+ }
+ }
+
+ clientset, err := kubernetes.NewForConfig(config)
+ if err != nil {
+ return err
+ }
+
+ csrResource.Status.Conditions = []csrv1.CertificateSigningRequestCondition{
+ {
+ Type: csrv1.CertificateApproved,
+ Status: corev1.ConditionTrue,
+ Reason: "AutoApproved",
+ Message: "This CSR was approved by user certificate approve.",
+ },
+ }
+ _, err = clientset.CertificatesV1().
+ CertificateSigningRequests().
+ UpdateApproval(context.TODO(), csrResource.Name, csrResource, v1.UpdateOptions{})
+ if err != nil {
+ return err
+ }
+
+ w, err := clientset.CertificatesV1().
+ CertificateSigningRequests().
+ Watch(context.TODO(), v1.ListOptions{FieldSelector: "metadata.name=" + csrResource.Name})
+ if err != nil {
+ return err
+ }
+ start := time.Now()
+ for {
+ select {
+ case <-time.After(time.Second * 10):
+ return errors.NewBadRequest("The CSR is not ready.")
+ case event := <-w.ResultChan():
+ if event.Type == watch.Modified || event.Type == watch.Added {
+ certificateSigningRequest, ok := event.Object.(*csrv1.CertificateSigningRequest)
+ if !ok {
+ return errors.NewBadRequest("unexpected csr watch type")
+ }
+ if certificateSigningRequest.Status.Certificate != nil {
+ csr.ctxTLSCrt = certificateSigningRequest.Status.Certificate
+ dis := time.Since(start).Milliseconds()
+ defaultLog.Info("The csr is ready", "using Milliseconds", dis)
+ return nil
+ }
+ }
+ }
+ }
+}
+
+// generateCSR will generate a new *x509.CertificateRequest template to be used
+// by issuers that utilise CSRs to obtain Certificates.
+// The CSR will not be signed, and should be passed to either EncodeCSR or
+// to the x509.CreateCertificateRequest function.
+func (csr *CsrConfig) generateCSR(key crypto.Signer) (*x509.CertificateRequest, []byte, error) {
+ if len(csr.user) == 0 {
+ return nil, nil, errors.NewBadRequest("must specify a CommonName")
+ }
+
+ dnsNames := make([]string, 0, len(csr.dnsNames))
+ ips := make([]net.IP, 0, len(csr.ipAddresses))
+
+ dnsNames = append(dnsNames, csr.dnsNames...)
+ ips = append(ips, csr.ipAddresses...)
+ certTmpl := x509.CertificateRequest{
+ Subject: pkix.Name{
+ CommonName: csr.user,
+ Organization: csr.groups,
+ },
+
+ DNSNames: dnsNames,
+ IPAddresses: ips,
+ }
+ certDERBytes, err := x509.CreateCertificateRequest(rand.Reader, &certTmpl, key)
+ if err != nil {
+ return nil, nil, err
+ }
+ r1, r3 := x509.ParseCertificateRequest(certDERBytes)
+ return r1, certDERBytes, r3
+}
diff --git a/controllers/user/controllers/helper/kubeconfig/interface.go b/controllers/user/controllers/helper/kubeconfig/interface.go
new file mode 100644
index 000000000000..3d036f8d0d6b
--- /dev/null
+++ b/controllers/user/controllers/helper/kubeconfig/interface.go
@@ -0,0 +1,165 @@
+/*
+Copyright 2023 cuisongliu@qq.com.
+
+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 kubeconfig
+
+import (
+ "context"
+ "net"
+ "os"
+
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ csrv1 "k8s.io/api/certificates/v1"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd/api"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+var defaultLog = ctrl.Log.WithName("kubeconfig")
+
+const defaultCSRExpirationSeconds int32 = userv1.DefaultCSRExpirationSeconds
+
+type Interface interface {
+ Apply(config *rest.Config, client client.Client) (*api.Config, error)
+}
+
+type TokenRequestInterface interface {
+ Interface
+ ApplyWithTokenRequest(
+ ctx context.Context,
+ config *rest.Config,
+ client client.Client,
+ ) (*api.Config, metav1.Time, error)
+}
+
+type CertConfig struct {
+ *DefaultConfig
+ caKeyFile string
+ groups []string
+ dnsNames []string
+ ipAddresses []net.IP
+}
+
+type CsrConfig struct {
+ *DefaultConfig
+ groups []string
+ dnsNames []string
+ ipAddresses []net.IP
+ csr *csrv1.CertificateSigningRequest
+ ctxCAKey []byte
+ ctxTLSKey []byte
+ ctxTLSCrt []byte
+ ctxTLSCsr []byte
+}
+type ServiceAccountConfig struct {
+ *DefaultConfig
+ namespace string
+ secretName string
+ forceNewSecret bool
+ sa *v1.ServiceAccount
+}
+
+type WebhookConfig struct {
+ *DefaultConfig
+ webhookURL string
+}
+
+func GetKubernetesHost(config *rest.Config) string {
+ host, port := os.Getenv("SEALOS_CLOUD_APISERVER_HOST"), os.Getenv("SEALOS_CLOUD_APISERVER_PORT")
+ if len(host) != 0 && len(port) != 0 {
+ return "https://" + net.JoinHostPort(host, port)
+ }
+ host, port = os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
+ if len(host) == 0 || len(port) == 0 {
+ return config.Host
+ }
+ return "https://" + net.JoinHostPort(host, port)
+}
+
+type DefaultConfig struct {
+ user string
+ clusterName string // default is kubernetes
+ expirationSeconds int32
+}
+
+func NewConfig(user, clusterName string, expirationSeconds int32) *DefaultConfig {
+ if clusterName == "" {
+ clusterName = "sealos"
+ }
+ expirationSeconds = userv1.NormalizeCSRExpirationSeconds(expirationSeconds)
+ return &DefaultConfig{
+ user: user,
+ clusterName: clusterName,
+ expirationSeconds: expirationSeconds,
+ }
+}
+
+func (d *DefaultConfig) WithCertConfig(
+ caKeyFile string,
+ groups, dnsNames []string,
+ ipAddrs []net.IP,
+) Interface {
+ return &CertConfig{
+ DefaultConfig: d,
+ caKeyFile: caKeyFile,
+ groups: groups,
+ dnsNames: dnsNames,
+ ipAddresses: ipAddrs,
+ }
+}
+
+func (d *DefaultConfig) WithCsrConfig(
+ groups, dnsNames []string,
+ ipAddrs []net.IP,
+ csr *csrv1.CertificateSigningRequest,
+) Interface {
+ return &CsrConfig{
+ DefaultConfig: d,
+ groups: groups,
+ dnsNames: dnsNames,
+ ipAddresses: ipAddrs,
+ csr: csr,
+ }
+}
+
+func (d *DefaultConfig) WithServiceAccountConfig(
+ namespace string,
+ sa *v1.ServiceAccount,
+) *ServiceAccountConfig {
+ if namespace == "" {
+ namespace = "default"
+ }
+ return &ServiceAccountConfig{
+ DefaultConfig: d,
+ namespace: namespace,
+ sa: sa,
+ }
+}
+
+func (sac *ServiceAccountConfig) WithForceNewSecret() *ServiceAccountConfig {
+ sac.forceNewSecret = true
+ return sac
+}
+
+func (d *DefaultConfig) WithWebhookConfigConfig(webhookURL string) Interface {
+ return &WebhookConfig{
+ DefaultConfig: d,
+ webhookURL: webhookURL,
+ }
+}
diff --git a/controllers/user/controllers/helper/kubeconfig/sa.go b/controllers/user/controllers/helper/kubeconfig/sa.go
new file mode 100644
index 000000000000..b14f63876bd0
--- /dev/null
+++ b/controllers/user/controllers/helper/kubeconfig/sa.go
@@ -0,0 +1,287 @@
+/*
+Copyright 2022 cuisongliu@qq.com.
+
+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 kubeconfig
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "errors"
+ "fmt"
+
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ config2 "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ authenticationv1 "k8s.io/api/authentication/v1"
+ v1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd/api"
+ "k8s.io/utils/ptr"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+func (sac *ServiceAccountConfig) Apply(
+ config *rest.Config,
+ client client.Client,
+) (*api.Config, error) {
+ cfg, _, err := sac.ApplyWithTokenRequest(context.Background(), config, client)
+ return cfg, err
+}
+
+func (sac *ServiceAccountConfig) ApplyWithTokenRequest(
+ ctx context.Context,
+ config *rest.Config,
+ client client.Client,
+) (*api.Config, metav1.Time, error) {
+ if err := sac.applyServiceAccount(ctx, config, client); err != nil {
+ return nil, metav1.Time{}, fmt.Errorf("failed to apply service account error: %w", err)
+ }
+ boundSecret, err := sac.applyBoundTokenSecret(ctx, client)
+ if err != nil {
+ return nil, metav1.Time{}, fmt.Errorf("failed to apply bound token secret: %w", err)
+ }
+ tokenRequest, err := sac.requestToken(ctx, config, boundSecret)
+ if err != nil {
+ return nil, metav1.Time{}, fmt.Errorf("failed to fetch token: %w", err)
+ }
+ cfg, err := sac.generatorKubeConfig(config, tokenRequest.Status.Token)
+ if err != nil {
+ return nil, metav1.Time{}, fmt.Errorf("failed to generate kube config: %w", err)
+ }
+ return cfg, tokenRequest.Status.ExpirationTimestamp, nil
+}
+
+func (sac *ServiceAccountConfig) applyServiceAccount(
+ ctx context.Context,
+ _ *rest.Config,
+ client client.Client,
+) error {
+ sa := sac.sa
+ if sa == nil {
+ sa = &v1.ServiceAccount{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: sac.user,
+ Namespace: sac.namespace,
+ },
+ }
+ }
+ if sa.Name == "" {
+ sa.Name = sac.user
+ }
+ if sa.Namespace == "" {
+ sa.Namespace = sac.namespace
+ }
+ _, err := controllerutil.CreateOrUpdate(ctx, client, sa, func() error {
+ if sac.forceNewSecret || len(sa.Secrets) == 0 {
+ sa.Secrets = []v1.ObjectReference{
+ {
+ Name: sac.generateSecretName(),
+ },
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ sac.sa = sa
+ if len(sa.Secrets) > 0 {
+ sac.secretName = sa.Secrets[0].Name
+ }
+ return nil
+}
+
+func (sac *ServiceAccountConfig) applyBoundTokenSecret(
+ ctx context.Context,
+ cli client.Client,
+) (*v1.Secret, error) {
+ secretName := sac.secretName
+ if secretName == "" {
+ secretName = sac.generateSecretName()
+ }
+ secret := &v1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: secretName,
+ Namespace: sac.namespace,
+ },
+ }
+ _, err := controllerutil.CreateOrUpdate(ctx, cli, secret, func() error {
+ // Secret type is immutable after creation. Keep the type of legacy
+ // bound secrets and use Opaque only for newly created secrets.
+ if secret.Type == "" {
+ secret.Type = v1.SecretTypeOpaque
+ }
+ if secret.Annotations == nil {
+ secret.Annotations = map[string]string{}
+ }
+ secret.Annotations[v1.ServiceAccountNameKey] = sac.user
+ if sac.sa != nil {
+ secret.OwnerReferences = append([]metav1.OwnerReference(nil), sac.sa.OwnerReferences...)
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ if secret.UID == "" {
+ return nil, fmt.Errorf(
+ "bound token secret %s/%s has empty uid",
+ secret.Namespace,
+ secret.Name,
+ )
+ }
+ return secret, nil
+}
+
+// CleanupLegacyBoundTokenSecrets removes stale bound token secrets for a user.
+func CleanupLegacyBoundTokenSecrets(
+ ctx context.Context,
+ reader client.Reader,
+ writer client.Writer,
+ userName, keepSecretName string,
+) error {
+ secrets := &metav1.PartialObjectMetadataList{}
+ secrets.SetGroupVersionKind(schema.GroupVersion{Version: "v1"}.WithKind("SecretList"))
+ if err := reader.List(
+ ctx,
+ secrets,
+ client.InNamespace(config2.GetUserSystemNamespace()),
+ client.MatchingFields{v1.ServiceAccountNameKey: userName},
+ ); err != nil {
+ return fmt.Errorf("failed to list legacy bound token secrets: %w", err)
+ }
+ if keepSecretName == "" {
+ return errors.New("keep secret name is empty")
+ }
+ for i := range secrets.Items {
+ secret := &secrets.Items[i]
+ if secret.Name == "" || secret.Name == keepSecretName {
+ continue
+ }
+ if secret.Annotations == nil || secret.Annotations[v1.ServiceAccountNameKey] != userName {
+ continue
+ }
+ deleteTarget := &v1.Secret{ObjectMeta: metav1.ObjectMeta{
+ Name: secret.Name, Namespace: secret.Namespace,
+ }}
+ if err := writer.Delete(ctx, deleteTarget); err != nil && !apierrors.IsNotFound(err) {
+ return fmt.Errorf("failed to delete legacy bound token secret %s: %w", secret.Name, err)
+ }
+ }
+ return nil
+}
+
+func (sac *ServiceAccountConfig) requestToken(
+ ctx context.Context,
+ config *rest.Config,
+ boundSecret *v1.Secret,
+) (*authenticationv1.TokenRequest, error) {
+ clientset, err := kubernetes.NewForConfig(config)
+ if err != nil {
+ return nil, err
+ }
+ tokenRequest, err := clientset.CoreV1().
+ ServiceAccounts(sac.namespace).
+ CreateToken(ctx, sac.user, &authenticationv1.TokenRequest{
+ Spec: authenticationv1.TokenRequestSpec{
+ ExpirationSeconds: ptr.To(int64(sac.tokenRequestExpirationSeconds())),
+ BoundObjectRef: &authenticationv1.BoundObjectReference{
+ Kind: "Secret",
+ APIVersion: "v1",
+ Name: boundSecret.Name,
+ UID: boundSecret.UID,
+ },
+ },
+ }, metav1.CreateOptions{})
+ if err != nil {
+ return nil, err
+ }
+ if tokenRequest.Status.Token == "" {
+ return nil, fmt.Errorf(
+ "token request returned empty token for serviceaccount %s/%s",
+ sac.namespace,
+ sac.user,
+ )
+ }
+ return tokenRequest, nil
+}
+
+func (sac *ServiceAccountConfig) tokenRequestExpirationSeconds() int32 {
+ return userv1.NormalizeCSRExpirationSeconds(sac.expirationSeconds)
+}
+
+func TokenSecretName(name string) string {
+ return "sealos-token-" + name
+}
+
+func (sac *ServiceAccountConfig) generateSecretName() string {
+ if sac.secretName != "" {
+ return sac.secretName
+ }
+ if !sac.forceNewSecret &&
+ sac.sa != nil &&
+ len(sac.sa.Secrets) > 0 &&
+ sac.sa.Secrets[0].Name != "" {
+ return sac.sa.Secrets[0].Name
+ }
+ return "sealos-token-" + sac.user + "-" + GetRandomString(5)
+}
+
+func GetRandomString(n int) string {
+ randBytes := make([]byte, n/2)
+ if _, err := rand.Read(randBytes); err != nil {
+ return ""
+ }
+ return hex.EncodeToString(randBytes)
+}
+
+func (sac *ServiceAccountConfig) generatorKubeConfig(
+ cfg *rest.Config,
+ token string,
+) (*api.Config, error) {
+ // make sure cadata is loaded into config under incluster mode
+ if err := rest.LoadTLSFiles(cfg); err != nil {
+ return nil, err
+ }
+ ctx := fmt.Sprintf("%s@%s", sac.user, sac.clusterName)
+ config := &api.Config{
+ Clusters: map[string]*api.Cluster{
+ sac.clusterName: {
+ Server: GetKubernetesHost(cfg),
+ CertificateAuthorityData: cfg.CAData,
+ },
+ },
+ Contexts: map[string]*api.Context{
+ ctx: {
+ Cluster: sac.clusterName,
+ AuthInfo: sac.user,
+ Namespace: config2.GetUsersNamespace(sac.user),
+ },
+ },
+ AuthInfos: map[string]*api.AuthInfo{
+ sac.user: {
+ Token: token,
+ },
+ },
+ CurrentContext: ctx,
+ }
+ return config, nil
+}
diff --git a/controllers/user/controllers/helper/kubeconfig/sa_legacy_secret_test.go b/controllers/user/controllers/helper/kubeconfig/sa_legacy_secret_test.go
new file mode 100644
index 000000000000..a2b56c4265fc
--- /dev/null
+++ b/controllers/user/controllers/helper/kubeconfig/sa_legacy_secret_test.go
@@ -0,0 +1,279 @@
+/*
+Copyright 2022 labring.
+
+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 kubeconfig
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ config2 "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/kubernetes/scheme"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+type canceledContextClient struct {
+ client.Client
+}
+
+func (c canceledContextClient) Get(
+ ctx context.Context,
+ key client.ObjectKey,
+ obj client.Object,
+ opts ...client.GetOption,
+) error {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ return errors.New("service account read did not receive the canceled context")
+}
+
+func TestNewConfigNormalizesBelowMinimumExpiration(t *testing.T) {
+ t.Parallel()
+
+ cfg := NewConfig("alice", "", 7_200)
+ if cfg.expirationSeconds != userv1.DefaultCSRExpirationSeconds {
+ t.Fatalf(
+ "config expiration = %d, want minimum %d",
+ cfg.expirationSeconds,
+ userv1.DefaultCSRExpirationSeconds,
+ )
+ }
+ sac := cfg.WithServiceAccountConfig("user-system", nil)
+ if got := sac.tokenRequestExpirationSeconds(); got != userv1.DefaultCSRExpirationSeconds {
+ t.Fatalf(
+ "token request expiration = %d, want minimum %d",
+ got,
+ userv1.DefaultCSRExpirationSeconds,
+ )
+ }
+}
+
+func TestCleanupLegacyBoundTokenSecrets(t *testing.T) {
+ t.Parallel()
+
+ const (
+ userName = "alice"
+ currentSecret = "sealos-token-alice-new"
+ legacySecret = "sealos-token-alice-old"
+ duplicateSecret = "sealos-token-alice-dupe"
+ otherSecret = "sealos-token-bob-old"
+ )
+
+ current := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: currentSecret,
+ Namespace: config2.GetUserSystemNamespace(),
+ Annotations: map[string]string{
+ corev1.ServiceAccountNameKey: userName,
+ },
+ },
+ Type: corev1.SecretTypeOpaque,
+ }
+ legacy := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: legacySecret,
+ Namespace: config2.GetUserSystemNamespace(),
+ Annotations: map[string]string{
+ corev1.ServiceAccountNameKey: userName,
+ },
+ },
+ Type: corev1.SecretTypeServiceAccountToken,
+ }
+ duplicate := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: duplicateSecret,
+ Namespace: config2.GetUserSystemNamespace(),
+ Annotations: map[string]string{
+ corev1.ServiceAccountNameKey: userName,
+ },
+ },
+ Type: corev1.SecretTypeOpaque,
+ }
+ other := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: otherSecret,
+ Namespace: config2.GetUserSystemNamespace(),
+ Annotations: map[string]string{
+ corev1.ServiceAccountNameKey: "bob",
+ },
+ },
+ Type: corev1.SecretTypeServiceAccountToken,
+ }
+
+ secretMetadata := &metav1.PartialObjectMetadata{}
+ secretMetadata.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Secret"))
+ cli := fake.NewClientBuilder().
+ WithScheme(scheme.Scheme).
+ WithObjects(current, legacy, duplicate, other).
+ WithIndex(secretMetadata, corev1.ServiceAccountNameKey, func(obj client.Object) []string {
+ if obj.GetAnnotations() == nil {
+ return nil
+ }
+ value := obj.GetAnnotations()[corev1.ServiceAccountNameKey]
+ if value == "" {
+ return nil
+ }
+ return []string{value}
+ }).
+ Build()
+
+ if err := CleanupLegacyBoundTokenSecrets(
+ context.Background(),
+ cli,
+ cli,
+ userName,
+ currentSecret,
+ ); err != nil {
+ t.Fatalf("cleanup legacy secrets: %v", err)
+ }
+
+ var got corev1.Secret
+ if err := cli.Get(context.Background(), client.ObjectKeyFromObject(current), &got); err != nil {
+ t.Fatalf("get current secret: %v", err)
+ }
+ if got.Type != corev1.SecretTypeOpaque {
+ t.Fatalf("current secret type = %s, want %s", got.Type, corev1.SecretTypeOpaque)
+ }
+
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKeyFromObject(legacy),
+ &got,
+ ); !apierrors.IsNotFound(err) {
+ t.Fatalf("legacy secret err = %v, want not found", err)
+ }
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKeyFromObject(duplicate),
+ &got,
+ ); !apierrors.IsNotFound(err) {
+ t.Fatalf("duplicate secret err = %v, want not found", err)
+ }
+
+ if err := cli.Get(context.Background(), client.ObjectKeyFromObject(other), &got); err != nil {
+ t.Fatalf("get other secret: %v", err)
+ }
+}
+
+func TestServiceAccountConfigWithForceNewSecret(t *testing.T) {
+ t.Parallel()
+
+ const (
+ userName = "alice"
+ oldSecret = "sealos-token-alice-old"
+ namespace = "user-system"
+ )
+
+ sa := &corev1.ServiceAccount{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: userName,
+ Namespace: namespace,
+ },
+ Secrets: []corev1.ObjectReference{
+ {
+ Name: oldSecret,
+ },
+ },
+ }
+
+ cli := fake.NewClientBuilder().
+ WithScheme(scheme.Scheme).
+ WithObjects(sa).
+ Build()
+
+ cfg := &ServiceAccountConfig{
+ DefaultConfig: &DefaultConfig{
+ user: userName,
+ clusterName: "",
+ expirationSeconds: defaultCSRExpirationSeconds,
+ },
+ namespace: namespace,
+ sa: sa,
+ forceNewSecret: true,
+ }
+
+ if err := cfg.applyServiceAccount(context.Background(), nil, cli); err != nil {
+ t.Fatalf("apply service account: %v", err)
+ }
+ if len(cfg.sa.Secrets) != 1 {
+ t.Fatalf("secret count = %d, want 1", len(cfg.sa.Secrets))
+ }
+ if cfg.sa.Secrets[0].Name == oldSecret {
+ t.Fatalf("secret name reused old secret %q", oldSecret)
+ }
+ if cfg.secretName != cfg.sa.Secrets[0].Name {
+ t.Fatalf(
+ "cached secret name %q != current secret name %q",
+ cfg.secretName,
+ cfg.sa.Secrets[0].Name,
+ )
+ }
+}
+
+func TestApplyServiceAccountPropagatesCanceledContext(t *testing.T) {
+ t.Parallel()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel()
+ cfg := &ServiceAccountConfig{
+ DefaultConfig: &DefaultConfig{user: "alice"},
+ namespace: "user-system",
+ }
+ err := cfg.applyServiceAccount(ctx, nil, canceledContextClient{})
+ if !errors.Is(err, context.Canceled) {
+ t.Fatalf("apply service account error = %v, want context canceled", err)
+ }
+}
+
+func TestApplyBoundTokenSecretPreservesExistingType(t *testing.T) {
+ t.Parallel()
+
+ const (
+ userName = "alice"
+ namespace = "user-system"
+ secretName = "sealos-token-alice"
+ )
+ secret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: secretName,
+ Namespace: namespace,
+ UID: types.UID("secret-alice"),
+ },
+ Type: corev1.SecretTypeServiceAccountToken,
+ }
+ cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(secret).Build()
+ cfg := &ServiceAccountConfig{
+ DefaultConfig: &DefaultConfig{user: userName},
+ namespace: namespace,
+ secretName: secretName,
+ }
+
+ got, err := cfg.applyBoundTokenSecret(context.Background(), cli)
+ if err != nil {
+ t.Fatalf("apply bound token secret: %v", err)
+ }
+ if got.Type != corev1.SecretTypeServiceAccountToken {
+ t.Fatalf("secret type = %s, want %s", got.Type, corev1.SecretTypeServiceAccountToken)
+ }
+}
diff --git a/controllers/user/controllers/helper/kubeconfig/suite_test.go b/controllers/user/controllers/helper/kubeconfig/suite_test.go
new file mode 100644
index 000000000000..fb0579ef7892
--- /dev/null
+++ b/controllers/user/controllers/helper/kubeconfig/suite_test.go
@@ -0,0 +1,234 @@
+/*
+Copyright 2022 labring.
+
+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 kubeconfig
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "testing"
+ "time"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ csrv1 "k8s.io/api/certificates/v1"
+ rbacv1 "k8s.io/api/rbac/v1"
+ "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/kubernetes/scheme"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/envtest"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+)
+
+// These tests use Ginkgo (BDD-style Go testing framework). Refer to
+// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
+var (
+ cfg *rest.Config
+ k8sClient client.Client
+ testEnv *envtest.Environment
+)
+
+func TestUtils(t *testing.T) {
+ if os.Getenv("RUN_KUBECONFIG_CLUSTER_TESTS") != "true" {
+ t.Skip("set RUN_KUBECONFIG_CLUSTER_TESTS=true to run kubeconfig cluster tests")
+ }
+ RegisterFailHandler(Fail)
+
+ RunSpecs(t, "run helper suite")
+}
+
+var _ = BeforeSuite(func() {
+ logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+ By("bootstrapping test environment")
+ useExisting := true
+ if val := os.Getenv("USE_EXISTING_CLUSTER"); val != "" {
+ if parsed, err := strconv.ParseBool(val); err == nil {
+ useExisting = parsed
+ }
+ }
+ testEnv = &envtest.Environment{
+ UseExistingCluster: &useExisting,
+ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
+ ErrorIfCRDPathMissing: true,
+ }
+
+ var err error
+ // cfg is defined in this file globally.
+ cfg, err = testEnv.Start()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cfg).NotTo(BeNil())
+
+ err = csrv1.AddToScheme(scheme.Scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ //+kubebuilder:scaffold:scheme
+
+ k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient).NotTo(BeNil())
+}, 60)
+
+var _ = AfterSuite(func() {
+ By("tearing down the test environment")
+ err := testEnv.Stop()
+ Expect(err).NotTo(HaveOccurred())
+})
+
+var _ = Describe("user kubeconfig ", func() {
+ Context("syncReNewConfig test", func() {
+ BeforeEach(func() {
+ clientSet, _ := kubernetes.NewForConfig(cfg)
+ rb := &rbacv1.RoleBinding{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "cuisongliu-rolebinding",
+ },
+ Subjects: []rbacv1.Subject{
+ {
+ Kind: "user",
+ Name: "cuisongliu",
+ Namespace: "default",
+ APIGroup: "rbac.authorization.k8s.io",
+ },
+ {
+ Kind: "ServiceAccount",
+ Name: "cuisongliu",
+ Namespace: "user-system",
+ },
+ },
+ RoleRef: rbacv1.RoleRef{
+ Kind: "ClusterRole",
+ Name: "cluster-admin",
+ APIGroup: "rbac.authorization.k8s.io",
+ },
+ }
+
+ _, err := clientSet.RbacV1().
+ RoleBindings("default").
+ Create(context.TODO(), rb, metav1.CreateOptions{})
+ Expect(err).NotTo(HaveOccurred())
+ })
+ AfterEach(func() {
+ clientSet, _ := kubernetes.NewForConfig(cfg)
+ _ = clientSet.RbacV1().
+ RoleBindings("default").
+ Delete(context.TODO(), "cuisongliu-rolebinding", metav1.DeleteOptions{})
+ _ = os.RemoveAll("output")
+ })
+ It("empty csr generate", func() {
+ defaultConfig := NewConfig("cuisongliu", "", 100000000)
+ gen := defaultConfig.WithCsrConfig(
+ []string{},
+ []string{"apiserver.cluster.local"},
+ nil,
+ nil,
+ )
+ By("start to get kubeconfig")
+ config, err := gen.Apply(cfg, k8sClient)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(config).NotTo(BeNil())
+ if info, ok := config.AuthInfos["cuisongliu"]; ok {
+ if info != nil {
+ cert, err := DecodeX509CertificateBytes(info.ClientCertificateData)
+ if err != nil {
+ Expect(err).ToNot(HaveOccurred())
+ }
+ if cert.NotAfter.Before(time.Now()) {
+ config = nil
+ By(fmt.Sprintf("ClientCertificateData %s is expired", "cuisongliu"))
+ return
+ }
+ By("Cert NotAfter is " + cert.NotAfter.String())
+ }
+ }
+
+ kubeData, err := clientcmd.Write(*config)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(kubeData).NotTo(BeNil())
+ By("start to write kubeconfig")
+ err = os.WriteFile("output", kubeData, 0o600)
+ Expect(err).ToNot(HaveOccurred())
+
+ newCfg, err := clientcmd.BuildConfigFromFlags("", "output")
+ Expect(err).ToNot(HaveOccurred())
+ Expect(newCfg).NotTo(BeNil())
+ newCfg.QPS = 1e6
+ newCfg.Burst = 1e6
+ clientSet, err := kubernetes.NewForConfig(newCfg)
+ Expect(err).ToNot(HaveOccurred())
+ _, err = clientSet.CoreV1().Pods("default").List(context.TODO(), metav1.ListOptions{})
+ Expect(err).ToNot(HaveOccurred())
+ _, err = clientSet.CoreV1().
+ Pods("kube-system").
+ List(context.TODO(), metav1.ListOptions{})
+ errStatus := errors.ReasonForError(err)
+ Expect(err).To(HaveOccurred())
+ Expect(errStatus).To(Equal(metav1.StatusReasonForbidden))
+ })
+ It("token generate", func() {
+ defaultConfig := NewConfig("cuisongliu", "", 100000000)
+ gen := defaultConfig.WithServiceAccountConfig("test", nil)
+ By("start to get kubeconfig")
+ config, err := gen.Apply(cfg, k8sClient)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(config).NotTo(BeNil())
+ kubeData, err := clientcmd.Write(*config)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(kubeData).NotTo(BeNil())
+ By("start to write kubeconfig")
+ err = os.WriteFile("output", kubeData, 0o600)
+ Expect(err).ToNot(HaveOccurred())
+
+ newCfg, err := clientcmd.BuildConfigFromFlags("", "output")
+ Expect(err).ToNot(HaveOccurred())
+ Expect(newCfg).NotTo(BeNil())
+ newCfg.QPS = 1e6
+ newCfg.Burst = 1e6
+ clientSet, err := kubernetes.NewForConfig(newCfg)
+ Expect(err).ToNot(HaveOccurred())
+ _, err = clientSet.CoreV1().Pods("default").List(context.TODO(), metav1.ListOptions{})
+ Expect(err).ToNot(HaveOccurred())
+ _, err = clientSet.CoreV1().
+ Pods("kube-system").
+ List(context.TODO(), metav1.ListOptions{})
+ errStatus := errors.ReasonForError(err)
+ Expect(err).To(HaveOccurred())
+ Expect(errStatus).To(Equal(metav1.StatusReasonForbidden))
+ })
+ It("webhook generate", func() {
+ defaultConfig := NewConfig("cuisongliu", "", 100000000)
+ gen := defaultConfig.WithWebhookConfigConfig("https://192.168.64.1:6443")
+ By("start to get kubeconfig")
+ config, err := gen.Apply(cfg, k8sClient)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(config).NotTo(BeNil())
+ kubeData, err := clientcmd.Write(*config)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(kubeData).NotTo(BeNil())
+ By("start to write kubeconfig")
+ err = os.WriteFile("output-webhook", kubeData, 0o600)
+ Expect(err).ToNot(HaveOccurred())
+ })
+ })
+})
diff --git a/controllers/user/controllers/helper/kubeconfig/tools.go b/controllers/user/controllers/helper/kubeconfig/tools.go
new file mode 100644
index 000000000000..6cc75a9f8041
--- /dev/null
+++ b/controllers/user/controllers/helper/kubeconfig/tools.go
@@ -0,0 +1,62 @@
+/*
+Copyright 2023 cuisongliu@qq.com.
+
+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 kubeconfig
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "fmt"
+)
+
+// DecodeX509CertificateChainBytes will decode a PEM encoded x509 Certificate chain.
+func DecodeX509CertificateChainBytes(certBytes []byte) ([]*x509.Certificate, error) {
+ certs := make([]*x509.Certificate, 0, 1)
+
+ var block *pem.Block
+
+ for {
+ // decode the tls certificate pem
+ block, certBytes = pem.Decode(certBytes)
+ if block == nil {
+ break
+ }
+
+ // parse the tls certificate
+ cert, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing TLS certificate: %s", err.Error())
+ }
+ certs = append(certs, cert)
+ }
+
+ if len(certs) == 0 {
+ return nil, errors.New("error decoding certificate PEM block")
+ }
+
+ return certs, nil
+}
+
+// DecodeX509CertificateBytes will decode a PEM encoded x509 Certificate.
+func DecodeX509CertificateBytes(certBytes []byte) (*x509.Certificate, error) {
+ certs, err := DecodeX509CertificateChainBytes(certBytes)
+ if err != nil {
+ return nil, err
+ }
+
+ return certs[0], nil
+}
diff --git a/controllers/user/controllers/helper/kubeconfig/webhook.go b/controllers/user/controllers/helper/kubeconfig/webhook.go
new file mode 100644
index 000000000000..feeb31f511f8
--- /dev/null
+++ b/controllers/user/controllers/helper/kubeconfig/webhook.go
@@ -0,0 +1,48 @@
+/*
+Copyright 2022 cuisongliu@qq.com.
+
+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 kubeconfig
+
+import (
+ "fmt"
+
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd/api"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+func (c *WebhookConfig) Apply(_ *rest.Config, _ client.Client) (*api.Config, error) {
+ // make sure cadata is loaded into config under incluster mode
+ ctx := fmt.Sprintf("%s@%s", c.user, c.clusterName)
+ return &api.Config{
+ Clusters: map[string]*api.Cluster{
+ c.clusterName: {
+ Server: c.webhookURL,
+ InsecureSkipTLSVerify: true,
+ },
+ },
+ Contexts: map[string]*api.Context{
+ ctx: {
+ Cluster: c.clusterName,
+ AuthInfo: "webhook-user",
+ },
+ },
+ AuthInfos: map[string]*api.AuthInfo{
+ "webhook-user": {},
+ },
+ CurrentContext: ctx,
+ }, nil
+}
diff --git a/controllers/user/controllers/helper/kubeconfig/webhook.md b/controllers/user/controllers/helper/kubeconfig/webhook.md
new file mode 100644
index 000000000000..40225f5f4a2a
--- /dev/null
+++ b/controllers/user/controllers/helper/kubeconfig/webhook.md
@@ -0,0 +1,228 @@
+## How to using webhook generate to webhook_config
+set remote http webhook url
+```go
+
+gen := NewGenerate(&Config{
+ Webhook: true,
+ WebhookURL: "http://192.168.64.1:8080/auth",
+})
+config, err := gen.KubeConfig(cfg, k8sClient)
+kubeData, err := clientcmd.Write(*config)
+err = os.WriteFile("output-webhook", kubeData, 0600)
+```
+
+Note that the address of this webhookURL should be specific to the path `/path`
+
+## Add webhook config to apiserver node
+update apiserver config `/etc/kubernetes/manifests/kube-apiserver.yaml`
+1. authorization-mode add Webhook mode
+2. authorization-webhook-config-file set webhook config
+3. add volume for apiserer config mount webhook config
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ annotations:
+ kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 192.168.64.31:6443
+ creationTimestamp: null
+ labels:
+ component: kube-apiserver
+ tier: control-plane
+ name: kube-apiserver
+ namespace: kube-system
+spec:
+ containers:
+ - command:
+ - kube-apiserver
+ - --advertise-address=192.168.64.31
+ - --allow-privileged=true
+ - --audit-log-format=json
+ - --audit-log-maxage=7
+ - --audit-log-maxbackup=10
+ - --audit-log-maxsize=200
+ - --audit-log-path=/var/log/kubernetes/audit.log
+ - --audit-policy-file=/etc/kubernetes/audit-policy.yml
+ - --authorization-mode=Node,RBAC,Webhook
+ - --authorization-webhook-config-file=/root/.kube_webhook/config
+ - --client-ca-file=/etc/kubernetes/pki/ca.crt
+ - --enable-admission-plugins=NodeRestriction
+ - --enable-aggregator-routing=true
+ - --enable-bootstrap-token-auth=true
+ - --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
+ - --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
+ - --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
+ - --etcd-servers=https://192.168.64.31:2379
+ - --feature-gates=TTLAfterFinished=true,EphemeralContainers=true
+ - --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
+ - --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
+ - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
+ - --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
+ - --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
+ - --requestheader-allowed-names=front-proxy-client
+ - --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
+ - --requestheader-extra-headers-prefix=X-Remote-Extra-
+ - --requestheader-group-headers=X-Remote-Group
+ - --requestheader-username-headers=X-Remote-User
+ - --secure-port=6443
+ - --service-account-issuer=https://kubernetes.default.svc.cluster.local
+ - --service-account-key-file=/etc/kubernetes/pki/sa.pub
+ - --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
+ - --service-cluster-ip-range=10.96.0.0/22
+ - --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
+ - --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
+ image: k8s.gcr.io/kube-apiserver:v1.23.10
+ imagePullPolicy: IfNotPresent
+ livenessProbe:
+ failureThreshold: 8
+ httpGet:
+ host: 192.168.64.31
+ path: /livez
+ port: 6443
+ scheme: HTTPS
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ timeoutSeconds: 15
+ name: kube-apiserver
+ readinessProbe:
+ failureThreshold: 3
+ httpGet:
+ host: 192.168.64.31
+ path: /readyz
+ port: 6443
+ scheme: HTTPS
+ periodSeconds: 1
+ timeoutSeconds: 15
+ resources:
+ requests:
+ cpu: 250m
+ startupProbe:
+ failureThreshold: 24
+ httpGet:
+ host: 192.168.64.31
+ path: /livez
+ port: 6443
+ scheme: HTTPS
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ timeoutSeconds: 15
+ volumeMounts:
+ - mountPath: /etc/kubernetes
+ name: audit
+ - mountPath: /var/log/kubernetes
+ name: audit-log
+ - mountPath: /etc/ssl/certs
+ name: ca-certs
+ readOnly: true
+ - mountPath: /etc/ca-certificates
+ name: etc-ca-certificates
+ readOnly: true
+ - mountPath: /etc/pki
+ name: etc-pki
+ readOnly: true
+ - mountPath: /etc/kubernetes/pki
+ name: k8s-certs
+ readOnly: true
+ - mountPath: /etc/localtime
+ name: localtime
+ readOnly: true
+ - mountPath: /usr/local/share/ca-certificates
+ name: usr-local-share-ca-certificates
+ readOnly: true
+ - mountPath: /usr/share/ca-certificates
+ name: usr-share-ca-certificates
+ readOnly: true
+ - mountPath: /root/.kube_webhook
+ name: webhook
+ readOnly: true
+ hostNetwork: true
+ priorityClassName: system-node-critical
+ securityContext:
+ seccompProfile:
+ type: RuntimeDefault
+ volumes:
+ - hostPath:
+ path: /etc/kubernetes
+ type: DirectoryOrCreate
+ name: audit
+ - hostPath:
+ path: /var/log/kubernetes
+ type: DirectoryOrCreate
+ name: audit-log
+ - hostPath:
+ path: /etc/ssl/certs
+ type: DirectoryOrCreate
+ name: ca-certs
+ - hostPath:
+ path: /etc/ca-certificates
+ type: DirectoryOrCreate
+ name: etc-ca-certificates
+ - hostPath:
+ path: /etc/pki
+ type: DirectoryOrCreate
+ name: etc-pki
+ - hostPath:
+ path: /etc/kubernetes/pki
+ type: DirectoryOrCreate
+ name: k8s-certs
+ - hostPath:
+ path: /etc/localtime
+ type: File
+ name: localtime
+ - hostPath:
+ path: /usr/local/share/ca-certificates
+ type: DirectoryOrCreate
+ name: usr-local-share-ca-certificates
+ - hostPath:
+ path: /usr/share/ca-certificates
+ type: DirectoryOrCreate
+ name: usr-share-ca-certificates
+ - hostPath:
+ path: /root/.kube_webhook
+ type: DirectoryOrCreate
+ name: webhook
+status: {}
+```
+
+## coding http webhook server
+```go
+func HttpServer(port uint16) error {
+ r := gin.New()
+ gin.SetMode(gin.ReleaseMode)
+ r.Use(middlewares.GinLogger(), middlewares.GinRecovery(true))
+
+ r.POST("/auth", Auth)
+ gin.SetMode(gin.DebugMode)
+ //prefix := "/Users/cuisongliu/Workspaces/go/src/github.com/cuisongliu/rbac-blacklist/pki"
+ return r.Run(fmt.Sprintf("%s:%d", "0.0.0.0", port))
+}
+
+var serverLogger = ctrl.Log.WithName("serverLogger")
+
+func Auth(ctx *gin.Context) {
+ fmt.Println("auth in ...")
+ var review v1.SubjectAccessReview
+ err := ctx.BindJSON(&review)
+ //{"kind":"SubjectAccessReview","apiVersion":"authorization.k8s.io/v1beta1","metadata":{"creationTimestamp":null},"spec":{"resourceAttributes":{"namespace":"ns-cuisongliu","verb":"list","version":"v1","resource":"pods"},"user":"system:serviceaccount:default:cuisongliu","group":["system:serviceaccounts","system:serviceaccounts:default","system:authenticated"],"uid":"01575033-6a76-4426-88d0-3da94c6c3e03"},"status":{"allowed":false}}
+ //d, err := ctx.GetRawData()
+ fmt.Println("inff", "msg", string(review.Spec.User))
+ if err != nil {
+ fmt.Println(err, "has error")
+ }
+
+ //allow := true
+ //if review.Spec.ResourceAttributes.Resource == "configmaps" && review.Spec.ResourceAttributes.Name == "my-config" {
+ // allow = false
+ //}
+ r := &v1.SubjectAccessReview{
+ TypeMeta: v12.TypeMeta{
+ Kind: "SubjectAccessReview",
+ APIVersion: "authorization.k8s.io/v1beta1",
+ },
+ Status: v1.SubjectAccessReviewStatus{Allowed: review.Status.Allowed, Denied: false},
+ }
+ ctx.JSONP(200, r)
+}
+
+```
+
+The data obtained by the review structure is the data after the previous authentication logic (RBAC, ABAC)
diff --git a/controllers/user/controllers/helper/ratelimiter/ratelimiter.go b/controllers/user/controllers/helper/ratelimiter/ratelimiter.go
new file mode 100644
index 000000000000..6a0394fd8076
--- /dev/null
+++ b/controllers/user/controllers/helper/ratelimiter/ratelimiter.go
@@ -0,0 +1,95 @@
+// Copyright © 2023 sealos.
+//
+// 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 ratelimiter
+
+import (
+ "flag"
+ "time"
+
+ "golang.org/x/time/rate"
+ "k8s.io/client-go/util/workqueue"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+const (
+ defaultMinRetryDelay = 5 * time.Millisecond
+ defaultMaxRetryDelay = 1000 * time.Second
+ defaultQPS = float64(10.0)
+ defaultBurst = 100
+ defaultConcurrent = 5
+ flagMinRetryDelay = "min-retry-delay"
+ flagMaxRetryDelay = "max-retry-delay"
+ flagQPS = "default-qps"
+ flagBurst = "default-burst"
+ flagConcurrent = "default-concurrent"
+)
+
+// RateLimiterOptions used on reconcilers.
+type RateLimiterOptions struct {
+ minRetryDelay time.Duration
+ maxRetryDelay time.Duration
+ qps float64
+ burst int
+ concurrent int
+}
+
+func (o *RateLimiterOptions) BindFlags(fs *flag.FlagSet) {
+ fs.DurationVar(
+ &o.minRetryDelay,
+ flagMinRetryDelay,
+ defaultMinRetryDelay,
+ "Specifies the minimum delay time before retrying the reconciliation of an object. This delay provides a buffer to prevent rapid-fire retries.",
+ )
+ fs.DurationVar(
+ &o.maxRetryDelay,
+ flagMaxRetryDelay,
+ defaultMaxRetryDelay,
+ "Specifies the maximum delay time before retrying the reconciliation of an object. This cap ensures that retry delays don't grow excessively long.",
+ )
+ fs.Float64Var(
+ &o.qps,
+ flagQPS,
+ defaultQPS,
+ "Sets the maximum allowed quantity of process units (batches) that can be processed per second. This limit helps maintain a controlled processing rate.",
+ )
+ fs.IntVar(
+ &o.burst,
+ flagBurst,
+ defaultBurst,
+ "Sets the maximum quantity of process units (batches) that can be processed in a burst. This limit helps control the processing rate during short periods of high activity.",
+ )
+ flag.IntVar(
+ &o.concurrent,
+ flagConcurrent,
+ defaultConcurrent,
+ "The number of concurrent reconciles.",
+ )
+}
+
+func GetRateLimiter(opts RateLimiterOptions) workqueue.TypedRateLimiter[reconcile.Request] {
+ return workqueue.NewTypedMaxOfRateLimiter[reconcile.Request](
+ workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](
+ opts.minRetryDelay,
+ opts.maxRetryDelay,
+ ),
+ &workqueue.TypedBucketRateLimiter[reconcile.Request]{
+ Limiter: rate.NewLimiter(rate.Limit(opts.qps), opts.burst),
+ },
+ )
+}
+
+func GetConcurrent(opts RateLimiterOptions) int {
+ return opts.concurrent
+}
diff --git a/controllers/user/controllers/helper/string_any.go b/controllers/user/controllers/helper/string_any.go
new file mode 100644
index 000000000000..94ef8a11fc3c
--- /dev/null
+++ b/controllers/user/controllers/helper/string_any.go
@@ -0,0 +1,210 @@
+/*
+Copyright The Kubernetes Authors.
+
+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.
+*/
+
+// Code generated by set-gen. DO NOT EDIT.
+
+package helper
+
+import (
+ "sort"
+)
+
+// sets.String is a set of strings, implemented via map[string]struct{} for minimal memory consumption.
+type Any map[string]any
+
+// NewAny creates a Any from a list of values.
+func NewAny(items map[string]any) Any {
+ ss := Any{}
+ ss.Insert(items)
+ return ss
+}
+
+// Insert adds items to the set.
+func (s Any) Insert(items map[string]any) Any {
+ if items == nil {
+ s = make(map[string]any, 0)
+ return s
+ }
+ for k, item := range items {
+ s[k] = item
+ }
+ return s
+}
+
+func (s Any) InsertValue(key string, value any) Any {
+ s[key] = value
+ return s
+}
+
+// Delete removes all items from the set.
+func (s Any) Delete(items ...string) Any {
+ for _, item := range items {
+ delete(s, item)
+ }
+ return s
+}
+
+// Has returns true if and only if item is contained in the set.
+func (s Any) Has(item string) bool {
+ _, contained := s[item]
+ return contained
+}
+
+// HasAll returns true if and only if all items are contained in the set.
+func (s Any) HasAll(items ...string) bool {
+ for _, item := range items {
+ if !s.Has(item) {
+ return false
+ }
+ }
+ return true
+}
+
+// HasAny returns true if any items are contained in the set.
+func (s Any) HasAny(items ...string) bool {
+ for _, item := range items {
+ if s.Has(item) {
+ return true
+ }
+ }
+ return false
+}
+
+// Difference returns a set of objects that are not in s2
+// For example:
+// s1 = {a1, a2, a3}
+// s2 = {a1, a2, a4, a5}
+// s1.Difference(s2) = {a3}
+// s2.Difference(s1) = {a4, a5}
+func (s Any) Difference(s2 Any) Any {
+ result := NewAny(nil)
+ for key, v := range s {
+ if !s2.Has(key) {
+ result.InsertValue(key, v)
+ }
+ }
+ return result
+}
+
+// Union returns a new set which includes items in either s1 or s2.
+// For example:
+// s1 = {a1, a2}
+// s2 = {a3, a4}
+// s1.Union(s2) = {a1, a2, a3, a4}
+// s2.Union(s1) = {a1, a2, a3, a4}
+func (s1 Any) Union(s2 Any) Any {
+ result := NewAny(nil)
+ for key, v := range s1 {
+ result.InsertValue(key, v)
+ }
+ for key, v := range s2 {
+ result.InsertValue(key, v)
+ }
+ return result
+}
+
+// Intersection returns a new set which includes the item in BOTH s1 and s2
+// For example:
+// s1 = {a1, a2}
+// s2 = {a2, a3}
+// s1.Intersection(s2) = {a2}
+func (s1 Any) Intersection(s2 Any) Any {
+ var walk, other Any
+ result := NewAny(nil)
+ if s1.Len() < s2.Len() {
+ walk = s1
+ other = s2
+ } else {
+ walk = s2
+ other = s1
+ }
+ for key, v := range walk {
+ if other.Has(key) {
+ result.InsertValue(key, v)
+ }
+ }
+ return result
+}
+
+// IsSuperset returns true if and only if s1 is a superset of s2.
+func (s1 Any) IsSuperset(s2 Any) bool {
+ for item := range s2 {
+ if !s1.Has(item) {
+ return false
+ }
+ }
+ return true
+}
+
+// Equal returns true if and only if s1 is equal (as a set) to s2.
+// Two sets are equal if their membership is identical.
+// (In practice, this means same elements, order doesn't matter)
+func (s1 Any) Equal(s2 Any) bool {
+ return len(s1) == len(s2) && s1.IsSuperset(s2)
+}
+
+type sortableSliceOfString []string
+
+func (s sortableSliceOfString) Len() int { return len(s) }
+func (s sortableSliceOfString) Less(i, j int) bool { return lessString(s[i], s[j]) }
+func (s sortableSliceOfString) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
+// List returns the contents as a sorted string slice.
+func (s Any) ListKey() []string {
+ res := make(sortableSliceOfString, 0, len(s))
+ for key := range s {
+ res = append(res, key)
+ }
+ sort.Sort(res)
+ return []string(res)
+}
+
+// List returns the contents as a sorted string slice.
+func (s Any) ListValue() []any {
+ res := make([]any, 0, len(s))
+ for _, v := range s {
+ res = append(res, v)
+ }
+ return []any(res)
+}
+
+// UnsortedList returns the slice with contents in random order.
+func (s Any) UnsortedList() []string {
+ res := make([]string, 0, len(s))
+ for key := range s {
+ res = append(res, key)
+ }
+ return res
+}
+
+// Returns a single element from the set.
+func (s Any) PopAny() (string, bool) {
+ for key := range s {
+ s.Delete(key)
+ return key, true
+ }
+ var zeroValue string
+ return zeroValue, false
+}
+
+// Len returns the size of the set.
+func (s Any) Len() int {
+ return len(s)
+}
+
+func lessString(lhs, rhs string) bool {
+ return lhs < rhs
+}
diff --git a/controllers/user/controllers/license_watcher.go b/controllers/user/controllers/license_watcher.go
new file mode 100644
index 000000000000..8945904e1e95
--- /dev/null
+++ b/controllers/user/controllers/license_watcher.go
@@ -0,0 +1,59 @@
+// Copyright © 2026 sealos.
+//
+// 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 controllers
+
+import (
+ "context"
+
+ licensev1 "github.com/labring/sealos/controllers/license/api/v1"
+ "github.com/labring/sealos/controllers/user/pkg/licensegate"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/cache"
+ ctrl "sigs.k8s.io/controller-runtime"
+)
+
+func SetupLicenseGate(mgr ctrl.Manager) error {
+ logger := ctrl.Log.WithName("license-gate")
+ reader := mgr.GetAPIReader()
+ if err := licensegate.Refresh(context.Background(), reader); err != nil {
+ logger.Error(err, "initial license gate refresh failed")
+ }
+ licenseMetadata := &metav1.PartialObjectMetadata{}
+ licenseMetadata.SetGroupVersionKind(licensev1.GroupVersion.WithKind("License"))
+ informer, err := mgr.GetCache().GetInformer(context.Background(), licenseMetadata)
+ if err != nil {
+ return err
+ }
+ if _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+ AddFunc: func(obj any) {
+ if err := licensegate.Refresh(context.Background(), reader); err != nil {
+ logger.Error(err, "license gate refresh failed on add")
+ }
+ },
+ UpdateFunc: func(oldObj, newObj any) {
+ if err := licensegate.Refresh(context.Background(), reader); err != nil {
+ logger.Error(err, "license gate refresh failed on update")
+ }
+ },
+ DeleteFunc: func(obj any) {
+ if err := licensegate.Refresh(context.Background(), reader); err != nil {
+ logger.Error(err, "license gate refresh failed on delete")
+ }
+ },
+ }); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/controllers/user/controllers/migrate/migrate.go b/controllers/user/controllers/migrate/migrate.go
new file mode 100644
index 000000000000..07f9abe42d88
--- /dev/null
+++ b/controllers/user/controllers/migrate/migrate.go
@@ -0,0 +1,59 @@
+/*
+Copyright 2023 cuisongliu@qq.com.
+
+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 migrate
+
+import (
+ "context"
+
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/util/retry"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+var (
+ UGBindingFinalizer = "sealos.io/user.group.binding.finalizers"
+ UGFinalizer = "sealos.io/user.group.finalizers"
+)
+
+func RemoveFinalizer(
+ ctx context.Context,
+ cli client.Client,
+ obj client.Object,
+ oldFinalizer string,
+) {
+ oldFinalizerName := oldFinalizer
+ if controllerutil.ContainsFinalizer(obj, oldFinalizerName) {
+ controllerutil.RemoveFinalizer(obj, oldFinalizerName)
+ }
+ _ = retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ return cli.Update(ctx, obj)
+ })
+}
+
+func SetOwner(ctx context.Context, cli client.Client, obj, owner client.Object) {
+ _ = retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ if owner == nil {
+ obj.SetOwnerReferences([]v1.OwnerReference{})
+ return cli.Update(ctx, obj)
+ }
+ _, err := controllerutil.CreateOrUpdate(ctx, cli, obj, func() error {
+ return controllerutil.SetControllerReference(owner, obj, cli.Scheme())
+ })
+ return err
+ })
+}
diff --git a/controllers/user/controllers/operationrequest_controller.go b/controllers/user/controllers/operationrequest_controller.go
new file mode 100644
index 000000000000..0b683eb1b450
--- /dev/null
+++ b/controllers/user/controllers/operationrequest_controller.go
@@ -0,0 +1,483 @@
+/*
+Copyright 2022 labring.
+
+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 controllers
+
+import (
+ "context"
+ "fmt"
+ "maps"
+ "sync"
+ "time"
+
+ "github.com/go-logr/logr"
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ "github.com/labring/sealos/controllers/user/controllers/helper/ratelimiter"
+ v1 "k8s.io/api/core/v1"
+ rbacv1 "k8s.io/api/rbac/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/tools/record"
+ "k8s.io/utils/keymutex"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ controller "sigs.k8s.io/controller-runtime/pkg/controller"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+)
+
+// OperationReqRequeueDuration is the time interval to reconcile a OperationRequest if no error occurs
+const OperationReqRequeueDuration time.Duration = 30 * time.Second
+
+// OperationReqReconciler reconciles a Operationrequest object
+type OperationReqReconciler struct {
+ client.Client
+
+ Logger logr.Logger
+ Scheme *runtime.Scheme
+ Recorder record.EventRecorder
+ locks keymutex.KeyMutex
+ lockOnce sync.Once
+
+ // expirationTime is the time duration of the request is expired
+ expirationTime time.Duration
+ // retentionTime is the time duration of the request is retained after it is isCompleted
+ retentionTime time.Duration
+}
+
+func (r *OperationReqReconciler) lockWorkspace(namespace string) func() {
+ r.lockOnce.Do(func() {
+ // A fixed lock set bounds memory while preserving concurrency across
+ // independent workspaces.
+ r.locks = keymutex.NewHashed(256)
+ })
+ r.locks.LockKey(namespace)
+ return func() {
+ _ = r.locks.UnlockKey(namespace)
+ }
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *OperationReqReconciler) SetupWithManager(
+ mgr ctrl.Manager,
+ opts ratelimiter.RateLimiterOptions,
+ expTime, retTime time.Duration,
+) error {
+ const controllerName = "operationrequest_controller"
+ if r.Client == nil {
+ r.Client = mgr.GetClient()
+ }
+ r.Logger = ctrl.Log.WithName(controllerName)
+ if r.Recorder == nil {
+ r.Recorder = mgr.GetEventRecorderFor(controllerName)
+ }
+ r.Scheme = mgr.GetScheme()
+ r.expirationTime = expTime
+ r.retentionTime = retTime
+ r.Logger.V(1).Info("init reconcile operationrequest controller")
+ return ctrl.NewControllerManagedBy(mgr).
+ For(
+ &userv1.Operationrequest{},
+ builder.WithPredicates(namespaceOnlyPredicate(config.GetUserSystemNamespace())),
+ builder.OnlyMetadata,
+ ).
+ WithOptions(controller.Options{
+ MaxConcurrentReconciles: ratelimiter.GetConcurrent(opts),
+ RateLimiter: ratelimiter.GetRateLimiter(opts),
+ }).
+ Complete(r)
+}
+
+func namespaceOnlyPredicate(namespace string) predicate.Predicate {
+ return predicate.Funcs{
+ CreateFunc: func(e event.CreateEvent) bool {
+ return e.Object.GetNamespace() == namespace
+ },
+ DeleteFunc: func(e event.DeleteEvent) bool {
+ return e.Object.GetNamespace() == namespace
+ },
+ UpdateFunc: func(e event.UpdateEvent) bool {
+ return e.ObjectNew.GetNamespace() == namespace
+ },
+ GenericFunc: func(e event.GenericEvent) bool {
+ return e.Object.GetNamespace() == namespace
+ },
+ }
+}
+
+// +kubebuilder:rbac:groups=user.sealos.io,resources=operationrequests,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=user.sealos.io,resources=operationrequests/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=user.sealos.io,resources=operationrequests/finalizers,verbs=update
+
+func (r *OperationReqReconciler) Reconcile(
+ ctx context.Context,
+ req ctrl.Request,
+) (ctrl.Result, error) {
+ operationRequest := &userv1.Operationrequest{}
+ if err := r.Get(ctx, req.NamespacedName, operationRequest); err != nil {
+ return ctrl.Result{}, client.IgnoreNotFound(err)
+ }
+ return r.reconcile(ctx, operationRequest)
+}
+
+func (r *OperationReqReconciler) reconcile(
+ ctx context.Context,
+ request *userv1.Operationrequest,
+) (ctrl.Result, error) {
+ unlock := r.lockWorkspace(request.Spec.Namespace)
+ defer unlock()
+ r.Logger.V(1).Info("start reconcile controller operationRequest", getLog(request)...)
+ // count the time cost of handling the request
+ startTime := time.Now()
+ defer func() {
+ r.Logger.V(1).
+ Info("complete request handling", getLog(request, "create time", request.CreationTimestamp, "handling cost time", time.Since(startTime))...)
+ }()
+
+ // delete OperationRequest first if its status is isCompleted and exist for retention time
+ if r.isRetained(request) {
+ r.Logger.V(1).Info("delete request", getLog(request)...)
+ if err := r.deleteRequest(ctx, request); err != nil {
+ return ctrl.Result{}, err
+ }
+ return ctrl.Result{}, nil
+ }
+ // return early if its status is isCompleted and didn't exist for retention time
+ if r.isCompleted(request) {
+ r.Logger.V(1).Info("request is completed and requeue", getLog(request)...)
+ return ctrl.Result{RequeueAfter: OperationReqRequeueDuration}, nil
+ }
+ // change OperationRequest status to failed if it is expired
+ if r.isExpired(request) {
+ r.Logger.V(1).Info("request is expired, update status to failed", getLog(request)...)
+ if err := r.updateRequestStatus(ctx, request, userv1.RequestFailed); err != nil {
+ return ctrl.Result{}, err
+ }
+ return ctrl.Result{}, nil
+ }
+
+ // update OperationRequest status to processing
+ err := r.updateRequestStatus(ctx, request, userv1.RequestProcessing)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ // convert OperationRequest to RoleBinding
+ desiredRoleBinding := convertRequestToRoleBinding(request)
+ rolebinding := &rbacv1.RoleBinding{ObjectMeta: metav1.ObjectMeta{
+ Name: desiredRoleBinding.Name,
+ Namespace: desiredRoleBinding.Namespace,
+ }}
+ r.Logger.V(1).Info("convert OperationRequest to RoleBinding",
+ "rolebinding.name", desiredRoleBinding.Name,
+ "rolebinding.namespace", desiredRoleBinding.Namespace,
+ "rolebinding.subjects", desiredRoleBinding.Subjects,
+ "rolebinding.roleRef", desiredRoleBinding.RoleRef,
+ )
+
+ user := &userv1.User{}
+ if err := r.Get(
+ ctx,
+ client.ObjectKey{Name: config.GetUserNameByNamespace(request.Spec.Namespace)},
+ user,
+ ); err != nil {
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeWarning,
+ "Failed to get user",
+ "Failed to get user %s",
+ request.Spec.User,
+ )
+ return ctrl.Result{}, err
+ }
+ bindUser := &userv1.User{}
+ if err := r.Get(ctx, client.ObjectKey{Name: request.Spec.User}, bindUser); err != nil {
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeWarning,
+ "Failed to get bind user",
+ "Failed to get bind user %s",
+ request.Spec.User,
+ )
+ return ctrl.Result{}, err
+ }
+ configureRoleBinding := func() error {
+ rolebinding.Annotations = maps.Clone(desiredRoleBinding.Annotations)
+ rolebinding.Labels = maps.Clone(desiredRoleBinding.Labels)
+ rolebinding.Subjects = append([]rbacv1.Subject(nil), desiredRoleBinding.Subjects...)
+ rolebinding.RoleRef = desiredRoleBinding.RoleRef
+ return ctrl.SetControllerReference(bindUser, rolebinding, r.Scheme)
+ }
+
+ // handle OperationRequest, create or delete rolebinding
+ switch request.Spec.Action {
+ case userv1.Grant:
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeNormal,
+ "Grant",
+ "Grant role %s to user %s",
+ request.Spec.Role,
+ request.Spec.User,
+ )
+ if _, err := ctrl.CreateOrUpdate(
+ ctx,
+ r.Client,
+ rolebinding,
+ configureRoleBinding,
+ ); err != nil {
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeWarning,
+ "Failed to create/update rolebinding",
+ "Failed to create rolebinding %s/%s",
+ rolebinding.Namespace,
+ rolebinding.Name,
+ )
+ return ctrl.Result{}, err
+ }
+ if request.Spec.Role == userv1.OwnerRoleType {
+ // update user annotation
+ if err := r.patchUserOwner(ctx, user, request.Spec.User); err != nil {
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeWarning,
+ "Failed to update user",
+ "Failed to update user %s",
+ request.Spec.User,
+ )
+ return ctrl.Result{}, err
+ }
+ }
+ case userv1.Deprive:
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeNormal,
+ "Deprive",
+ "Deprive role %s from user %s",
+ request.Spec.Role,
+ request.Spec.User,
+ )
+ if err := r.Delete(ctx, rolebinding); client.IgnoreNotFound(err) != nil {
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeWarning,
+ "Failed to delete rolebinding",
+ "Failed to delete rolebinding %s/%s",
+ rolebinding.Namespace,
+ rolebinding.Name,
+ )
+ return ctrl.Result{}, err
+ }
+ case userv1.Update:
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeNormal,
+ "Update",
+ "Update role %s to user %s",
+ request.Spec.Role,
+ request.Spec.User,
+ )
+ if err := r.Delete(ctx, rolebinding); client.IgnoreNotFound(err) != nil {
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeWarning,
+ "Failed to delete rolebinding",
+ "Failed to delete rolebinding %s/%s",
+ rolebinding.Namespace,
+ rolebinding.Name,
+ )
+ return ctrl.Result{}, err
+ }
+ if err = configureRoleBinding(); err != nil {
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeWarning,
+ "Failed to set owner reference",
+ "Failed to set owner reference for rolebinding %s/%s",
+ rolebinding.Namespace,
+ rolebinding.Name,
+ )
+ return ctrl.Result{}, err
+ }
+ if err := r.Create(ctx, rolebinding); err != nil {
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeWarning,
+ "Failed to create rolebinding",
+ "Failed to create rolebinding %s/%s",
+ rolebinding.Namespace,
+ rolebinding.Name,
+ )
+ return ctrl.Result{}, err
+ }
+ if request.Spec.Role == userv1.OwnerRoleType {
+ // update user annotation
+ if err := r.patchUserOwner(ctx, user, request.Spec.User); err != nil {
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeWarning,
+ "Failed to update user",
+ "Failed to update user %s",
+ request.Spec.User,
+ )
+ return ctrl.Result{}, err
+ }
+ }
+ default:
+ return ctrl.Result{}, fmt.Errorf("invalid action %s", request.Spec.Action)
+ }
+
+ // update OperationRequest status to completed
+ err = r.updateRequestStatus(ctx, request, userv1.RequestCompleted)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeNormal,
+ "Completed",
+ "Completed operation request %s/%s",
+ request.Spec.Namespace,
+ request.Name,
+ )
+ return ctrl.Result{RequeueAfter: OperationReqRequeueDuration}, nil
+}
+
+func (r *OperationReqReconciler) patchUserOwner(
+ ctx context.Context,
+ user *userv1.User,
+ owner string,
+) error {
+ original := user.DeepCopy()
+ if user.Annotations == nil {
+ user.Annotations = make(map[string]string)
+ }
+ user.Annotations[userv1.UserAnnotationOwnerKey] = owner
+ return r.Patch(ctx, user, client.MergeFrom(original))
+}
+
+// isRetained returns true if the request is isCompleted and exist for retention time
+func (r *OperationReqReconciler) isRetained(request *userv1.Operationrequest) bool {
+ if request.Status.Phase == userv1.RequestCompleted &&
+ request.CreationTimestamp.Add(r.retentionTime).Before(time.Now()) {
+ return true
+ }
+ return false
+}
+
+// isCompleted returns true if the request is isCompleted
+func (r *OperationReqReconciler) isCompleted(request *userv1.Operationrequest) bool {
+ return request.Status.Phase == userv1.RequestCompleted
+}
+
+// isExpired returns true if the request is expired
+func (r *OperationReqReconciler) isExpired(request *userv1.Operationrequest) bool {
+ if request.Status.Phase != userv1.RequestCompleted &&
+ request.CreationTimestamp.Add(r.expirationTime).Before(time.Now()) {
+ return true
+ }
+ return false
+}
+
+func (r *OperationReqReconciler) deleteRequest(
+ ctx context.Context,
+ request *userv1.Operationrequest,
+) error {
+ r.Logger.V(1).Info("deleting OperationRequest", "request", request)
+ if err := r.Delete(ctx, request); client.IgnoreNotFound(err) != nil {
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeWarning,
+ "Failed to delete OperationRequest",
+ "Failed to delete OperationRequest %s/%s",
+ request.Spec.Namespace,
+ request.Name,
+ )
+ r.Logger.Error(err, "Failed to delete OperationRequest", getLog(request)...)
+ return fmt.Errorf(
+ "failed to delete OperationRequest %s/%s: %w",
+ request.Spec.Namespace,
+ request.Name,
+ err,
+ )
+ }
+ r.Logger.V(1).Info("delete OperationRequest success", getLog(request)...)
+ return nil
+}
+
+func (r *OperationReqReconciler) updateRequestStatus(
+ ctx context.Context,
+ request *userv1.Operationrequest,
+ phase userv1.RequestPhase,
+) error {
+ request.Status.Phase = phase
+ if err := r.Status().Update(ctx, request); err != nil {
+ r.Recorder.Eventf(
+ request,
+ v1.EventTypeWarning,
+ "Failed to update OperationRequest status",
+ "Failed to update OperationRequest status %s/%s",
+ request.Spec.Namespace,
+ request.Name,
+ )
+ r.Logger.V(1).Info("update OperationRequest status failed", getLog(request)...)
+ return err
+ }
+ r.Logger.V(1).Info("update OperationRequest status success", getLog(request)...)
+ return nil
+}
+
+func convertRequestToRoleBinding(request *userv1.Operationrequest) *rbacv1.RoleBinding {
+ return &rbacv1.RoleBinding{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: config.GetGroupRoleBindingName(request.Spec.User),
+ Namespace: request.Spec.Namespace,
+ Annotations: map[string]string{
+ userAnnotationOwnerKey: request.Spec.User,
+ },
+ Labels: map[string]string{
+ userLabelOwnerKey: request.Spec.User,
+ },
+ },
+ Subjects: []rbacv1.Subject{
+ {
+ Kind: rbacv1.ServiceAccountKind,
+ Name: request.Spec.User,
+ Namespace: config.GetUserSystemNamespace(),
+ },
+ },
+ RoleRef: rbacv1.RoleRef{
+ Kind: "Role",
+ Name: string(request.Spec.Role),
+ APIGroup: rbacv1.GroupName,
+ },
+ }
+}
+
+func getLog(request *userv1.Operationrequest, kv ...any) []any {
+ return append([]any{
+ "request.name", request.Name,
+ "request.Spec.Namespace", request.Spec.Namespace,
+ "request.user", request.Spec.User,
+ "request.role", request.Spec.Role,
+ "request.action", request.Spec.Action,
+ "request.phase", request.Status.Phase,
+ }, kv...)
+}
diff --git a/controllers/user/controllers/operationrequest_controller_test.go b/controllers/user/controllers/operationrequest_controller_test.go
new file mode 100644
index 000000000000..f8a707162667
--- /dev/null
+++ b/controllers/user/controllers/operationrequest_controller_test.go
@@ -0,0 +1,186 @@
+// Copyright 2026 labring.
+//
+// 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 controllers
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/go-logr/logr"
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ rbacv1 "k8s.io/api/rbac/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/tools/record"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func TestOperationRequestWorkspaceLockSerializesSameWorkspace(t *testing.T) {
+ t.Parallel()
+ reconciler := &OperationReqReconciler{}
+ unlockFirst := reconciler.lockWorkspace("ns-workspace")
+
+ attempting := make(chan struct{})
+ acquired := make(chan func(), 1)
+ go func() {
+ close(attempting)
+ acquired <- reconciler.lockWorkspace("ns-workspace")
+ }()
+ <-attempting
+
+ select {
+ case unlock := <-acquired:
+ unlock()
+ t.Fatal("same workspace lock was acquired concurrently")
+ case <-time.After(20 * time.Millisecond):
+ }
+
+ unlockFirst()
+ select {
+ case unlock := <-acquired:
+ unlock()
+ case <-time.After(time.Second):
+ t.Fatal("waiting workspace lock was not released")
+ }
+}
+
+func TestOperationRequestPersistsDesiredRoleBinding(t *testing.T) {
+ t.Parallel()
+ for _, action := range []userv1.ActionType{userv1.Grant, userv1.Update} {
+ t.Run(string(action), func(t *testing.T) {
+ t.Parallel()
+ testOperationRequestPersistsDesiredRoleBinding(t, action)
+ })
+ }
+}
+
+func testOperationRequestPersistsDesiredRoleBinding(t *testing.T, action userv1.ActionType) {
+ t.Helper()
+ scheme := runtime.NewScheme()
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add User scheme: %v", err)
+ }
+ if err := rbacv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add RBAC scheme: %v", err)
+ }
+
+ const (
+ workspaceName = "workspace"
+ workspaceNamespace = "ns-workspace"
+ memberName = "member"
+ )
+ workspace := &userv1.User{ObjectMeta: metav1.ObjectMeta{
+ Name: workspaceName,
+ Annotations: map[string]string{
+ userv1.UserAnnotationOwnerKey: workspaceName,
+ },
+ }}
+ member := &userv1.User{ObjectMeta: metav1.ObjectMeta{
+ Name: memberName,
+ UID: types.UID("member-uid"),
+ }}
+ request := &userv1.Operationrequest{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "change-workspace-owner",
+ Namespace: config.GetUserSystemNamespace(),
+ CreationTimestamp: metav1.Now(),
+ },
+ Spec: userv1.OperationrequestSpec{
+ Namespace: workspaceNamespace,
+ User: memberName,
+ Role: userv1.OwnerRoleType,
+ Action: action,
+ },
+ }
+ existing := &rbacv1.RoleBinding{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: config.GetGroupRoleBindingName(memberName),
+ Namespace: workspaceNamespace,
+ },
+ Subjects: []rbacv1.Subject{{Kind: rbacv1.UserKind, Name: "stale"}},
+ RoleRef: rbacv1.RoleRef{
+ APIGroup: rbacv1.GroupName,
+ Kind: "Role",
+ Name: string(userv1.OwnerRoleType),
+ },
+ }
+
+ cli := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithStatusSubresource(&userv1.Operationrequest{}).
+ WithObjects(workspace, member, request, existing).
+ Build()
+ storedRequest := &userv1.Operationrequest{}
+ requestKey := client.ObjectKeyFromObject(request)
+ if err := cli.Get(context.Background(), requestKey, storedRequest); err != nil {
+ t.Fatalf("get OperationRequest: %v", err)
+ }
+ reconciler := &OperationReqReconciler{
+ Client: cli,
+ Logger: logr.Discard(),
+ Scheme: scheme,
+ Recorder: record.NewFakeRecorder(20),
+ expirationTime: time.Minute,
+ retentionTime: time.Minute,
+ }
+ if _, err := reconciler.reconcile(context.Background(), storedRequest); err != nil {
+ t.Fatalf("reconcile OperationRequest: %v", err)
+ }
+
+ roleBinding := &rbacv1.RoleBinding{}
+ roleBindingKey := client.ObjectKey{
+ Name: config.GetGroupRoleBindingName(memberName),
+ Namespace: workspaceNamespace,
+ }
+ if err := cli.Get(context.Background(), roleBindingKey, roleBinding); err != nil {
+ t.Fatalf("get RoleBinding: %v", err)
+ }
+ if roleBinding.RoleRef.Name != string(userv1.OwnerRoleType) ||
+ len(roleBinding.Subjects) != 1 || roleBinding.Subjects[0].Name != memberName {
+ t.Fatalf("RoleBinding does not match request: %#v", roleBinding)
+ }
+ controller := metav1.GetControllerOf(roleBinding)
+ if controller == nil || controller.Name != memberName || controller.UID != member.UID {
+ t.Fatalf("RoleBinding controller = %#v, want member User", controller)
+ }
+
+ updatedWorkspace := &userv1.User{}
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKey{Name: workspaceName},
+ updatedWorkspace,
+ ); err != nil {
+ t.Fatalf("get workspace User: %v", err)
+ }
+ if updatedWorkspace.Annotations[userv1.UserAnnotationOwnerKey] != memberName {
+ t.Fatalf(
+ "workspace owner = %q, want %q",
+ updatedWorkspace.Annotations[userv1.UserAnnotationOwnerKey],
+ memberName,
+ )
+ }
+
+ updatedRequest := &userv1.Operationrequest{}
+ if err := cli.Get(context.Background(), requestKey, updatedRequest); err != nil {
+ t.Fatalf("get completed OperationRequest: %v", err)
+ }
+ if updatedRequest.Status.Phase != userv1.RequestCompleted {
+ t.Fatalf("OperationRequest phase = %q, want Completed", updatedRequest.Status.Phase)
+ }
+}
diff --git a/controllers/user/controllers/suite_test.go b/controllers/user/controllers/suite_test.go
new file mode 100644
index 000000000000..d2a58b34ec15
--- /dev/null
+++ b/controllers/user/controllers/suite_test.go
@@ -0,0 +1,191 @@
+/*
+Copyright 2022 labring.
+
+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 controllers
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "testing"
+ "time"
+
+ v1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/controllers/helper/kubeconfig"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ csrv1 "k8s.io/api/certificates/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes/scheme"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/envtest"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+)
+
+// These tests use Ginkgo (BDD-style Go testing framework). Refer to
+// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
+var (
+ cfg *rest.Config
+ k8sClient client.Client
+ testEnv *envtest.Environment
+)
+
+func TestControllers(t *testing.T) {
+ RegisterFailHandler(Fail)
+
+ RunSpecs(t, "run controllers suite")
+}
+
+var _ = BeforeSuite(func() {
+ logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+ By("bootstrapping test environment")
+ useExisting := false
+ if val := os.Getenv("USE_EXISTING_CLUSTER"); val != "" {
+ if parsed, err := strconv.ParseBool(val); err == nil {
+ useExisting = parsed
+ }
+ }
+ testEnv = &envtest.Environment{
+ UseExistingCluster: &useExisting,
+ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
+ ErrorIfCRDPathMissing: true,
+ }
+
+ var err error
+ // cfg is defined in this file globally.
+ cfg, err = testEnv.Start()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cfg).NotTo(BeNil())
+
+ err = csrv1.AddToScheme(scheme.Scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ err = v1.AddToScheme(scheme.Scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ //+kubebuilder:scaffold:scheme
+
+ k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient).NotTo(BeNil())
+}, 60)
+
+var _ = AfterSuite(func() {
+ By("tearing down the test environment")
+ err := testEnv.Stop()
+ Expect(err).NotTo(HaveOccurred())
+})
+
+var _ = Describe("user kubeconfig ", func() {
+ Context("kubeconfig rotate trigger", func() {
+ It("should rotate when spec is set and status is empty", func() {
+ r := &UserReconciler{}
+ now := metav1.Now()
+ user := &v1.User{}
+ user.Spec.KubeConfigRotateAt = &now
+ Expect(r.shouldRotateKubeConfig(user)).To(BeTrue())
+ })
+
+ It("should not rotate when spec equals status", func() {
+ r := &UserReconciler{}
+ now := metav1.Now()
+ user := &v1.User{}
+ user.Spec.KubeConfigRotateAt = &now
+ user.Status.ObservedKubeConfigRotateAt = &now
+ Expect(r.shouldRotateKubeConfig(user)).To(BeFalse())
+ })
+
+ It("should rotate when spec differs from status", func() {
+ r := &UserReconciler{}
+ now := metav1.Now()
+ later := metav1.NewTime(now.Add(10 * time.Second))
+ user := &v1.User{}
+ user.Spec.KubeConfigRotateAt = &later
+ user.Status.ObservedKubeConfigRotateAt = &now
+ Expect(r.shouldRotateKubeConfig(user)).To(BeTrue())
+ })
+ })
+
+ Context("syncReNewConfig test", func() {
+ AfterEach(func() {
+ })
+ It("empty kubeconfig", func() {
+ usr := &v1.User{}
+ usr.Name = "cuisongliu"
+ cfg, event, err := syncReNewConfig(usr)
+ // Expect(err).NotTo(HaveOccurred())
+ Expect(err).ToNot(HaveOccurred())
+ Expect(event).To(BeNil())
+ Expect(cfg).To(BeNil())
+ })
+
+ It("new expired kubeconfig", func() {
+ usr := &v1.User{}
+ usr.Name = "f8699ded-58d3-432b-a9ff-56568b57a38d"
+ kubeConfig := `apiVersion: v1
+clusters:
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM2VENDQWRHZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQ0FYRFRJeU1EY3pNVEUwTkRJME1sb1lEekl4TWpJd056QTNNVFEwTWpReVdqQVZNUk13RVFZRApWUVFERXdwcmRXSmxjbTVsZEdWek1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCnVzS3Z4U1RUNFdXUUhOM0VvZmtoT0kyV1FMMWs4UTJxUWRZMklNNTBDT3NycmVuTW5tMzhpSmJONEgwTlpmQkUKZ3lYNzEvNG81WmxmUEdWRFdjL01qNkJQQXAzcEYrdVZQdnFQc1pKNUdKa2pNbGMzdWRVNzkxcC9jRlc5dUcxSQp6U01HVVRyL0V4d2UxVVZWd3NkZTdVMVpUZGhOajdPbmJQd0F1dGJVVEVxdmtIWEFpSW11K0NLaFRnRjVRVTZwCkI0T2luRVk2dWFOMWtFbzFXSUJFODlockNmSzhCeXV5V1dNTFY1RktaZjg5Ry9XNmh3T0F3Qjc2bGlabC8zTEMKTEdLdS8xWnh3VUw4QW54TWxBRi9RVFprK0ZaSkloVVFHbWlreVRwdi8vYWNyeUhXSWxHSmRPeGl6T2tUSnUzKwppMUZPR2JIR0xCZ3lnejdyc0RYMTB3SURBUUFCbzBJd1FEQU9CZ05WSFE4QkFmOEVCQU1DQXFRd0R3WURWUjBUCkFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVJbUZKVGtEMTc3OUhZVTRpNjlHbzlLczRXM0V3RFFZSktvWkkKaHZjTkFRRUxCUUFEZ2dFQkFHb2FkSEUvbGg5ZnpGSUhoRUhJUTNQSEdaOVQ2K0NOL040cG5hcGcyUFY4U09XNwpOTmVTTXZpWmJxQk5VaG1kelNiaG5sTGllRWZHYlBjSm9BVThvdUN6bXFidXFzK3pDN3U1RnBrb0loYUJ1RHdSCm5ucmJXR1Z3cE93K1RvcjhmL1NyTGxxZVl3SFhneGprMGpmaEcwM1FRQUxvb3lBRWFEWjVPU0dyWHo0SWlkYTQKVHJwZmJZcjFvWE54UjllcFRkdDJIRTZoUlEyNEVUVXBjRVRqa01uSWtObWdLdnJHSndMUUF4b0d4QklqSUh4NApQZEdlOHdURit5V3I5WlNDWkNNZ0NNR2xuSVJtdXEyVzRQWklFdU8xUkZ3RDNCTVAyQmlsNEVWekhaZlpuUkVRClZRTDI0M08xangvb3gwaGlUOWRXRXBpbXZoMm1md0hNbzlSS2d4dz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
+ server: https://192.168.64.24:6443
+ name: sealos
+contexts:
+- context:
+ cluster: sealos
+ namespace: ns-f8699ded-58d3-432b-a9ff-56568b57a38d
+ user: f8699ded-58d3-432b-a9ff-56568b57a38d
+ name: f8699ded-58d3-432b-a9ff-56568b57a38d@sealos
+current-context: f8699ded-58d3-432b-a9ff-56568b57a38d@sealos
+kind: Config
+preferences: {}
+users:
+- name: f8699ded-58d3-432b-a9ff-56568b57a38d
+ user:
+ token: test-token
+ client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURTRENDQWpDZ0F3SUJBZ0lRYldZeDZSS3dOQ1M5L29oQTArMkZ4ekFOQmdrcWhraUc5dzBCQVFzRkFEQVYKTVJNd0VRWURWUVFERXdwcmRXSmxjbTVsZEdWek1CNFhEVEl5TURneU1EQTFNalkxT0ZvWERUSXlNRGd5TURBMgpNekUxT0Zvd0x6RXRNQ3NHQTFVRUF4TWtaamcyT1Rsa1pXUXROVGhrTXkwME16SmlMV0U1Wm1ZdE5UWTFOamhpCk5UZGhNemhrTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEzQmRFa0VFNy9tTjQKWllTUkIvSW5JSGpMeW9DNHZKeUUreHM2bkczdXc3VjROdm1ia0ROSU9uS2cvQW9uNE1TNWhHbHJ1S0VydnJWSQo0L0pjTnFNMXFxTTVWVWVMRXVjVzU3cXE3QkdDbmZQSW9ndEd1YmpZRTJpWXhhYU8ySFVuSHF0VWNUajZDbTd1Ck1PWVA2di9tMkRZWjNxTDdjYTRjc1MzcVp1aW5oTG5ML1hJQXMwZUg3SmRnQkJISW1aVUlrRW9ueUNvNzI5VGkKZkFEdWhiQjZ1REtiRmFsMlAwQzZ5a1ltU2VVNHVHaytXQ0pDeDF2Nkd5THIzWXo1cHh6bXd4ck5CUUF2d2hYTwpQMFczVUZ1Rnc5MGlwQTN1dXpaT09aakxpd2U1N1ZnT1ZxbFFpMGY4SFFmYWRBUU8vTnFVc1lTQTk5K2ZzSXFoClJWK3Z5RUtITndJREFRQUJvM293ZURBT0JnTlZIUThCQWY4RUJBTUNCYUF3RXdZRFZSMGxCQXd3Q2dZSUt3WUIKQlFVSEF3SXdEQVlEVlIwVEFRSC9CQUl3QURBZkJnTlZIU01FR0RBV2dCUWlZVWxPUVBYdnYwZGhUaUxyMGFqMApxemhiY1RBaUJnTlZIUkVFR3pBWmdoZGhjR2x6WlhKMlpYSXVZMngxYzNSbGNpNXNiMk5oYkRBTkJna3Foa2lHCjl3MEJBUXNGQUFPQ0FRRUFsK1BSN1NHVVZMdjQ0bC9mY1ducHJqcFZpanM2eGJpSm9mVGNSL0JaSnRGaWpTWjEKNlRJU0t1c3hjb2lzZE16M1dsVWsyREpPYlJVV2FKamY4VXRUdkZmR0Z3UEJHU1k5aGxtR0NENEpVeXJHWVBzawoyby9jTGVDVVRWQzJvd2FPOXVyRTdmOGR4eU4xMHliaGhaZ1BuM0xDUGtiL0hCTFNJWFNFcWxwd0NmeURaMXFHCkJMbkVpa2VuVnJXc1FHSVJ1Mk91WkJINjYyU1VjMkk5SHlHemorTm1UamRES1VHNnZHdkw3WGdJdlgreWhjTDMKcVBsbFR1aFlab0lZdkErS1p1MFdFQWoyQWkvVU5MU21IbktyQkF6bm1qeXVGY2JFZFpYZDFSVGQycytWd0FFSgoxMnVHbXhIank2WCsvb0NxY0d6ZXJYUVRDSmVGWi8wWTBVMFl4UT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
+ client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBM0JkRWtFRTcvbU40WllTUkIvSW5JSGpMeW9DNHZKeUUreHM2bkczdXc3VjROdm1iCmtETklPbktnL0FvbjRNUzVoR2xydUtFcnZyVkk0L0pjTnFNMXFxTTVWVWVMRXVjVzU3cXE3QkdDbmZQSW9ndEcKdWJqWUUyaVl4YWFPMkhVbkhxdFVjVGo2Q203dU1PWVA2di9tMkRZWjNxTDdjYTRjc1MzcVp1aW5oTG5ML1hJQQpzMGVIN0pkZ0JCSEltWlVJa0VvbnlDbzcyOVRpZkFEdWhiQjZ1REtiRmFsMlAwQzZ5a1ltU2VVNHVHaytXQ0pDCngxdjZHeUxyM1l6NXB4em13eHJOQlFBdndoWE9QMFczVUZ1Rnc5MGlwQTN1dXpaT09aakxpd2U1N1ZnT1ZxbFEKaTBmOEhRZmFkQVFPL05xVXNZU0E5OStmc0lxaFJWK3Z5RUtITndJREFRQUJBb0lCQUdDVHZUeG53OEd2T0dlagpzbGlBUS9jVnlxZERzTXpqQ2Q5K1pVdTdXYWg3ZXhMMG1QUy83QlBwdFFha0ZqZWxXNXJrLzZYMjQxRStENHduCkQ1dlNKbnlJUDJKU2tJNXM1VG91U1U1cHFKMVRHbGt4QlluOXVLTmJwSjRtcGt3SnJHN2kwNTBUV1hVMmxFTFUKMFd0WGU1Nm9ydFZwUTFqeEJCb2pnZFpDaFMvUjMzM2dnUW9uRFI4NGx2aDc2cC9zM2x4MHFPbUE4K25nbURJdwpyUGdobzBuOFhVZFFaUDk0alYzVlREcklZNHFOZFlYbzRsODZqUUozTHhndkp4YzV2MUJuUk9MYXJ0N2FaR01iCnB6VzhsRXpKd2pkYlFiQ1hGRkc4bXFZTU42RkREaGl1YmRnb1h6dUd5RXNobSswRVk4clpOcWxhVzcvc0lzNjkKcUIxa0VqRUNnWUVBOFh3OWtjbjVBbGhEbnQ3SEg5bUszeUZ4TTU5Q3BRZVA0b0FPWXJuOFNZSkpvRTdzU1dvVwpXZmFmTmFBVnFQL3lCZFFnUTdSbFZYTlVSdHVibXN0MW5JKzFnbTJVQ0tEQ1VuWGR2Ylp1eHZiYnFSdlJXc0EyCmdTNG03eVBoR1hDb1lVMzhHQy9qcXB3UmFHMnVHWFp5T1M0TjVDN1lBUDMyMWpKNEp6cExzWHNDZ1lFQTZWSFUKUDlDS2VUUVFuV2RzdFRiVXJVYVZpVjhadGo3U0JkRWFtQnZWUnRoeGluU1QzaCtmOGxuaTY1czVXUzNvZUQycApYZW5oeXZwWDkvOFhrT2FtRXdzZVVHNFdCVk16Y1kzVm9CdDNlTVE2WGRaaHgwOWRRek5zZXhUMkN4bTZHTWpUCjV5cXR6TUtOTVBoOFB0cGlsUFNHbytKSXdRVkxaWkVnK24wNkhuVUNnWUVBbmttLzJlWTRFSzltYzZhM2cwc0gKV2tjRGVzRHo0RlRhbmE4dXZzd0djUEN6N2g2TmgzbkFlT3ZOWkVzd3AzeE5Xa0MzZldtcjJwMGtLdVljVXhUMgpYTTllUE1ZeStJelhrMUdyTFlWOWkzR1lmbnE3ZWU3d1N6RERXSkYrSlR6UlFpYnFEYmltVk5qRUdGMThkemhLCm11eHpNcFQ2Qlh4eTVlaGpGZU9DWmkwQ2dZRUFsR2RVL3FZUmZkaE0vU0ZzdHJMQ2dkaFVnd1QzWU1FQ2EycSsKWktQSGU0RnVicWVKNmczcGVZZ00ydGxubDc2b2o5cUFvTmlEb3N5cktYV2FzckxTVFdpVUJvcU8vU0lYcFpHVwpvSGozKzl1c1dFVmsraFlUOXd0OVk2aEllM1VJdG56K3M1bWs1SW1XcnVCT2Z0Zi9Qa2x1WGswdkEzN3ZueEc1CmpUb2J2b1VDZ1lFQTUvUDhBc1kzdjVVL24yUk5EWWczNEJPN1Awb3ZDVHo5V09TMmkybHBIcUxqMGR4V2RPUjcKU1VpNVN1UDBFK1RQODNwRGo3R3l0NVVVYmRaKys5RU4xMll2cHVxUmpIM3NMRFo3Qi8xN0tocWk3QVRIRExLcwpDalp4YmpQRlA5RUduUkxqbXJHaFd0YTlzaUN0YXhyb3ZlbVk4ME5rK2V1STBNQ1oyMklGZFdRPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
+`
+ usr.Status.KubeConfig = kubeConfig
+ cfg, event, err := syncReNewConfig(usr)
+ eventStr := fmt.Sprintf("ClientCertificateData %s is expired", usr.Name)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(event).To(Equal(&eventStr))
+ Expect(cfg).To(BeNil())
+ })
+
+ It("new kubeconfig", func() {
+ user := &v1.User{}
+ user.Name = "cuisongliu"
+ defaultExpirationDuration := int32(100000000)
+ user.Spec.CSRExpirationSeconds = defaultExpirationDuration
+ defaultConfig := kubeconfig.NewConfig("cuisongliu", "", 100000000)
+ config, err := defaultConfig.WithServiceAccountConfig("default", nil).
+ Apply(cfg, k8sClient)
+ Expect(err).ToNot(HaveOccurred())
+ kubeData, err := clientcmd.Write(*config)
+ Expect(err).ToNot(HaveOccurred())
+ user.Status.KubeConfig = string(kubeData)
+ newCfg, event, err := syncReNewConfig(user)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(event).To(BeNil())
+ Expect(newCfg).NotTo(BeNil())
+ })
+ })
+})
diff --git a/controllers/user/controllers/user_controller.go b/controllers/user/controllers/user_controller.go
new file mode 100644
index 000000000000..2937a62f22e3
--- /dev/null
+++ b/controllers/user/controllers/user_controller.go
@@ -0,0 +1,1819 @@
+/*
+Copyright 2022 labring.
+
+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 controllers
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "maps"
+ "reflect"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/go-logr/logr"
+ licensev1 "github.com/labring/sealos/controllers/license/api/v1"
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ usercache "github.com/labring/sealos/controllers/user/controllers/cache"
+ "github.com/labring/sealos/controllers/user/controllers/helper"
+ "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ "github.com/labring/sealos/controllers/user/controllers/helper/finalizer"
+ "github.com/labring/sealos/controllers/user/controllers/helper/hash"
+ "github.com/labring/sealos/controllers/user/controllers/helper/kubeconfig"
+ "github.com/labring/sealos/controllers/user/controllers/helper/ratelimiter"
+ "github.com/labring/sealos/controllers/user/pkg/licensegate"
+ "github.com/labring/sealos/controllers/user/pkg/usercount"
+ v1 "k8s.io/api/core/v1"
+ rbacv1 "k8s.io/api/rbac/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ apiMeta "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+ "k8s.io/client-go/tools/clientcmd/api"
+ "k8s.io/client-go/tools/record"
+ "k8s.io/client-go/util/retry"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ kubecontroller "sigs.k8s.io/controller-runtime/pkg/controller"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/handler"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+)
+
+const (
+ userAnnotationCreatorKey = userv1.UserAnnotationCreatorKey
+ userAnnotationOwnerKey = userv1.UserAnnotationOwnerKey
+ userLabelOwnerKey = userv1.UserLabelOwnerKey
+ licenseLimitedCondition = userv1.ConditionType("LicenseLimited")
+ adminUserName = "admin"
+ adminClusterRoleBindingName = config.AdminClusterRoleBindingName
+ userFinalizerName = "sealos.io/user.finalizers"
+ namespaceSyncReadyCondition = userv1.ConditionType("NamespaceSyncReady")
+ serviceAccountReadyCondition = userv1.ConditionType("ServiceAccountSyncReady")
+ kubeConfigReadyCondition = userv1.ConditionType("KubeConfigSyncReady")
+ roleSyncReadyCondition = userv1.ConditionType("RoleSyncReady")
+ roleBindingReadyCondition = userv1.ConditionType("RoleBindingSyncReady")
+ clusterRoleBindingReadyCondition = userv1.ConditionType("ClusterRoleBindingSyncReady")
+)
+
+// UserReconciler reconciles a User object
+type UserReconciler struct {
+ Logger logr.Logger
+ Recorder record.EventRecorder
+ cache client.Reader
+ userCounter *usercount.Counter
+ config *rest.Config
+ *runtime.Scheme
+ client.Client
+ finalizer *finalizer.Finalizer
+ minRequeueDuration time.Duration
+ nextKubeConfigSync sync.Map
+ // EnableAdminClusterAdmin preserves the legacy cluster-admin binding for
+ // the admin user when explicitly enabled. It is disabled by default.
+ EnableAdminClusterAdmin bool
+ // EnableStrictNamespacePodSecurity applies Pod Security labels to every
+ // namespace whose name starts with ns-, including namespaces without a User.
+ // It is enabled by default by the controller entrypoint.
+ EnableStrictNamespacePodSecurity bool
+}
+
+type userReconcileState struct {
+ serviceAccount *v1.ServiceAccount
+ tokenExpirationDeadline *metav1.Time
+ currentSecretName string
+ kubeConfigSyncAttempted bool
+ kubeConfigSynced bool
+ cleanupLegacySecrets bool
+ syncError error
+}
+
+func (s *userReconcileState) recordSyncError(err error) {
+ if s == nil || err == nil {
+ return
+ }
+ s.syncError = errors.Join(s.syncError, err)
+}
+
+// +kubebuilder:rbac:groups=*,resources=*,verbs=*
+// +kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+//
+// For more details, check Reconcile and its Result here:
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile
+func (r *UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ r.Logger.V(1).Info("start reconcile for users")
+ user := &userv1.User{}
+ if err := r.Get(ctx, req.NamespacedName, user); err != nil {
+ if apierrors.IsNotFound(err) {
+ r.nextKubeConfigSync.Delete(req.Name)
+ if req.Name == adminUserName {
+ if cleanupErr := r.cleanupDisabledAdminClusterRoleBinding(ctx); cleanupErr != nil {
+ return ctrl.Result{}, cleanupErr
+ }
+ }
+ }
+ if apierrors.IsNotFound(err) && r.EnableStrictNamespacePodSecurity {
+ if syncErr := r.syncOrphanNamespace(
+ ctx,
+ config.GetUsersNamespace(req.Name),
+ ); syncErr != nil {
+ return ctrl.Result{}, syncErr
+ }
+ }
+ return ctrl.Result{}, client.IgnoreNotFound(err)
+ }
+
+ if ok, err := r.finalizer.RemoveFinalizer(
+ ctx,
+ user,
+ func(ctx context.Context, obj client.Object) error {
+ ns := &v1.Namespace{}
+ ns.Name = config.GetUsersNamespace(user.Name)
+ _ = r.Delete(ctx, ns)
+ if user.Name == adminUserName {
+ return r.cleanupDisabledAdminClusterRoleBinding(ctx)
+ }
+ return nil
+ },
+ ); ok {
+ r.nextKubeConfigSync.Delete(user.Name)
+ return ctrl.Result{}, err
+ }
+
+ if ok, err := r.finalizer.AddFinalizer(ctx, user); ok {
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ return r.reconcile(ctx, user)
+ }
+ return ctrl.Result{}, errors.New("reconcile error from Finalizer")
+}
+
+type OwnerAnnotationChangedPredicate struct {
+ predicate.Funcs
+}
+
+type DeletionTimestampChangedPredicate struct {
+ predicate.Funcs
+}
+
+// NamespacePodSecurityPredicate reconciles ns-* namespaces when their
+// Pod Security Admission labels or User ownership metadata changes.
+type NamespacePodSecurityPredicate struct {
+ predicate.Funcs
+}
+
+type AdminClusterRoleBindingPredicate struct {
+ predicate.Funcs
+}
+
+// ignorePreStartCreatePredicate drops Create events for objects that already
+// existed when this controller was configured. Existing Users re-evaluate the
+// shared License and verify their children during startup. The manager starts
+// each informer once; watch reconnects relist through the same informer.
+//
+// TODO: after upgrading controller-runtime to v0.22+, use
+// event.CreateEvent.IsInInitialList and coalesce initial events for the same
+// User with a bounded debounce before enqueueing one reconciliation request.
+// When the minimum supported API server provides WatchList, validate its
+// compatibility and fallback behavior before enabling client-go's
+// KUBE_FEATURE_WatchListClient to reduce initial LIST peak memory.
+type ignorePreStartCreatePredicate struct {
+ predicate.Funcs
+ startedAt time.Time
+}
+
+func (p ignorePreStartCreatePredicate) Create(e event.CreateEvent) bool {
+ if e.Object == nil {
+ return false
+ }
+ createdAt := e.Object.GetCreationTimestamp().Time
+ if p.startedAt.IsZero() || createdAt.IsZero() {
+ return true
+ }
+ return !createdAt.Before(p.startedAt)
+}
+
+func (AdminClusterRoleBindingPredicate) Create(e event.CreateEvent) bool {
+ return e.Object.GetName() == adminClusterRoleBindingName
+}
+
+func (AdminClusterRoleBindingPredicate) Update(e event.UpdateEvent) bool {
+ return e.ObjectNew.GetName() == adminClusterRoleBindingName
+}
+
+func (AdminClusterRoleBindingPredicate) Delete(e event.DeleteEvent) bool {
+ return e.Object.GetName() == adminClusterRoleBindingName
+}
+
+func (AdminClusterRoleBindingPredicate) Generic(event.GenericEvent) bool {
+ return false
+}
+
+func (NamespacePodSecurityPredicate) Create(e event.CreateEvent) bool {
+ return isUserNamespace(e.Object.GetName())
+}
+
+func (NamespacePodSecurityPredicate) Update(e event.UpdateEvent) bool {
+ if !isUserNamespace(e.ObjectNew.GetName()) {
+ return false
+ }
+ return namespaceMetadataChanged(
+ e.ObjectOld.GetAnnotations(),
+ e.ObjectNew.GetAnnotations(),
+ e.ObjectOld.GetLabels(),
+ e.ObjectNew.GetLabels(),
+ ) || e.ObjectOld.GetAnnotations()[userv1.UserAnnotationCreatorKey] !=
+ e.ObjectNew.GetAnnotations()[userv1.UserAnnotationCreatorKey] ||
+ !reflect.DeepEqual(e.ObjectOld.GetOwnerReferences(), e.ObjectNew.GetOwnerReferences())
+}
+
+func (NamespacePodSecurityPredicate) Delete(e event.DeleteEvent) bool {
+ return isUserNamespace(e.Object.GetName())
+}
+
+func (NamespacePodSecurityPredicate) Generic(event.GenericEvent) bool {
+ return false
+}
+
+func podSecurityLabelsChanged(oldLabels, newLabels map[string]string) bool {
+ for key := range oldLabels {
+ if config.IsPodSecurityLabel(key) && oldLabels[key] != newLabels[key] {
+ return true
+ }
+ }
+ for key := range newLabels {
+ if config.IsPodSecurityLabel(key) && oldLabels[key] != newLabels[key] {
+ return true
+ }
+ }
+ return false
+}
+
+func namespaceMetadataChanged(
+ oldAnnotations, newAnnotations, oldLabels, newLabels map[string]string,
+) bool {
+ return podSecurityLabelsChanged(oldLabels, newLabels) ||
+ oldAnnotations[userv1.UserAnnotationOwnerKey] != newAnnotations[userv1.UserAnnotationOwnerKey] ||
+ oldLabels[userv1.UserLabelOwnerKey] != newLabels[userv1.UserLabelOwnerKey]
+}
+
+func isUserNamespace(name string) bool {
+ return strings.HasPrefix(name, "ns-") && len(name) > len("ns-")
+}
+
+func (r *UserReconciler) namespaceToUserRequests(
+ _ context.Context,
+ obj client.Object,
+) []ctrl.Request {
+ if !isUserNamespace(obj.GetName()) {
+ return nil
+ }
+ return []ctrl.Request{{NamespacedName: client.ObjectKey{
+ Name: config.GetUserNameByNamespace(obj.GetName()),
+ }}}
+}
+
+func metadataValueMatches(values map[string]string, key, expected string) bool {
+ value, ok := values[key]
+ return ok && value == expected
+}
+
+func controlledByUser(obj metav1.Object, user *userv1.User) bool {
+ for _, ref := range obj.GetOwnerReferences() {
+ if ref.Controller != nil && *ref.Controller &&
+ ref.APIVersion == userv1.GroupVersion.String() &&
+ ref.Kind == "User" && ref.Name == user.Name && ref.UID == user.UID {
+ return true
+ }
+ }
+ return false
+}
+
+func userStatusNeedsSync(user *userv1.User) bool {
+ if user == nil {
+ return true
+ }
+ if user.Status.Phase != userv1.UserActive ||
+ user.Status.ObservedGeneration != user.Generation ||
+ !helper.IsConditionsTrue(user.Status.Conditions) ||
+ !helper.IsConditionTrue(user.Status.Conditions, userv1.Condition{
+ Type: userv1.Ready,
+ Status: v1.ConditionTrue,
+ }) {
+ return true
+ }
+ if !csrExpirationStatusMatches(
+ user.Spec.CSRExpirationSeconds,
+ user.Status.ObservedCSRExpirationSeconds,
+ ) {
+ return true
+ }
+ return user.Spec.KubeConfigRotateAt != nil &&
+ (user.Status.ObservedKubeConfigRotateAt == nil ||
+ !user.Spec.KubeConfigRotateAt.Equal(user.Status.ObservedKubeConfigRotateAt))
+}
+
+func csrExpirationStatusMatches(spec, observed int32) bool {
+ return userv1.NormalizeCSRExpirationSeconds(spec) ==
+ userv1.NormalizeCSRExpirationSeconds(observed)
+}
+
+func setObservedCSRExpirationSeconds(user *userv1.User) {
+ if user == nil {
+ return
+ }
+ user.Status.ObservedCSRExpirationSeconds = userv1.NormalizeCSRExpirationSeconds(
+ user.Spec.CSRExpirationSeconds,
+ )
+}
+
+func podSecurityLabelsNeedSync(
+ name string,
+ labels map[string]string,
+ enableAdminClusterAdmin bool,
+) bool {
+ desired := desiredNamespaceLabels(name, cloneStringMap(labels), enableAdminClusterAdmin)
+ return podSecurityLabelsChanged(labels, desired)
+}
+
+func cloneStringMap(source map[string]string) map[string]string {
+ if source == nil {
+ return make(map[string]string)
+ }
+ clone := make(map[string]string, len(source))
+ maps.Copy(clone, source)
+ return clone
+}
+
+func (r *UserReconciler) kubeConfigSyncDue(user *userv1.User) bool {
+ if user == nil {
+ return true
+ }
+ if value, ok := r.nextKubeConfigSync.Load(user.Name); ok {
+ deadline, valid := value.(time.Time)
+ return !valid || !deadline.After(time.Now())
+ }
+ if user.Status.KubeConfigRefreshAt == nil {
+ return true
+ }
+ return !user.Status.KubeConfigRefreshAt.After(time.Now())
+}
+
+func namespaceMatchesUser(
+ namespace metav1.Object,
+ user *userv1.User,
+ enableAdminClusterAdmin bool,
+) bool {
+ owner := user.Annotations[userv1.UserAnnotationOwnerKey]
+ securityLabelsNeedSync := podSecurityLabelsNeedSync(
+ namespace.GetName(),
+ namespace.GetLabels(),
+ enableAdminClusterAdmin,
+ )
+ return metadataValueMatches(
+ namespace.GetAnnotations(),
+ userv1.UserAnnotationCreatorKey,
+ user.Name,
+ ) &&
+ metadataValueMatches(namespace.GetAnnotations(), userv1.UserAnnotationOwnerKey, owner) &&
+ metadataValueMatches(namespace.GetLabels(), userv1.UserLabelOwnerKey, owner) &&
+ controlledByUser(namespace, user) &&
+ !securityLabelsNeedSync
+}
+
+func roleMatchesUser(
+ ctx context.Context,
+ reader client.Reader,
+ key client.ObjectKey,
+ roleType userv1.RoleType,
+ user *userv1.User,
+) bool {
+ role := &rbacv1.Role{}
+ if err := reader.Get(ctx, key, role); err != nil {
+ return false
+ }
+ if !metadataMatchesUserResource(role, user) {
+ return false
+ }
+ if cachedRulesHash, ok := role.Annotations[config.RoleRulesHashAnnotation]; ok {
+ return cachedRulesHash == hash.HashToString(config.GetUserRole(roleType))
+ }
+ return reflect.DeepEqual(role.Rules, config.GetUserRole(roleType))
+}
+
+func roleBindingMatchesUser(roleBinding *rbacv1.RoleBinding, user *userv1.User) bool {
+ if !metadataMatchesUserResource(roleBinding, user) {
+ return false
+ }
+ if cachedSpecHash, ok := roleBinding.Annotations[usercache.RoleBindingSpecHashAnnotation]; ok {
+ return cachedSpecHash == usercache.RoleBindingSpecHash(
+ rbacv1.RoleRef{
+ APIGroup: rbacv1.GroupName,
+ Kind: "Role",
+ Name: string(userv1.OwnerRoleType),
+ },
+ config.GetUsersSubject(user.Name),
+ )
+ }
+ return reflect.DeepEqual(roleBinding.Subjects, config.GetUsersSubject(user.Name)) &&
+ reflect.DeepEqual(roleBinding.RoleRef, rbacv1.RoleRef{
+ APIGroup: rbacv1.GroupName,
+ Kind: "Role",
+ Name: string(userv1.OwnerRoleType),
+ })
+}
+
+func clusterRoleBindingMatchesUser(binding *rbacv1.ClusterRoleBinding, user *userv1.User) bool {
+ if !metadataMatchesUserResource(binding, user) {
+ return false
+ }
+ if cachedSpecHash, ok := binding.Annotations[usercache.RoleBindingSpecHashAnnotation]; ok {
+ return cachedSpecHash == usercache.RoleBindingSpecHash(
+ rbacv1.RoleRef{
+ APIGroup: rbacv1.GroupName,
+ Kind: "ClusterRole",
+ Name: "cluster-admin",
+ },
+ config.GetUsersSubject(user.Name),
+ )
+ }
+ return reflect.DeepEqual(binding.Subjects, config.GetUsersSubject(user.Name)) &&
+ reflect.DeepEqual(binding.RoleRef, rbacv1.RoleRef{
+ APIGroup: rbacv1.GroupName,
+ Kind: "ClusterRole",
+ Name: "cluster-admin",
+ })
+}
+
+func metadataMatchesUserResource(metadata metav1.Object, user *userv1.User) bool {
+ owner := user.Annotations[userv1.UserAnnotationOwnerKey]
+ return metadataValueMatches(
+ metadata.GetAnnotations(),
+ userv1.UserAnnotationCreatorKey,
+ user.Name,
+ ) &&
+ metadataValueMatches(metadata.GetAnnotations(), userv1.UserAnnotationOwnerKey, owner) &&
+ controlledByUser(metadata, user)
+}
+
+func (r *UserReconciler) syncOrphanNamespace(ctx context.Context, namespaceName string) error {
+ if !r.EnableStrictNamespacePodSecurity || !isUserNamespace(namespaceName) {
+ return nil
+ }
+ return retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ ns := &v1.Namespace{}
+ if err := r.Get(ctx, client.ObjectKey{Name: namespaceName}, ns); err != nil {
+ if apierrors.IsNotFound(err) {
+ return nil
+ }
+ return err
+ }
+ if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error {
+ if ns.Labels == nil {
+ ns.Labels = make(map[string]string)
+ }
+ ns.Labels = config.SetPodSecurity(ns.Labels)
+ return nil
+ }); err != nil {
+ return fmt.Errorf(
+ "unable to apply Pod Security labels to orphan namespace %s: %w",
+ namespaceName,
+ err,
+ )
+ }
+ return nil
+ })
+}
+
+func (r *UserReconciler) cleanupDisabledAdminClusterRoleBinding(ctx context.Context) error {
+ if r.EnableAdminClusterAdmin {
+ return nil
+ }
+ binding := &rbacv1.ClusterRoleBinding{}
+ binding.Name = adminClusterRoleBindingName
+ if err := r.Delete(ctx, binding); err != nil && !apierrors.IsNotFound(err) {
+ return fmt.Errorf("unable to remove disabled admin cluster role binding: %w", err)
+ }
+ return nil
+}
+
+func (r *UserReconciler) adminClusterRoleBindingToUserRequests(
+ _ context.Context,
+ obj client.Object,
+) []ctrl.Request {
+ if obj.GetName() != adminClusterRoleBindingName {
+ return nil
+ }
+ return []ctrl.Request{{NamespacedName: client.ObjectKey{Name: adminUserName}}}
+}
+
+func desiredNamespaceLabels(
+ name string,
+ labels map[string]string,
+ enableAdminClusterAdmin bool,
+) map[string]string {
+ if name == config.GetUsersNamespace(adminUserName) && enableAdminClusterAdmin {
+ for key := range labels {
+ if config.IsPodSecurityLabel(key) {
+ delete(labels, key)
+ }
+ }
+ return labels
+ }
+ return config.SetPodSecurity(labels)
+}
+
+type adminPrivilegeMigration struct {
+ client client.Client
+ reader client.Reader
+ reconciler *UserReconciler
+ enableAdminClusterAdmin bool
+}
+
+func (c *adminPrivilegeMigration) Start(ctx context.Context) error {
+ if !c.enableAdminClusterAdmin {
+ binding := &rbacv1.ClusterRoleBinding{}
+ binding.Name = adminClusterRoleBindingName
+ if err := c.client.Delete(ctx, binding); err != nil && !apierrors.IsNotFound(err) {
+ return fmt.Errorf("remove disabled admin cluster role binding: %w", err)
+ }
+ }
+ if c.reconciler == nil {
+ return nil
+ }
+ admin := &userv1.User{}
+ if c.reader == nil {
+ return errors.New("admin user privilege migration reader is nil")
+ }
+ if err := c.reader.Get(ctx, client.ObjectKey{Name: adminUserName}, admin); err != nil {
+ if apierrors.IsNotFound(err) || apiMeta.IsNoMatchError(err) {
+ return nil
+ }
+ return fmt.Errorf("get admin user for privilege migration: %w", err)
+ }
+ state := &userReconcileState{}
+ c.reconciler.syncNamespace(ctx, admin, state)
+ c.reconciler.syncClusterRoleBinding(ctx, admin, state)
+ if state.syncError != nil {
+ return fmt.Errorf("sync admin privileges: %w", state.syncError)
+ }
+ return nil
+}
+
+func (c *adminPrivilegeMigration) NeedLeaderElection() bool {
+ return true
+}
+
+func (OwnerAnnotationChangedPredicate) Update(e event.UpdateEvent) bool {
+ return e.ObjectOld.GetAnnotations()[userAnnotationOwnerKey] !=
+ e.ObjectNew.GetAnnotations()[userAnnotationOwnerKey]
+}
+
+func (DeletionTimestampChangedPredicate) Update(e event.UpdateEvent) bool {
+ return !reflect.DeepEqual(
+ e.ObjectOld.GetDeletionTimestamp(),
+ e.ObjectNew.GetDeletionTimestamp(),
+ )
+}
+
+// SetupWithManager sets up the controller with the Manager.
+// The deprecated max-requeue-duration and restart-predicate-time arguments are
+// retained for compatibility and ignored.
+func (r *UserReconciler) SetupWithManager(mgr ctrl.Manager, opts ratelimiter.RateLimiterOptions,
+ minRequeueDuration, _, _ time.Duration,
+ userCounter *usercount.Counter,
+) error {
+ controllerStartedAt := time.Now()
+ const controllerName = "user_controller"
+ if r.Client == nil {
+ r.Client = mgr.GetClient()
+ }
+ r.Logger = ctrl.Log.WithName(controllerName)
+ if r.Recorder == nil {
+ r.Recorder = mgr.GetEventRecorderFor(controllerName)
+ }
+ if r.finalizer == nil {
+ r.finalizer = finalizer.NewFinalizer(r.Client, userFinalizerName).
+ WithReader(mgr.GetAPIReader())
+ }
+ r.Scheme = mgr.GetScheme()
+ r.cache = mgr.GetCache()
+ r.userCounter = userCounter
+ r.config = mgr.GetConfig()
+ r.Logger.V(1).Info("init reconcile controller user")
+ r.minRequeueDuration = minRequeueDuration
+
+ if err := mgr.Add(&adminPrivilegeMigration{
+ client: r.Client,
+ reader: mgr.GetAPIReader(),
+ reconciler: r,
+ enableAdminClusterAdmin: r.EnableAdminClusterAdmin,
+ }); err != nil {
+ return fmt.Errorf("add admin privilege migration: %w", err)
+ }
+
+ secretMetadata := &metav1.PartialObjectMetadata{}
+ secretMetadata.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Secret"))
+ if err := mgr.GetFieldIndexer().IndexField(
+ context.Background(),
+ secretMetadata,
+ v1.ServiceAccountNameKey,
+ func(rawObj client.Object) []string {
+ var annotations map[string]string
+ switch secret := rawObj.(type) {
+ case *v1.Secret:
+ annotations = secret.Annotations
+ case *metav1.PartialObjectMetadata:
+ annotations = secret.Annotations
+ }
+ if annotations == nil {
+ return nil
+ }
+ value := annotations[v1.ServiceAccountNameKey]
+ if value == "" {
+ return nil
+ }
+ return []string{value}
+ },
+ ); err != nil {
+ return err
+ }
+ if err := registerStartupCacheInformers(mgr.GetCache()); err != nil {
+ return err
+ }
+
+ ownerEventHandler := handler.EnqueueRequestForOwner(
+ r.Scheme,
+ r.RESTMapper(),
+ &userv1.User{},
+ handler.OnlyControllerOwner(),
+ )
+ ignorePreStartCreate := ignorePreStartCreatePredicate{startedAt: controllerStartedAt}
+ // Preserve the name derived from For(&userv1.User{}) for metrics and queue identity.
+ return ctrl.NewControllerManagedBy(mgr).
+ Named("user").
+ Watches(
+ &userv1.User{},
+ userEventHandler{},
+ builder.WithPredicates(predicate.Or(
+ predicate.GenerationChangedPredicate{},
+ OwnerAnnotationChangedPredicate{},
+ DeletionTimestampChangedPredicate{},
+ )),
+ ).
+ WatchesMetadata(
+ &v1.Namespace{},
+ handler.EnqueueRequestsFromMapFunc(r.namespaceToUserRequests),
+ builder.WithPredicates(NamespacePodSecurityPredicate{}),
+ ).
+ Watches(
+ &rbacv1.ClusterRoleBinding{},
+ handler.EnqueueRequestsFromMapFunc(r.adminClusterRoleBindingToUserRequests),
+ builder.WithPredicates(AdminClusterRoleBindingPredicate{}),
+ ).
+ Watches(
+ &licensev1.License{},
+ handler.EnqueueRequestsFromMapFunc(r.licenseToUserRequests),
+ builder.OnlyMetadata,
+ builder.WithPredicates(ignorePreStartCreate),
+ ).
+ Watches(
+ &rbacv1.Role{},
+ ownerEventHandler,
+ builder.WithPredicates(ignorePreStartCreate),
+ ).
+ Watches(
+ &rbacv1.RoleBinding{},
+ ownerEventHandler,
+ builder.WithPredicates(ignorePreStartCreate),
+ ).
+ Watches(
+ &v1.ServiceAccount{},
+ ownerEventHandler,
+ builder.WithPredicates(ignorePreStartCreate),
+ ).
+ WatchesMetadata(
+ &v1.Secret{},
+ ownerEventHandler,
+ builder.WithPredicates(ignorePreStartCreate),
+ ).
+ WithOptions(kubecontroller.Options{
+ MaxConcurrentReconciles: ratelimiter.GetConcurrent(opts),
+ RateLimiter: ratelimiter.GetRateLimiter(opts),
+ }).
+ Complete(r)
+}
+
+func registerStartupCacheInformers(informers ctrlcache.Informers) error {
+ if informers == nil {
+ return errors.New("user controller cache is nil")
+ }
+ namespaceMetadata := &metav1.PartialObjectMetadata{}
+ namespaceMetadata.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Namespace"))
+ secretMetadata := &metav1.PartialObjectMetadata{}
+ secretMetadata.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Secret"))
+ objects := []client.Object{
+ namespaceMetadata,
+ &v1.ServiceAccount{},
+ secretMetadata,
+ &rbacv1.Role{},
+ &rbacv1.RoleBinding{},
+ &rbacv1.ClusterRoleBinding{},
+ }
+ for _, object := range objects {
+ if _, err := informers.GetInformer(context.Background(), object); err != nil {
+ return fmt.Errorf("register startup cache informer for %T: %w", object, err)
+ }
+ }
+ return nil
+}
+
+func (r *UserReconciler) reconcile(ctx context.Context, obj client.Object) (ctrl.Result, error) {
+ r.Logger.V(1).
+ Info("update reconcile controller user", "request", client.ObjectKeyFromObject(obj))
+ startTime := time.Now()
+
+ user, ok := obj.(*userv1.User)
+ if !ok {
+ return ctrl.Result{}, errors.New("obj convert user is error")
+ }
+
+ originalStatus := user.Status.DeepCopy()
+ blocked, err := r.handleLicenseLimit(ctx, user, originalStatus)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ if blocked {
+ return ctrl.Result{RequeueAfter: r.minRequeueDuration}, nil
+ }
+
+ defer func() {
+ r.Logger.V(1).
+ Info("finished reconcile", "user info", user.Name, "create time", user.CreationTimestamp, "reconcile cost time", time.Since(startTime))
+ }()
+
+ state := &userReconcileState{}
+ if userStatusNeedsSync(user) {
+ r.initStatus(ctx, user, state)
+ }
+ r.syncNamespaceIfNeeded(ctx, user, state)
+ r.syncServiceAccountIfNeeded(ctx, user, state)
+ r.syncKubeConfigIfNeeded(ctx, user, state)
+ r.syncRolesIfNeeded(ctx, user, state)
+ r.syncRoleBindingIfNeeded(ctx, user, state)
+ r.syncClusterRoleBindingIfNeeded(ctx, user, state)
+ r.syncFinalStatus(ctx, user, state)
+ if user.Status.Phase != userv1.UserUnknown {
+ user.Status.Phase = userv1.UserActive
+ }
+ if state.cleanupLegacySecrets {
+ // Best-effort migration cleanup for legacy service-account-token secrets.
+ if err := kubeconfig.CleanupLegacyBoundTokenSecrets(
+ ctx,
+ r.cache,
+ r.Client,
+ user.Name,
+ state.currentSecretName,
+ ); err != nil {
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "CleanupLegacyBoundTokenSecrets",
+ "Cleanup stale bound token secrets for %s is error: %v",
+ user.Name,
+ err,
+ )
+ r.Logger.Error(err, "cleanup stale bound token secrets", "user", user.Name)
+ }
+ }
+ statusChanged := !reflect.DeepEqual(user.Status, *originalStatus)
+ if statusChanged && state.kubeConfigSynced {
+ // Normalize the observed duration only when this reconcile successfully
+ // refreshed kubeconfig and is about to persist its status.
+ setObservedCSRExpirationSeconds(user)
+ }
+ if !statusChanged {
+ requeueAfter := nextKubeConfigRequeueDuration(user, state)
+ if syncErr := r.finishKubeConfigSync(user, state); syncErr != nil {
+ return ctrl.Result{}, syncErr
+ }
+ if state.syncError != nil {
+ return ctrl.Result{}, state.syncError
+ }
+ return ctrl.Result{RequeueAfter: requeueAfter}, nil
+ }
+ if err = r.updateStatus(ctx, user, originalStatus); err != nil {
+ if state.kubeConfigSyncAttempted {
+ // The generated kubeconfig is not durable until the status patch succeeds.
+ r.nextKubeConfigSync.Store(user.Name, time.Now())
+ }
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "SyncStatus",
+ "Sync status %s is error: %v",
+ user.Name,
+ err,
+ )
+ return ctrl.Result{}, err
+ }
+ requeueAfter := nextKubeConfigRequeueDuration(user, state)
+ if syncErr := r.finishKubeConfigSync(user, state); syncErr != nil {
+ return ctrl.Result{}, syncErr
+ }
+ if state.syncError != nil {
+ return ctrl.Result{}, state.syncError
+ }
+ return ctrl.Result{RequeueAfter: requeueAfter}, nil
+}
+
+func (r *UserReconciler) finishKubeConfigSync(
+ user *userv1.User,
+ state *userReconcileState,
+) error {
+ if state == nil || user == nil {
+ return nil
+ }
+ if !state.kubeConfigSyncAttempted {
+ return nil
+ }
+ if !state.kubeConfigSynced {
+ // Keep the in-memory deadline due so the next workqueue retry does not
+ // get hidden by a future persisted refresh time.
+ r.nextKubeConfigSync.Store(user.Name, time.Now())
+ return state.syncError
+ }
+ if user.Status.KubeConfigRefreshAt == nil || user.Status.KubeConfigRefreshAt.IsZero() {
+ // A successful refresh always persists this field. Clearing the entry
+ // keeps a malformed in-memory state from suppressing a retry.
+ r.nextKubeConfigSync.Delete(user.Name)
+ return nil
+ }
+ // The workqueue requeue interval is for ordinary drift checks. Kubeconfig
+ // refreshes must follow the persisted token deadline instead.
+ r.nextKubeConfigSync.Store(user.Name, user.Status.KubeConfigRefreshAt.Time)
+ return nil
+}
+
+func (r *UserReconciler) initStatus(_ context.Context, user *userv1.User, _ *userReconcileState) {
+ initializedCondition := userv1.Condition{
+ Type: userv1.Initialized,
+ Status: v1.ConditionTrue,
+ Reason: string(userv1.Initialized),
+ Message: "user has been initialized",
+ LastTransitionTime: metav1.Now(),
+ LastHeartbeatTime: metav1.Now(),
+ }
+ user.Status.Phase = userv1.UserPending
+ user.Status.ObservedGeneration = user.Generation
+ if !helper.IsConditionTrue(user.Status.Conditions, initializedCondition) {
+ user.Status.Conditions = helper.UpdateCondition(
+ user.Status.Conditions,
+ initializedCondition,
+ )
+ }
+}
+
+func (r *UserReconciler) markConditionReadyIfNeeded(
+ user *userv1.User,
+ conditionType userv1.ConditionType,
+) {
+ for i := range user.Status.Conditions {
+ if user.Status.Conditions[i].Type != conditionType ||
+ user.Status.Conditions[i].Status == v1.ConditionTrue {
+ continue
+ }
+ r.saveCondition(user, &userv1.Condition{
+ Type: conditionType,
+ Status: v1.ConditionTrue,
+ Reason: string(userv1.Ready),
+ Message: "cached resource matches desired state",
+ LastTransitionTime: metav1.Now(),
+ LastHeartbeatTime: metav1.Now(),
+ })
+ return
+ }
+}
+
+func (r *UserReconciler) syncNamespaceIfNeeded(
+ ctx context.Context,
+ user *userv1.User,
+ state *userReconcileState,
+) {
+ if r.cache != nil {
+ namespace := &metav1.PartialObjectMetadata{}
+ namespace.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Namespace"))
+ if err := r.cache.Get(
+ ctx,
+ client.ObjectKey{Name: config.GetUsersNamespace(user.Name)},
+ namespace,
+ ); err == nil &&
+ namespaceMatchesUser(namespace, user, r.EnableAdminClusterAdmin) {
+ r.markConditionReadyIfNeeded(user, namespaceSyncReadyCondition)
+ return
+ }
+ }
+ r.syncNamespace(ctx, user, state)
+}
+
+func (r *UserReconciler) syncServiceAccountIfNeeded(
+ ctx context.Context,
+ user *userv1.User,
+ state *userReconcileState,
+) {
+ if r.cache != nil {
+ serviceAccount := &v1.ServiceAccount{}
+ if err := r.cache.Get(ctx, client.ObjectKey{
+ Name: user.Name, Namespace: config.GetUserSystemNamespace(),
+ }, serviceAccount); err == nil && metadataMatchesUserResource(serviceAccount, user) {
+ state.serviceAccount = serviceAccount
+ r.markConditionReadyIfNeeded(user, serviceAccountReadyCondition)
+ return
+ }
+ }
+ r.syncServiceAccount(ctx, user, state)
+}
+
+func (r *UserReconciler) syncKubeConfigIfNeeded(
+ ctx context.Context,
+ user *userv1.User,
+ state *userReconcileState,
+) {
+ if r.cache != nil && !kubeConfigSyncFailed(user) &&
+ !r.kubeConfigSyncDue(user) &&
+ csrExpirationStatusMatches(
+ user.Spec.CSRExpirationSeconds,
+ user.Status.ObservedCSRExpirationSeconds,
+ ) &&
+ user.Status.KubeConfigRefreshAt != nil &&
+ !userKubeConfigNeedsRotation(user) && r.boundTokenSecretMatches(ctx, user, state) {
+ r.markConditionReadyIfNeeded(user, kubeConfigReadyCondition)
+ return
+ }
+ state.kubeConfigSyncAttempted = true
+ r.syncKubeConfig(ctx, user, state)
+}
+
+func (r *UserReconciler) boundTokenSecretMatches(
+ ctx context.Context,
+ user *userv1.User,
+ state *userReconcileState,
+) bool {
+ if state == nil || state.serviceAccount == nil || len(state.serviceAccount.Secrets) == 0 ||
+ state.serviceAccount.Secrets[0].Name == "" {
+ return false
+ }
+ secret := &metav1.PartialObjectMetadata{}
+ secret.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Secret"))
+ if err := r.cache.Get(ctx, client.ObjectKey{
+ Name: state.serviceAccount.Secrets[0].Name, Namespace: config.GetUserSystemNamespace(),
+ }, secret); err != nil {
+ return false
+ }
+ if secret.Annotations[v1.ServiceAccountNameKey] != user.Name ||
+ !controlledByUser(secret, user) {
+ return false
+ }
+ if user.Status.ObservedKubeConfigSecretUID == "" {
+ // Legacy Users without an observed UID can keep using the current Secret.
+ // Persist the UID only after a successful kubeconfig refresh.
+ return true
+ }
+ return string(secret.UID) == user.Status.ObservedKubeConfigSecretUID
+}
+
+func (r *UserReconciler) syncRolesIfNeeded(
+ ctx context.Context,
+ user *userv1.User,
+ state *userReconcileState,
+) {
+ roleCondition := &userv1.Condition{
+ Type: roleSyncReadyCondition,
+ Status: v1.ConditionTrue,
+ LastTransitionTime: metav1.Now(),
+ LastHeartbeatTime: metav1.Now(),
+ Reason: string(userv1.Ready),
+ Message: "sync namespace role successfully",
+ }
+ previousCondition := helper.GetCondition(user.Status.Conditions, roleCondition).DeepCopy()
+ defer func() {
+ if helper.DiffCondition(previousCondition, roleCondition) {
+ r.saveCondition(user, roleCondition.DeepCopy())
+ }
+ }()
+ for _, roleType := range []userv1.RoleType{
+ userv1.OwnerRoleType,
+ userv1.ManagerRoleType,
+ userv1.DeveloperRoleType,
+ } {
+ healthy := false
+ if r.cache != nil {
+ healthy = roleMatchesUser(ctx, r.cache, client.ObjectKey{
+ Name: string(roleType), Namespace: config.GetUsersNamespace(user.Name),
+ }, roleType, user)
+ }
+ if healthy {
+ continue
+ }
+ r.createRole(ctx, roleCondition, user, state, roleType)
+ }
+}
+
+func (r *UserReconciler) syncRoleBindingIfNeeded(
+ ctx context.Context,
+ user *userv1.User,
+ state *userReconcileState,
+) {
+ if r.cache != nil {
+ roleBinding := &rbacv1.RoleBinding{}
+ if err := r.cache.Get(ctx, client.ObjectKey{
+ Name: user.Name, Namespace: config.GetUsersNamespace(user.Name),
+ }, roleBinding); err == nil && roleBindingMatchesUser(roleBinding, user) {
+ r.markConditionReadyIfNeeded(user, roleBindingReadyCondition)
+ return
+ }
+ }
+ r.syncRoleBinding(ctx, user, state)
+}
+
+func (r *UserReconciler) syncClusterRoleBindingIfNeeded(
+ ctx context.Context,
+ user *userv1.User,
+ state *userReconcileState,
+) {
+ if user.Name != adminUserName {
+ return
+ }
+ if r.cache != nil {
+ binding := &rbacv1.ClusterRoleBinding{}
+ err := r.cache.Get(ctx, client.ObjectKey{Name: adminClusterRoleBindingName}, binding)
+ healthy := (!r.EnableAdminClusterAdmin && apierrors.IsNotFound(err)) ||
+ (r.EnableAdminClusterAdmin && err == nil && clusterRoleBindingMatchesUser(binding, user))
+ if healthy {
+ r.markConditionReadyIfNeeded(user, clusterRoleBindingReadyCondition)
+ return
+ }
+ }
+ r.syncClusterRoleBinding(ctx, user, state)
+}
+
+func (r *UserReconciler) syncNamespace(
+ ctx context.Context,
+ user *userv1.User,
+ state *userReconcileState,
+) {
+ namespaceConditionType := userv1.ConditionType("NamespaceSyncReady")
+ nsCondition := &userv1.Condition{
+ Type: namespaceConditionType,
+ Status: v1.ConditionTrue,
+ LastTransitionTime: metav1.Now(),
+ LastHeartbeatTime: metav1.Now(),
+ Reason: string(userv1.Ready),
+ Message: "sync namespace successfully",
+ }
+ condition := helper.GetCondition(user.Status.Conditions, nsCondition)
+ defer func() {
+ if helper.DiffCondition(condition, nsCondition) {
+ r.saveCondition(user, nsCondition.DeepCopy())
+ }
+ }()
+ if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ var change controllerutil.OperationResult
+ var err error
+ ns := &v1.Namespace{}
+ ns.Name = config.GetUsersNamespace(user.Name)
+ if err = r.Get(ctx, client.ObjectKeyFromObject(ns), ns); err != nil {
+ if !apierrors.IsNotFound(err) {
+ return err
+ }
+ }
+ var isCreated bool
+ if !ns.CreationTimestamp.IsZero() {
+ isCreated = true
+ r.Logger.V(1).
+ Info("define namespace User namespace is created", "isCreated", isCreated, "namespace", ns.Name)
+ }
+ if change, err = controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error {
+ if ns.Annotations == nil {
+ ns.Annotations = make(map[string]string)
+ }
+ if ns.Labels == nil {
+ ns.Labels = make(map[string]string)
+ }
+ ns.Annotations[userAnnotationCreatorKey] = user.Name
+ ns.Annotations[userAnnotationOwnerKey] = user.Annotations[userAnnotationOwnerKey]
+ ns.Labels = desiredNamespaceLabels(ns.Name, ns.Labels, r.EnableAdminClusterAdmin)
+ // add label for namespace to filter
+ ns.Labels[userLabelOwnerKey] = user.Annotations[userAnnotationOwnerKey]
+ ns.SetOwnerReferences([]metav1.OwnerReference{})
+ return controllerutil.SetControllerReference(user, ns, r.Scheme)
+ }); err != nil {
+ return fmt.Errorf("unable to create namespace by User: %w", err)
+ }
+ r.Logger.V(1).Info("create or update namespace by User", "OperationResult", change)
+ nsCondition.Message = fmt.Sprintf(
+ "sync namespace %s/%s successfully",
+ ns.Name,
+ ns.ResourceVersion,
+ )
+ return nil
+ }); err != nil {
+ state.recordSyncError(err)
+ helper.SetConditionError(nsCondition, "SyncUserError", err)
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "syncUser",
+ "Sync User namespace %s is error: %v",
+ user.Name,
+ err,
+ )
+ }
+}
+
+func (r *UserReconciler) createRole(
+ ctx context.Context,
+ condition *userv1.Condition,
+ user *userv1.User,
+ state *userReconcileState,
+ roleType userv1.RoleType,
+) {
+ if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ var change controllerutil.OperationResult
+ var err error
+ role := &rbacv1.Role{}
+ role.Name = string(roleType)
+ role.Namespace = config.GetUsersNamespace(user.Name)
+ role.Labels = map[string]string{}
+ if change, err = controllerutil.CreateOrUpdate(ctx, r.Client, role, func() error {
+ role.Annotations = map[string]string{
+ userAnnotationCreatorKey: user.Name,
+ userAnnotationOwnerKey: user.Annotations[userAnnotationOwnerKey],
+ }
+ role.Rules = config.GetUserRole(roleType)
+ return controllerutil.SetControllerReference(user, role, r.Scheme)
+ }); err != nil {
+ return fmt.Errorf("unable to create namespace role by User: %w", err)
+ }
+ r.Logger.V(1).Info("create or update namespace role by User", "OperationResult", change)
+ condition.Message = fmt.Sprintf(
+ "sync namespace role %s/%s successfully",
+ role.Name,
+ role.ResourceVersion,
+ )
+ return nil
+ }); err != nil {
+ state.recordSyncError(err)
+ helper.SetConditionError(condition, "SyncUserError", err)
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "syncUserRole",
+ "Sync User namespace role %s is error: %v",
+ user.Name,
+ err,
+ )
+ }
+}
+
+func (r *UserReconciler) syncRoleBinding(
+ ctx context.Context,
+ user *userv1.User,
+ state *userReconcileState,
+) {
+ roleBindingConditionType := userv1.ConditionType("RoleBindingSyncReady")
+ rbCondition := &userv1.Condition{
+ Type: roleBindingConditionType,
+ Status: v1.ConditionTrue,
+ LastTransitionTime: metav1.Now(),
+ LastHeartbeatTime: metav1.Now(),
+ Reason: string(userv1.Ready),
+ Message: "sync namespace role binding successfully",
+ }
+ condition := helper.GetCondition(user.Status.Conditions, rbCondition)
+ defer func() {
+ if helper.DiffCondition(condition, rbCondition) {
+ r.saveCondition(user, rbCondition.DeepCopy())
+ }
+ }()
+ if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ var change controllerutil.OperationResult
+ var err error
+ roleBinding := &rbacv1.RoleBinding{}
+ roleBinding.Name = user.Name
+ roleBinding.Namespace = config.GetUsersNamespace(user.Name)
+ roleBinding.Labels = map[string]string{}
+ if change, err = controllerutil.CreateOrUpdate(ctx, r.Client, roleBinding, func() error {
+ roleBinding.Annotations = map[string]string{
+ userAnnotationCreatorKey: user.Name,
+ userAnnotationOwnerKey: user.Annotations[userAnnotationOwnerKey],
+ }
+ roleBinding.RoleRef = rbacv1.RoleRef{
+ APIGroup: rbacv1.GroupName,
+ Kind: "Role",
+ Name: string(userv1.OwnerRoleType),
+ }
+ roleBinding.Subjects = config.GetUsersSubject(user.Name)
+ return controllerutil.SetControllerReference(user, roleBinding, r.Scheme)
+ }); err != nil {
+ return fmt.Errorf("unable to create namespace role binding by User: %w", err)
+ }
+ r.Logger.V(1).
+ Info("create or update namespace role binding by User", "OperationResult", change)
+ rbCondition.Message = fmt.Sprintf(
+ "sync namespace role binding %s/%s successfully",
+ roleBinding.Name,
+ roleBinding.ResourceVersion,
+ )
+ return nil
+ }); err != nil {
+ state.recordSyncError(err)
+ helper.SetConditionError(rbCondition, "SyncUserError", err)
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "syncUserRoleBinding",
+ "Sync User namespace role binding %s is error: %v",
+ user.Name,
+ err,
+ )
+ }
+}
+
+func (r *UserReconciler) syncClusterRoleBinding(
+ ctx context.Context,
+ user *userv1.User,
+ state *userReconcileState,
+) {
+ if user.Name != adminUserName {
+ return
+ }
+ roleBindingConditionType := userv1.ConditionType("ClusterRoleBindingSyncReady")
+ rbCondition := &userv1.Condition{
+ Type: roleBindingConditionType,
+ Status: v1.ConditionTrue,
+ LastTransitionTime: metav1.Now(),
+ LastHeartbeatTime: metav1.Now(),
+ Reason: string(userv1.Ready),
+ Message: "sync admin role binding successfully",
+ }
+ condition := helper.GetCondition(user.Status.Conditions, rbCondition)
+ defer func() {
+ if helper.DiffCondition(condition, rbCondition) {
+ r.saveCondition(user, rbCondition.DeepCopy())
+ }
+ }()
+ if !r.EnableAdminClusterAdmin {
+ clusterRoleBinding := &rbacv1.ClusterRoleBinding{}
+ clusterRoleBinding.Name = adminClusterRoleBindingName
+ if err := r.Delete(ctx, clusterRoleBinding); err != nil && !apierrors.IsNotFound(err) {
+ err = fmt.Errorf("unable to remove disabled admin cluster role binding: %w", err)
+ state.recordSyncError(err)
+ helper.SetConditionError(rbCondition, "SyncUserError", err)
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "syncUserClusterRoleBinding",
+ "Remove User admin cluster role binding %s is error: %v",
+ user.Name,
+ err,
+ )
+ return
+ }
+ rbCondition.Message = "admin cluster role binding disabled"
+ return
+ }
+ if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ var change controllerutil.OperationResult
+ var err error
+ clusterRoleBinding := &rbacv1.ClusterRoleBinding{}
+ clusterRoleBinding.Name = adminClusterRoleBindingName
+ clusterRoleBinding.Labels = map[string]string{}
+ if change, err = controllerutil.CreateOrUpdate(
+ ctx,
+ r.Client,
+ clusterRoleBinding,
+ func() error {
+ clusterRoleBinding.Annotations = map[string]string{
+ userAnnotationCreatorKey: user.Name,
+ userAnnotationOwnerKey: user.Annotations[userAnnotationOwnerKey],
+ }
+ clusterRoleBinding.RoleRef = rbacv1.RoleRef{
+ APIGroup: rbacv1.GroupName,
+ Kind: "ClusterRole",
+ Name: "cluster-admin",
+ }
+ clusterRoleBinding.Subjects = config.GetUsersSubject(user.Name)
+ return controllerutil.SetControllerReference(user, clusterRoleBinding, r.Scheme)
+ },
+ ); err != nil {
+ return fmt.Errorf(
+ "unable to create namespace admin cluster role binding by User: %w",
+ err,
+ )
+ }
+ r.Logger.V(1).
+ Info("create or update namespace admin cluster role binding by User", "OperationResult", change)
+ rbCondition.Message = fmt.Sprintf(
+ "sync namespace admin cluster role binding %s/%s successfully",
+ clusterRoleBinding.Name,
+ clusterRoleBinding.ResourceVersion,
+ )
+ return nil
+ }); err != nil {
+ state.recordSyncError(err)
+ helper.SetConditionError(rbCondition, "SyncUserError", err)
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "syncUserClusterRoleBinding",
+ "Sync User admin cluster role binding %s is error: %v",
+ user.Name,
+ err,
+ )
+ }
+}
+
+func (r *UserReconciler) saveCondition(user *userv1.User, condition *userv1.Condition) {
+ user.Status.Conditions = helper.UpdateCondition(user.Status.Conditions, *condition)
+}
+
+func (r *UserReconciler) syncServiceAccount(
+ ctx context.Context,
+ user *userv1.User,
+ state *userReconcileState,
+) {
+ saConditionType := userv1.ConditionType("ServiceAccountSyncReady")
+ saCondition := &userv1.Condition{
+ Type: saConditionType,
+ Status: v1.ConditionTrue,
+ LastTransitionTime: metav1.Now(),
+ LastHeartbeatTime: metav1.Now(),
+ Reason: string(userv1.Ready),
+ Message: "sync namespace sa successfully",
+ }
+ condition := helper.GetCondition(user.Status.Conditions, saCondition)
+ defer func() {
+ if helper.DiffCondition(condition, saCondition) {
+ r.saveCondition(user, saCondition.DeepCopy())
+ }
+ }()
+ state.serviceAccount = nil
+ if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ var change controllerutil.OperationResult
+ var err error
+ sa := &v1.ServiceAccount{}
+ sa.Name = user.Name
+ sa.Namespace = config.GetUserSystemNamespace()
+ sa.Labels = map[string]string{}
+
+ if err = r.Get(ctx, client.ObjectKey{
+ Namespace: config.GetUserSystemNamespace(),
+ Name: user.Name,
+ }, sa); err != nil && !apierrors.IsNotFound(err) {
+ return err
+ }
+
+ if change, err = controllerutil.CreateOrUpdate(ctx, r.Client, sa, func() error {
+ sa.Annotations = map[string]string{
+ userAnnotationCreatorKey: user.Name,
+ userAnnotationOwnerKey: user.Annotations[userAnnotationOwnerKey],
+ }
+ return controllerutil.SetControllerReference(user, sa, r.Scheme)
+ }); err != nil {
+ return fmt.Errorf("unable to create namespace sa by User: %w", err)
+ }
+ r.Logger.V(1).Info("create or update namespace sa by User", "OperationResult", change)
+ saCondition.Message = fmt.Sprintf(
+ "sync namespace sa %s/%s successfully",
+ sa.Name,
+ sa.ResourceVersion,
+ )
+ state.serviceAccount = sa
+ return nil
+ }); err != nil {
+ helper.SetConditionError(saCondition, "SyncUserError", err)
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "syncUserServiceAccount",
+ "Sync User namespace sa %s is error: %v",
+ user.Name,
+ err,
+ )
+ }
+}
+
+func (r *UserReconciler) syncKubeConfig(
+ ctx context.Context,
+ user *userv1.User,
+ state *userReconcileState,
+) {
+ userCondition := &userv1.Condition{
+ Type: kubeConfigReadyCondition,
+ Status: v1.ConditionTrue,
+ LastTransitionTime: metav1.Now(),
+ LastHeartbeatTime: metav1.Now(),
+ Reason: string(userv1.Ready),
+ Message: "sync kube config successfully",
+ }
+ condition := helper.GetCondition(user.Status.Conditions, userCondition)
+ defer func() {
+ if helper.DiffCondition(condition, userCondition) {
+ r.saveCondition(user, userCondition.DeepCopy())
+ }
+ }()
+ sa := state.serviceAccount
+ if sa == nil {
+ saErr := errors.New("serviceAccount not found")
+ state.recordSyncError(saErr)
+ helper.SetConditionError(
+ userCondition,
+ "SyncUserError",
+ saErr,
+ )
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "syncKubeConfig",
+ "Sync User namespace kubeconfig %s is error: %v",
+ user.Name,
+ saErr,
+ )
+ return
+ }
+ // Keep an owned copy because kubeconfig generation may update metadata on it.
+ sa = sa.DeepCopy()
+ if r.shouldRotateKubeConfig(user) {
+ if err := r.deleteBoundTokenSecret(ctx, user); err != nil {
+ state.recordSyncError(err)
+ helper.SetConditionError(userCondition, "SyncKubeConfigError", err)
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "syncKubeConfig",
+ "Delete bound token secret %s is error: %v",
+ user.Name,
+ err,
+ )
+ return
+ }
+ }
+ tokenRequestConfig := kubeconfig.NewConfig(user.Name, "", user.Spec.CSRExpirationSeconds).
+ WithServiceAccountConfig(config.GetUserSystemNamespace(), sa)
+ if r.shouldRotateKubeConfig(user) {
+ tokenRequestConfig = tokenRequestConfig.WithForceNewSecret()
+ }
+ apiConfig, tokenExpiresAt, err := tokenRequestConfig.ApplyWithTokenRequest(
+ ctx,
+ r.config,
+ r.Client,
+ )
+ if err != nil {
+ state.recordSyncError(err)
+ helper.SetConditionError(userCondition, "SyncKubeConfigError", err)
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "syncKubeConfig",
+ "Sync KubeConfig apply %s is error: %v",
+ user.Name,
+ err,
+ )
+ return
+ }
+ if apiConfig == nil {
+ configErr := errors.New("api.config is nil")
+ state.recordSyncError(configErr)
+ helper.SetConditionError(
+ userCondition,
+ "SyncKubeConfigError",
+ configErr,
+ )
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "syncKubeConfig",
+ "Sync KubeConfig apply %s is error: %v",
+ user.Name,
+ configErr,
+ )
+ return
+ }
+ kubeData, err := clientcmd.Write(*apiConfig)
+ if err != nil {
+ state.recordSyncError(err)
+ helper.SetConditionError(userCondition, "OutputKubeConfigError", err)
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "syncKubeConfig",
+ "Output KubeConfig apply %s is error: %v",
+ user.Name,
+ err,
+ )
+ return
+ }
+ keepSecretName := ""
+ if len(sa.Secrets) > 0 {
+ keepSecretName = sa.Secrets[0].Name
+ }
+ secretUID := ""
+ if keepSecretName != "" {
+ boundSecret := &v1.Secret{}
+ if err := r.Get(ctx, client.ObjectKey{
+ Name: keepSecretName, Namespace: config.GetUserSystemNamespace(),
+ }, boundSecret); err != nil {
+ state.recordSyncError(err)
+ helper.SetConditionError(userCondition, "SyncKubeConfigError", err)
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "syncKubeConfig",
+ "Get bound token secret %s is error: %v",
+ user.Name,
+ err,
+ )
+ return
+ }
+ secretUID = string(boundSecret.UID)
+ }
+ state.tokenExpirationDeadline = &tokenExpiresAt
+ refreshAt := metav1.NewTime(time.Now().Add(time.Until(tokenExpiresAt.Time) * 8 / 10))
+ user.Status.KubeConfigRefreshAt = &refreshAt
+ user.Status.ObservedKubeConfigSecretUID = secretUID
+ if r.shouldRotateKubeConfig(user) {
+ user.Status.ObservedKubeConfigRotateAt = user.Spec.KubeConfigRotateAt
+ }
+ user.Status.KubeConfig = string(kubeData)
+ userCondition.Message = "renew sync kube config successfully hash " + hash.HashToString(
+ user.Status.KubeConfig,
+ )
+ state.currentSecretName = keepSecretName
+ state.kubeConfigSynced = true
+ state.cleanupLegacySecrets = keepSecretName != ""
+}
+
+func (r *UserReconciler) deleteBoundTokenSecret(ctx context.Context, user *userv1.User) error {
+ secretName := kubeconfig.TokenSecretName(user.Name)
+ sa := &v1.ServiceAccount{}
+ if err := r.Get(ctx, client.ObjectKey{
+ Namespace: config.GetUserSystemNamespace(),
+ Name: user.Name,
+ }, sa); err != nil {
+ if !apierrors.IsNotFound(err) {
+ return fmt.Errorf("failed to get service account for bound token secret: %w", err)
+ }
+ } else if len(sa.Secrets) > 0 && sa.Secrets[0].Name != "" {
+ secretName = sa.Secrets[0].Name
+ }
+
+ secret := &v1.Secret{}
+ secret.Name = secretName
+ secret.Namespace = config.GetUserSystemNamespace()
+ if err := r.Delete(ctx, secret); err != nil && !apierrors.IsNotFound(err) {
+ return fmt.Errorf("failed to delete bound token secret: %w", err)
+ }
+ return nil
+}
+
+func syncReNewConfig(user *userv1.User) (*api.Config, *string, error) {
+ var apiConfig *api.Config
+ var err error
+ var event *string
+ if user.Status.KubeConfig != "" &&
+ csrExpirationStatusMatches(
+ user.Spec.CSRExpirationSeconds,
+ user.Status.ObservedCSRExpirationSeconds,
+ ) {
+ apiConfig, err = clientcmd.Load([]byte(user.Status.KubeConfig))
+ if err != nil {
+ return nil, nil, err
+ }
+ for _, ctx := range apiConfig.Contexts {
+ if ctx.Namespace == "" {
+ apiConfig = nil
+ ev := fmt.Sprintf("User %s Namespace is empty", user.Name)
+ event = &ev
+ return apiConfig, event, err
+ }
+ }
+ if info, ok := apiConfig.AuthInfos[user.Name]; ok {
+ if info != nil {
+ if info.Token == "" {
+ apiConfig = nil
+ ev := fmt.Sprintf("User %s Token is empty", user.Name)
+ event = &ev
+ return apiConfig, event, err
+ }
+ if info.ClientCertificateData == nil {
+ return apiConfig, event, err
+ }
+ cert, err := kubeconfig.DecodeX509CertificateBytes(info.ClientCertificateData)
+ if err != nil {
+ return nil, nil, err
+ }
+ if cert.NotAfter.Before(time.Now()) {
+ apiConfig = nil
+ ev := fmt.Sprintf("ClientCertificateData %s is expired", user.Name)
+ event = &ev
+ }
+ }
+ }
+ }
+ return apiConfig, event, err
+}
+
+func (r *UserReconciler) syncFinalStatus(
+ _ context.Context,
+ user *userv1.User,
+ _ *userReconcileState,
+) {
+ condition := &userv1.Condition{
+ Type: userv1.Ready,
+ Status: v1.ConditionTrue,
+ LastTransitionTime: metav1.Now(),
+ LastHeartbeatTime: metav1.Now(),
+ Reason: string(userv1.Ready),
+ Message: "User is available now",
+ }
+ defer r.saveCondition(user, condition)
+
+ if !helper.IsConditionsTrue(user.Status.Conditions) {
+ condition.LastHeartbeatTime = metav1.Now()
+ condition.Status = v1.ConditionFalse
+ condition.Reason = "Not" + string(userv1.Ready)
+ condition.Message = "User is not available now"
+ user.Status.Phase = userv1.UserUnknown
+ } else {
+ user.Status.Phase = userv1.UserActive
+ }
+}
+
+func (r *UserReconciler) shouldRotateKubeConfig(user *userv1.User) bool {
+ return userKubeConfigNeedsRotation(user)
+}
+
+func userKubeConfigNeedsRotation(user *userv1.User) bool {
+ if user == nil {
+ return false
+ }
+ if user.Spec.KubeConfigRotateAt == nil {
+ return false
+ }
+ if user.Status.ObservedKubeConfigRotateAt == nil {
+ return true
+ }
+ return !user.Spec.KubeConfigRotateAt.Equal(user.Status.ObservedKubeConfigRotateAt)
+}
+
+func kubeConfigSyncFailed(user *userv1.User) bool {
+ if user == nil {
+ return false
+ }
+ for _, condition := range user.Status.Conditions {
+ if condition.Type == kubeConfigReadyCondition {
+ return condition.Status == v1.ConditionFalse
+ }
+ }
+ return false
+}
+
+func (r *UserReconciler) updateStatus(
+ ctx context.Context,
+ user *userv1.User,
+ originalStatus *userv1.UserStatus,
+) error {
+ original := user.DeepCopy()
+ original.Status = *originalStatus.DeepCopy()
+ return r.Client.Status().Patch(ctx, user, client.MergeFrom(original))
+}
+
+func (r *UserReconciler) handleLicenseLimit(
+ ctx context.Context,
+ user *userv1.User,
+ originalStatus *userv1.UserStatus,
+) (bool, error) {
+ if !r.isNewUser(user) {
+ user.Status.Conditions = helper.DeleteCondition(
+ user.Status.Conditions,
+ licenseLimitedCondition,
+ )
+ return false, nil
+ }
+
+ if r.userCounter == nil || !r.userCounter.Initialized() {
+ return false, errors.New("user count cache is not initialized")
+ }
+ userCount := r.userCounter.CountExcluding(user.Name)
+ if licensegate.AllowNewUser(userCount) {
+ user.Status.Conditions = helper.DeleteCondition(
+ user.Status.Conditions,
+ licenseLimitedCondition,
+ )
+ return false, nil
+ }
+ limitCondition := &userv1.Condition{
+ Type: licenseLimitedCondition,
+ Status: v1.ConditionFalse,
+ LastTransitionTime: metav1.Now(),
+ LastHeartbeatTime: metav1.Now(),
+ Reason: "LicenseLimitExceeded",
+ Message: licensegate.LimitMessage(),
+ }
+ user.Status.Phase = userv1.UserPending
+ user.Status.Conditions = helper.UpdateCondition(user.Status.Conditions, *limitCondition)
+ if err := r.updateStatus(
+ ctx,
+ user,
+ originalStatus,
+ ); err != nil {
+ return false, err
+ }
+ r.Recorder.Eventf(
+ user,
+ v1.EventTypeWarning,
+ "LicenseLimitExceeded",
+ "%s: %d",
+ licensegate.LimitMessage(),
+ licensegate.UserLimit(),
+ )
+ return true, nil
+}
+
+func (r *UserReconciler) isNewUser(user *userv1.User) bool {
+ if user == nil || user.Status.ObservedGeneration != 0 {
+ return false
+ }
+ for _, condition := range user.Status.Conditions {
+ if condition.Type != licenseLimitedCondition {
+ return false
+ }
+ }
+ return true
+}
+
+func nextKubeConfigRequeueDuration(
+ user *userv1.User,
+ state *userReconcileState,
+) time.Duration {
+ if state != nil && state.tokenExpirationDeadline != nil &&
+ !state.tokenExpirationDeadline.IsZero() {
+ refreshDuration := time.Until(state.tokenExpirationDeadline.Time) * 8 / 10
+ if refreshDuration <= 0 {
+ return time.Second
+ }
+ return refreshDuration
+ }
+ if user == nil || user.Status.KubeConfigRefreshAt == nil ||
+ user.Status.KubeConfigRefreshAt.IsZero() {
+ return 0
+ }
+ refreshDuration := time.Until(user.Status.KubeConfigRefreshAt.Time)
+ if refreshDuration <= 0 {
+ return time.Second
+ }
+ return refreshDuration
+}
+
+func (r *UserReconciler) licenseToUserRequests(
+ ctx context.Context,
+ obj client.Object,
+) []ctrl.Request {
+ userList := &userv1.UserList{}
+ if err := r.cache.List(ctx, userList); err != nil {
+ r.Logger.Error(err, "list users for license change failed")
+ return nil
+ }
+ requests := make([]ctrl.Request, 0, len(userList.Items))
+ for i := range userList.Items {
+ requests = append(
+ requests,
+ ctrl.Request{NamespacedName: client.ObjectKeyFromObject(&userList.Items[i])},
+ )
+ }
+ return requests
+}
diff --git a/controllers/user/controllers/user_controller_reconcile_test.go b/controllers/user/controllers/user_controller_reconcile_test.go
new file mode 100644
index 000000000000..40b48edbd4e8
--- /dev/null
+++ b/controllers/user/controllers/user_controller_reconcile_test.go
@@ -0,0 +1,699 @@
+// Copyright 2026 labring.
+//
+// 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 controllers
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ usercache "github.com/labring/sealos/controllers/user/controllers/cache"
+ "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ corev1 "k8s.io/api/core/v1"
+ rbacv1 "k8s.io/api/rbac/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/tools/record"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func reconcileTestScheme(t *testing.T) *runtime.Scheme {
+ t.Helper()
+ scheme := runtime.NewScheme()
+ if err := corev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add core scheme: %v", err)
+ }
+ if err := rbacv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add RBAC scheme: %v", err)
+ }
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add user scheme: %v", err)
+ }
+ return scheme
+}
+
+func reconcileTestUser(name string) *userv1.User {
+ return &userv1.User{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ UID: types.UID("uid-" + name),
+ Annotations: map[string]string{
+ userv1.UserAnnotationOwnerKey: "owner",
+ },
+ },
+ }
+}
+
+func reconcileTestOwnerReference(user *userv1.User) metav1.OwnerReference {
+ controller := true
+ return metav1.OwnerReference{
+ APIVersion: userv1.GroupVersion.String(),
+ Kind: "User",
+ Name: user.Name,
+ UID: user.UID,
+ Controller: &controller,
+ }
+}
+
+func reconcileTestRole(user *userv1.User, roleType userv1.RoleType) *rbacv1.Role {
+ return &rbacv1.Role{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: string(roleType),
+ Namespace: config.GetUsersNamespace(user.Name),
+ Annotations: map[string]string{
+ userv1.UserAnnotationCreatorKey: user.Name,
+ userv1.UserAnnotationOwnerKey: "owner",
+ },
+ OwnerReferences: []metav1.OwnerReference{reconcileTestOwnerReference(user)},
+ },
+ Rules: config.GetUserRole(roleType),
+ }
+}
+
+func TestRoleBindingMatchesUserUsesProjectedSpecHash(t *testing.T) {
+ user := reconcileTestUser("alice")
+ binding := &rbacv1.RoleBinding{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: user.Name,
+ Namespace: config.GetUsersNamespace(user.Name),
+ Annotations: map[string]string{
+ userv1.UserAnnotationCreatorKey: user.Name,
+ userv1.UserAnnotationOwnerKey: "owner",
+ },
+ OwnerReferences: []metav1.OwnerReference{reconcileTestOwnerReference(user)},
+ },
+ }
+ binding.Annotations[usercache.RoleBindingSpecHashAnnotation] = usercache.RoleBindingSpecHash(
+ rbacv1.RoleRef{
+ APIGroup: rbacv1.GroupName,
+ Kind: "Role",
+ Name: string(userv1.OwnerRoleType),
+ },
+ config.GetUsersSubject(user.Name),
+ )
+ if !roleBindingMatchesUser(binding, user) {
+ t.Fatal("projected role binding hash was not accepted")
+ }
+ binding.Annotations[usercache.RoleBindingSpecHashAnnotation] = "drifted"
+ if roleBindingMatchesUser(binding, user) {
+ t.Fatal("drifted projected role binding hash was accepted")
+ }
+}
+
+func TestResourceSyncIfNeededRepairsMissingResources(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ userName string
+ adminRole bool
+ objects func(*userv1.User) []client.Object
+ sync func(context.Context, *UserReconciler, *userv1.User, *userReconcileState)
+ verify func(*testing.T, client.Client, *userv1.User)
+ }{
+ {
+ name: "namespace",
+ userName: "alice",
+ sync: func(ctx context.Context, r *UserReconciler, user *userv1.User, state *userReconcileState) {
+ r.syncNamespaceIfNeeded(ctx, user, state)
+ },
+ verify: func(t *testing.T, cli client.Client, user *userv1.User) {
+ t.Helper()
+ ns := &corev1.Namespace{}
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKey{Name: config.GetUsersNamespace(user.Name)},
+ ns,
+ ); err != nil {
+ t.Fatalf("get repaired namespace: %v", err)
+ }
+ if !namespaceMatchesUser(ns, user, false) {
+ t.Fatal("repaired namespace does not match user")
+ }
+ },
+ },
+ {
+ name: "service account",
+ userName: "alice",
+ sync: func(ctx context.Context, r *UserReconciler, user *userv1.User, state *userReconcileState) {
+ r.syncServiceAccountIfNeeded(ctx, user, state)
+ },
+ verify: func(t *testing.T, cli client.Client, user *userv1.User) {
+ t.Helper()
+ sa := &corev1.ServiceAccount{}
+ if err := cli.Get(context.Background(), client.ObjectKey{
+ Name: user.Name, Namespace: config.GetUserSystemNamespace(),
+ }, sa); err != nil {
+ t.Fatalf("get repaired service account: %v", err)
+ }
+ if !metadataMatchesUserResource(sa, user) {
+ t.Fatal("repaired service account does not match user")
+ }
+ },
+ },
+ {
+ name: "missing developer role",
+ userName: "alice",
+ objects: func(user *userv1.User) []client.Object {
+ return []client.Object{
+ reconcileTestRole(user, userv1.OwnerRoleType),
+ reconcileTestRole(user, userv1.ManagerRoleType),
+ }
+ },
+ sync: func(ctx context.Context, r *UserReconciler, user *userv1.User, state *userReconcileState) {
+ r.syncRolesIfNeeded(ctx, user, state)
+ },
+ verify: func(t *testing.T, cli client.Client, user *userv1.User) {
+ t.Helper()
+ role := &rbacv1.Role{}
+ if err := cli.Get(context.Background(), client.ObjectKey{
+ Name: string(userv1.DeveloperRoleType),
+ Namespace: config.GetUsersNamespace(user.Name),
+ }, role); err != nil {
+ t.Fatalf("get repaired developer role: %v", err)
+ }
+ if !roleMatchesUser(
+ context.Background(),
+ cli,
+ client.ObjectKeyFromObject(role),
+ userv1.DeveloperRoleType,
+ user,
+ ) {
+ t.Fatal("repaired developer role does not match user")
+ }
+ },
+ },
+ {
+ name: "drifted developer role",
+ userName: "alice",
+ objects: func(user *userv1.User) []client.Object {
+ role := reconcileTestRole(user, userv1.DeveloperRoleType)
+ role.Rules = []rbacv1.PolicyRule{{
+ Resources: []string{"pods"},
+ Verbs: []string{"get"},
+ }}
+ return []client.Object{
+ reconcileTestRole(user, userv1.OwnerRoleType),
+ reconcileTestRole(user, userv1.ManagerRoleType),
+ role,
+ }
+ },
+ sync: func(ctx context.Context, r *UserReconciler, user *userv1.User, state *userReconcileState) {
+ r.syncRolesIfNeeded(ctx, user, state)
+ },
+ verify: func(t *testing.T, cli client.Client, user *userv1.User) {
+ t.Helper()
+ role := &rbacv1.Role{}
+ if err := cli.Get(context.Background(), client.ObjectKey{
+ Name: string(userv1.DeveloperRoleType),
+ Namespace: config.GetUsersNamespace(user.Name),
+ }, role); err != nil {
+ t.Fatalf("get repaired developer role: %v", err)
+ }
+ if !roleMatchesUser(
+ context.Background(),
+ cli,
+ client.ObjectKeyFromObject(role),
+ userv1.DeveloperRoleType,
+ user,
+ ) {
+ t.Fatal("drifted developer role was not repaired")
+ }
+ },
+ },
+ {
+ name: "role binding",
+ userName: "alice",
+ sync: func(ctx context.Context, r *UserReconciler, user *userv1.User, state *userReconcileState) {
+ r.syncRoleBindingIfNeeded(ctx, user, state)
+ },
+ verify: func(t *testing.T, cli client.Client, user *userv1.User) {
+ t.Helper()
+ binding := &rbacv1.RoleBinding{}
+ if err := cli.Get(context.Background(), client.ObjectKey{
+ Name: user.Name, Namespace: config.GetUsersNamespace(user.Name),
+ }, binding); err != nil {
+ t.Fatalf("get repaired role binding: %v", err)
+ }
+ if !roleBindingMatchesUser(binding, user) {
+ t.Fatal("repaired role binding does not match user")
+ }
+ },
+ },
+ {
+ name: "admin cluster role binding",
+ userName: adminUserName,
+ adminRole: true,
+ sync: func(ctx context.Context, r *UserReconciler, user *userv1.User, state *userReconcileState) {
+ r.syncClusterRoleBindingIfNeeded(ctx, user, state)
+ },
+ verify: func(t *testing.T, cli client.Client, user *userv1.User) {
+ t.Helper()
+ binding := &rbacv1.ClusterRoleBinding{}
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKey{Name: adminClusterRoleBindingName},
+ binding,
+ ); err != nil {
+ t.Fatalf("get repaired admin cluster role binding: %v", err)
+ }
+ if !clusterRoleBindingMatchesUser(binding, user) {
+ t.Fatal("repaired admin cluster role binding does not match user")
+ }
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ scheme := reconcileTestScheme(t)
+ user := reconcileTestUser(tt.userName)
+ var objects []client.Object
+ if tt.objects != nil {
+ objects = tt.objects(user)
+ }
+ cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
+ r := &UserReconciler{
+ Client: cli,
+ Scheme: scheme,
+ Recorder: record.NewFakeRecorder(32),
+ EnableAdminClusterAdmin: tt.adminRole,
+ cache: cli,
+ }
+
+ tt.sync(context.Background(), r, user, &userReconcileState{})
+ tt.verify(t, cli, user)
+ })
+ }
+}
+
+func benchmarkProcessedUser(name string, refreshAt time.Time) *userv1.User {
+ return &userv1.User{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ UID: types.UID("uid-" + name),
+ Generation: 1,
+ Annotations: map[string]string{
+ userv1.UserAnnotationOwnerKey: "owner",
+ },
+ },
+ Spec: userv1.UserSpec{
+ CSRExpirationSeconds: userv1.DefaultCSRExpirationSeconds,
+ },
+ Status: userv1.UserStatus{
+ Phase: userv1.UserActive,
+ ObservedGeneration: 1,
+ ObservedCSRExpirationSeconds: userv1.DefaultCSRExpirationSeconds,
+ KubeConfigRefreshAt: &metav1.Time{Time: refreshAt},
+ ObservedKubeConfigSecretUID: "secret-" + name,
+ Conditions: []userv1.Condition{
+ {Type: userv1.Initialized, Status: corev1.ConditionTrue},
+ {
+ Type: namespaceSyncReadyCondition,
+ Status: corev1.ConditionTrue,
+ Reason: string(userv1.Ready),
+ Message: "sync namespace successfully",
+ },
+ {
+ Type: serviceAccountReadyCondition,
+ Status: corev1.ConditionTrue,
+ Reason: string(userv1.Ready),
+ Message: "sync namespace sa successfully",
+ },
+ {
+ Type: kubeConfigReadyCondition,
+ Status: corev1.ConditionTrue,
+ Reason: string(userv1.Ready),
+ Message: "sync kube config successfully",
+ },
+ {
+ Type: roleSyncReadyCondition,
+ Status: corev1.ConditionTrue,
+ Reason: string(userv1.Ready),
+ Message: "sync namespace role successfully",
+ },
+ {
+ Type: roleBindingReadyCondition,
+ Status: corev1.ConditionTrue,
+ Reason: string(userv1.Ready),
+ Message: "sync namespace role binding successfully",
+ },
+ {
+ Type: userv1.Ready,
+ Status: corev1.ConditionTrue,
+ Reason: string(userv1.Ready),
+ Message: "User is available now",
+ },
+ },
+ },
+ }
+}
+
+// healthyStartupCache returns only the fields used by the fast-path checks,
+// matching the projected objects held by the production cache.
+type healthyStartupCache struct{}
+
+func (healthyStartupCache) Get(
+ _ context.Context,
+ key client.ObjectKey,
+ obj client.Object,
+ _ ...client.GetOption,
+) error {
+ owner := "owner"
+ var userName string
+ switch typed := obj.(type) {
+ case *metav1.PartialObjectMetadata:
+ if typed.GroupVersionKind().Kind == "Secret" {
+ userName = strings.TrimPrefix(key.Name, "token-")
+ typed.Name = key.Name
+ typed.Namespace = key.Namespace
+ typed.UID = types.UID("secret-" + userName)
+ typed.Annotations = map[string]string{corev1.ServiceAccountNameKey: userName}
+ typed.OwnerReferences = []metav1.OwnerReference{benchmarkOwnerReference(userName)}
+ return nil
+ }
+ userName = config.GetUserNameByNamespace(key.Name)
+ typed.Name = key.Name
+ typed.Annotations = map[string]string{
+ userv1.UserAnnotationCreatorKey: userName,
+ userv1.UserAnnotationOwnerKey: owner,
+ }
+ typed.Labels = config.SetPodSecurity(map[string]string{
+ userv1.UserLabelOwnerKey: owner,
+ })
+ typed.OwnerReferences = []metav1.OwnerReference{benchmarkOwnerReference(userName)}
+ return nil
+ case *corev1.ServiceAccount:
+ userName = key.Name
+ typed.Name = key.Name
+ typed.Namespace = key.Namespace
+ typed.Annotations = benchmarkAnnotations(userName, owner)
+ typed.OwnerReferences = []metav1.OwnerReference{benchmarkOwnerReference(userName)}
+ typed.Secrets = []corev1.ObjectReference{{Name: "token-" + userName}}
+ return nil
+ case *corev1.Secret:
+ userName = strings.TrimPrefix(key.Name, "token-")
+ typed.Name = key.Name
+ typed.Namespace = key.Namespace
+ typed.UID = types.UID("secret-" + userName)
+ typed.Annotations = map[string]string{corev1.ServiceAccountNameKey: userName}
+ typed.OwnerReferences = []metav1.OwnerReference{benchmarkOwnerReference(userName)}
+ return nil
+ case *rbacv1.Role:
+ userName = config.GetUserNameByNamespace(key.Namespace)
+ typed.Name = key.Name
+ typed.Namespace = key.Namespace
+ typed.Annotations = benchmarkAnnotations(userName, owner)
+ typed.OwnerReferences = []metav1.OwnerReference{benchmarkOwnerReference(userName)}
+ typed.Rules = config.GetUserRole(userv1.RoleType(key.Name))
+ return nil
+ case *rbacv1.RoleBinding:
+ userName = config.GetUserNameByNamespace(key.Namespace)
+ typed.Name = key.Name
+ typed.Namespace = key.Namespace
+ typed.Annotations = benchmarkAnnotations(userName, owner)
+ typed.OwnerReferences = []metav1.OwnerReference{benchmarkOwnerReference(userName)}
+ typed.RoleRef = rbacv1.RoleRef{
+ APIGroup: rbacv1.GroupName,
+ Kind: "Role",
+ Name: string(userv1.OwnerRoleType),
+ }
+ typed.Subjects = config.GetUsersSubject(userName)
+ return nil
+ default:
+ return fmt.Errorf("unexpected cache object type %T", obj)
+ }
+}
+
+func (healthyStartupCache) List(context.Context, client.ObjectList, ...client.ListOption) error {
+ return errors.New("unexpected cache list call")
+}
+
+func benchmarkOwnerReference(userName string) metav1.OwnerReference {
+ controller := true
+ return metav1.OwnerReference{
+ APIVersion: userv1.GroupVersion.String(),
+ Kind: "User",
+ Name: userName,
+ UID: types.UID("uid-" + userName),
+ Controller: &controller,
+ }
+}
+
+func benchmarkAnnotations(userName, owner string) map[string]string {
+ return map[string]string{
+ userv1.UserAnnotationCreatorKey: userName,
+ userv1.UserAnnotationOwnerKey: owner,
+ }
+}
+
+func TestReconcileSkipsHealthyCachedUserResources(t *testing.T) {
+ t.Parallel()
+ user := benchmarkProcessedUser("alice", time.Now().Add(time.Hour))
+ originalStatus := user.Status.DeepCopy()
+ r := &UserReconciler{
+ cache: healthyStartupCache{},
+ minRequeueDuration: time.Minute,
+ }
+
+ if _, err := r.reconcile(context.Background(), user); err != nil {
+ t.Fatalf("reconcile healthy cached user: %v", err)
+ }
+ if !reflect.DeepEqual(user.Status, *originalStatus) {
+ t.Fatalf(
+ "healthy cached user status changed: before=%#v after=%#v",
+ originalStatus,
+ user.Status,
+ )
+ }
+}
+
+func TestReconcileSkipsHealthyStatusWithoutLegacyConditions(t *testing.T) {
+ t.Parallel()
+ user := benchmarkProcessedUser("legacy-user", time.Now().Add(time.Hour))
+ filteredConditions := make([]userv1.Condition, 0, len(user.Status.Conditions)-2)
+ for _, condition := range user.Status.Conditions {
+ if condition.Type != kubeConfigReadyCondition && condition.Type != roleSyncReadyCondition {
+ filteredConditions = append(filteredConditions, condition)
+ }
+ }
+ user.Status.Conditions = filteredConditions
+ originalStatus := user.Status.DeepCopy()
+ r := &UserReconciler{
+ cache: healthyStartupCache{},
+ minRequeueDuration: time.Minute,
+ }
+ if _, err := r.reconcile(context.Background(), user); err != nil {
+ t.Fatalf("reconcile healthy legacy user: %v", err)
+ }
+ if !reflect.DeepEqual(user.Status, *originalStatus) {
+ t.Fatalf(
+ "missing conditions were restored for healthy resources: before=%#v after=%#v",
+ originalStatus,
+ user.Status,
+ )
+ }
+}
+
+func TestKubeConfigSyncFailedConditionForcesRetry(t *testing.T) {
+ t.Parallel()
+ user := benchmarkProcessedUser("failed-kubeconfig", time.Now().Add(time.Hour))
+ for i := range user.Status.Conditions {
+ if user.Status.Conditions[i].Type == kubeConfigReadyCondition {
+ user.Status.Conditions[i].Status = corev1.ConditionFalse
+ break
+ }
+ }
+ if !kubeConfigSyncFailed(user) {
+ t.Fatal("failed kubeconfig condition was not detected")
+ }
+
+ for _, status := range []corev1.ConditionStatus{
+ corev1.ConditionTrue,
+ corev1.ConditionUnknown,
+ } {
+ for i := range user.Status.Conditions {
+ if user.Status.Conditions[i].Type == kubeConfigReadyCondition {
+ user.Status.Conditions[i].Status = status
+ break
+ }
+ }
+ if kubeConfigSyncFailed(user) {
+ t.Fatalf("kubeconfig condition %q incorrectly forced a retry", status)
+ }
+ }
+}
+
+func TestBoundTokenSecretMatchesDetectsIdentityDrift(t *testing.T) {
+ t.Parallel()
+ scheme := reconcileTestScheme(t)
+ user := reconcileTestUser("alice")
+ sa := &corev1.ServiceAccount{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: user.Name,
+ Namespace: config.GetUserSystemNamespace(),
+ OwnerReferences: []metav1.OwnerReference{reconcileTestOwnerReference(user)},
+ },
+ Secrets: []corev1.ObjectReference{{Name: "token-alice"}},
+ }
+ secret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "token-alice",
+ Namespace: config.GetUserSystemNamespace(),
+ UID: types.UID("secret-a"),
+ Annotations: map[string]string{corev1.ServiceAccountNameKey: user.Name},
+ OwnerReferences: []metav1.OwnerReference{reconcileTestOwnerReference(user)},
+ },
+ }
+ cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sa, secret).Build()
+ r := &UserReconciler{cache: cli}
+ state := &userReconcileState{serviceAccount: sa}
+ user.Status.ObservedKubeConfigSecretUID = string(secret.UID)
+
+ if !r.boundTokenSecretMatches(context.Background(), user, state) {
+ t.Fatal("matching bound token secret was rejected")
+ }
+
+ secret.UID = types.UID("secret-b")
+ if err := cli.Update(context.Background(), secret); err != nil {
+ t.Fatalf("update recreated secret: %v", err)
+ }
+ if r.boundTokenSecretMatches(context.Background(), user, state) {
+ t.Fatal("recreated bound token secret was accepted with stale UID")
+ }
+
+ user.Status.ObservedKubeConfigSecretUID = ""
+ if !r.boundTokenSecretMatches(context.Background(), user, state) {
+ t.Fatal("legacy user with a valid bound token secret was rejected")
+ }
+ if user.Status.ObservedKubeConfigSecretUID != "" {
+ t.Fatalf(
+ "legacy user secret UID was unexpectedly backfilled: got %q",
+ user.Status.ObservedKubeConfigSecretUID,
+ )
+ }
+
+ secret.Annotations[corev1.ServiceAccountNameKey] = "other-user"
+ if err := cli.Update(context.Background(), secret); err != nil {
+ t.Fatalf("update secret owner annotation: %v", err)
+ }
+ if r.boundTokenSecretMatches(context.Background(), user, state) {
+ t.Fatal("secret bound to another service account was accepted")
+ }
+}
+
+func TestKubeConfigSyncSkipsSecretUIDRotationForLegacyStatus(t *testing.T) {
+ t.Parallel()
+ scheme := reconcileTestScheme(t)
+ user := benchmarkProcessedUser("alice", time.Now().Add(time.Hour))
+ user.Status.ObservedKubeConfigSecretUID = ""
+ sa := &corev1.ServiceAccount{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: user.Name,
+ Namespace: config.GetUserSystemNamespace(),
+ OwnerReferences: []metav1.OwnerReference{reconcileTestOwnerReference(user)},
+ },
+ Secrets: []corev1.ObjectReference{{Name: "token-alice"}},
+ }
+ secret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "token-alice",
+ Namespace: config.GetUserSystemNamespace(),
+ UID: types.UID("recreated-secret"),
+ Annotations: map[string]string{
+ corev1.ServiceAccountNameKey: user.Name,
+ },
+ OwnerReferences: []metav1.OwnerReference{reconcileTestOwnerReference(user)},
+ },
+ }
+ cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sa, secret).Build()
+ r := &UserReconciler{cache: cli}
+ state := &userReconcileState{serviceAccount: sa}
+
+ r.syncKubeConfigIfNeeded(context.Background(), user, state)
+
+ if state.kubeConfigSyncAttempted {
+ t.Fatal("recreated Secret triggered kubeconfig rotation for legacy status")
+ }
+ if user.Status.ObservedKubeConfigSecretUID != "" {
+ t.Fatalf(
+ "legacy status UID was unexpectedly backfilled: %q",
+ user.Status.ObservedKubeConfigSecretUID,
+ )
+ }
+}
+
+func TestNewUserStatusNeedsInitialization(t *testing.T) {
+ t.Parallel()
+ user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "new-user"}}
+ if !(&UserReconciler{}).isNewUser(user) {
+ t.Fatal("empty user was not classified as new")
+ }
+ if !userStatusNeedsSync(user) {
+ t.Fatal("new user was classified as already synchronized")
+ }
+}
+
+func BenchmarkStartupReconcileProcessedUsers100K(b *testing.B) {
+ const userCount = 100_000
+ refreshAt := time.Now().Add(time.Hour)
+ users := make([]*userv1.User, userCount)
+ for i := range users {
+ users[i] = benchmarkProcessedUser(fmt.Sprintf("user-%d", i), refreshAt)
+ }
+ reconciler := &UserReconciler{cache: healthyStartupCache{}}
+ ctx := context.Background()
+ b.ReportAllocs()
+ b.ReportMetric(float64(userCount), "users/op")
+ b.ResetTimer()
+ for range b.N {
+ for _, user := range users {
+ if _, err := reconciler.reconcile(ctx, user); err != nil {
+ b.Fatal(err)
+ }
+ }
+ }
+}
+
+func BenchmarkStartupNewUserAfterHistoricalQueue100K(b *testing.B) {
+ const historicalUserCount = 100_000
+ refreshAt := time.Now().Add(time.Hour)
+ historicalUsers := make([]*userv1.User, historicalUserCount)
+ for i := range historicalUsers {
+ historicalUsers[i] = benchmarkProcessedUser(fmt.Sprintf("user-%d", i), refreshAt)
+ }
+ newUser := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "new-user"}}
+ b.ReportAllocs()
+ b.ReportMetric(float64(historicalUserCount+1), "users/op")
+ b.ResetTimer()
+ for range b.N {
+ for _, user := range historicalUsers {
+ if userStatusNeedsSync(user) {
+ b.Fatalf("historical user %s was classified as needing initialization", user.Name)
+ }
+ }
+ if !userStatusNeedsSync(newUser) {
+ b.Fatalf("new user %s was classified as already processed", newUser.Name)
+ }
+ }
+}
diff --git a/controllers/user/controllers/user_count.go b/controllers/user/controllers/user_count.go
new file mode 100644
index 000000000000..c59af10f1b31
--- /dev/null
+++ b/controllers/user/controllers/user_count.go
@@ -0,0 +1,97 @@
+// Copyright © 2026 sealos.
+//
+// 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 controllers
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/pkg/usercount"
+ toolscache "k8s.io/client-go/tools/cache"
+ ctrl "sigs.k8s.io/controller-runtime"
+ ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
+)
+
+type userCountRunnable struct {
+ cache ctrlcache.Cache
+}
+
+func (r *userCountRunnable) Start(ctx context.Context) error {
+ <-ctx.Done()
+ return nil
+}
+
+func (r *userCountRunnable) GetCache() ctrlcache.Cache {
+ return r.cache
+}
+
+func (r *userCountRunnable) NeedLeaderElection() bool {
+ return false
+}
+
+type userCountCache struct {
+ ctrlcache.Cache
+ counter *usercount.Counter
+ handlerRegistration toolscache.ResourceEventHandlerRegistration
+}
+
+func (c *userCountCache) WaitForCacheSync(ctx context.Context) bool {
+ if !c.Cache.WaitForCacheSync(ctx) {
+ return false
+ }
+ if !toolscache.WaitForCacheSync(ctx.Done(), c.handlerRegistration.HasSynced) {
+ return false
+ }
+ c.counter.MarkInitialized()
+ return true
+}
+
+func SetupUserCount(mgr ctrl.Manager) (*usercount.Counter, error) {
+ counter := usercount.NewCounter()
+ informer, err := mgr.GetCache().GetInformer(context.Background(), &userv1.User{})
+ if err != nil {
+ return nil, fmt.Errorf("get user informer: %w", err)
+ }
+ registration, err := informer.AddEventHandler(toolscache.ResourceEventHandlerFuncs{
+ AddFunc: counter.Add,
+ UpdateFunc: counter.Update,
+ DeleteFunc: counter.Delete,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("add user count event handler: %w", err)
+ }
+
+ if err := mgr.Add(&userCountRunnable{
+ cache: &userCountCache{
+ Cache: mgr.GetCache(),
+ counter: counter,
+ handlerRegistration: registration,
+ },
+ }); err != nil {
+ return nil, fmt.Errorf("add user count runnable: %w", err)
+ }
+ if err := mgr.AddReadyzCheck("user-count-cache", func(_ *http.Request) error {
+ if !counter.Initialized() {
+ return errors.New("user count cache is not initialized")
+ }
+ return nil
+ }); err != nil {
+ return nil, fmt.Errorf("add user count readiness check: %w", err)
+ }
+ return counter, nil
+}
diff --git a/controllers/user/controllers/user_count_test.go b/controllers/user/controllers/user_count_test.go
new file mode 100644
index 000000000000..4715e2ca97da
--- /dev/null
+++ b/controllers/user/controllers/user_count_test.go
@@ -0,0 +1,93 @@
+// Copyright 2026 labring.
+//
+// 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 controllers
+
+import (
+ "context"
+ "testing"
+
+ "github.com/labring/sealos/controllers/user/pkg/usercount"
+ toolscache "k8s.io/client-go/tools/cache"
+ ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
+)
+
+type stubCache struct {
+ ctrlcache.Cache
+ synced bool
+}
+
+func (c *stubCache) WaitForCacheSync(context.Context) bool {
+ return c.synced
+}
+
+type stubHandlerRegistration struct {
+ synced bool
+}
+
+func (r stubHandlerRegistration) HasSynced() bool {
+ return r.synced
+}
+
+func TestUserCountCacheWaitForCacheSync(t *testing.T) {
+ tests := []struct {
+ name string
+ cacheSynced bool
+ handlerSynced bool
+ wantReady bool
+ }{
+ {
+ name: "cache not synced",
+ cacheSynced: false,
+ handlerSynced: true,
+ },
+ {
+ name: "handler not synced",
+ cacheSynced: true,
+ handlerSynced: false,
+ },
+ {
+ name: "cache and handler synced",
+ cacheSynced: true,
+ handlerSynced: true,
+ wantReady: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ counter := usercount.NewCounter()
+ cache := &userCountCache{
+ Cache: &stubCache{synced: tt.cacheSynced},
+ counter: counter,
+ handlerRegistration: stubHandlerRegistration{synced: tt.handlerSynced},
+ }
+ ctx := context.Background()
+ if !tt.handlerSynced {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithCancel(ctx)
+ cancel()
+ }
+
+ if got := cache.WaitForCacheSync(ctx); got != tt.wantReady {
+ t.Fatalf("WaitForCacheSync() = %v, want %v", got, tt.wantReady)
+ }
+ if got := counter.Initialized(); got != tt.wantReady {
+ t.Fatalf("counter.Initialized() = %v, want %v", got, tt.wantReady)
+ }
+ })
+ }
+}
+
+var _ toolscache.ResourceEventHandlerRegistration = stubHandlerRegistration{}
diff --git a/controllers/user/controllers/user_event_handler.go b/controllers/user/controllers/user_event_handler.go
new file mode 100644
index 000000000000..9bd1fff7e3ba
--- /dev/null
+++ b/controllers/user/controllers/user_event_handler.go
@@ -0,0 +1,111 @@
+// Copyright 2026 labring.
+//
+// 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 controllers
+
+import (
+ "context"
+
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/util/workqueue"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/handler"
+)
+
+// userEventHandler keeps incomplete Users ahead of the initial historical
+// queue after a restart. Creation time is insufficient because a User may have
+// been waiting for reconciliation before the previous controller terminated.
+type userEventHandler struct{}
+
+var _ handler.EventHandler = userEventHandler{}
+
+func (userEventHandler) Create(
+ _ context.Context,
+ evt event.CreateEvent,
+ q workqueue.TypedRateLimitingInterface[ctrl.Request],
+) {
+ if evt.Object == nil {
+ return
+ }
+ addUserRequest(q, evt.Object, userPriority(evt.Object))
+}
+
+func (userEventHandler) Update(
+ _ context.Context,
+ evt event.UpdateEvent,
+ q workqueue.TypedRateLimitingInterface[ctrl.Request],
+) {
+ obj := evt.ObjectNew
+ if obj == nil {
+ obj = evt.ObjectOld
+ }
+ if obj == nil {
+ return
+ }
+ // Accepted User updates change generation or the owner annotation, so they
+ // represent live drift and must use the normal priority.
+ addUserRequest(q, obj, 0)
+}
+
+func (userEventHandler) Delete(
+ _ context.Context,
+ evt event.DeleteEvent,
+ q workqueue.TypedRateLimitingInterface[ctrl.Request],
+) {
+ if evt.Object == nil {
+ return
+ }
+ addUserRequest(q, evt.Object, 0)
+}
+
+func (userEventHandler) Generic(
+ _ context.Context,
+ evt event.GenericEvent,
+ q workqueue.TypedRateLimitingInterface[ctrl.Request],
+) {
+ if evt.Object == nil {
+ return
+ }
+ addUserRequest(q, evt.Object, 0)
+}
+
+func addUserRequest(
+ q workqueue.TypedRateLimitingInterface[ctrl.Request],
+ obj client.Object,
+ priority int,
+) {
+ request := ctrl.Request{NamespacedName: types.NamespacedName{
+ Name: obj.GetName(),
+ Namespace: obj.GetNamespace(),
+ }}
+ priorityQueue, ok := q.(priorityqueue.PriorityQueue[ctrl.Request])
+ if !ok {
+ q.Add(request)
+ return
+ }
+
+ priorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: priority}, request)
+}
+
+func userPriority(obj client.Object) int {
+ user, ok := obj.(*userv1.User)
+ if ok && !userStatusNeedsSync(user) {
+ return handler.LowPriority
+ }
+ return 0
+}
diff --git a/controllers/user/controllers/user_event_handler_test.go b/controllers/user/controllers/user_event_handler_test.go
new file mode 100644
index 000000000000..8e09cdc02dac
--- /dev/null
+++ b/controllers/user/controllers/user_event_handler_test.go
@@ -0,0 +1,229 @@
+// Copyright 2026 labring.
+//
+// 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 controllers
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/pkg/licensegate"
+ "github.com/labring/sealos/controllers/user/pkg/usercount"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/record"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+ "sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+func TestUserEventHandlerPrioritizesIncompleteUsers(t *testing.T) {
+ historical := metav1.NewTime(time.Now().Add(-time.Hour))
+ newUser := &userv1.User{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "new-user",
+ Generation: 1,
+ CreationTimestamp: historical,
+ },
+ }
+ failedUser := benchmarkProcessedUser("failed-user", time.Now().Add(time.Hour))
+ failedUser.CreationTimestamp = historical
+ for i := range failedUser.Status.Conditions {
+ if failedUser.Status.Conditions[i].Type == userv1.Ready {
+ failedUser.Status.Conditions[i].Status = corev1.ConditionFalse
+ }
+ }
+ healthyUser := benchmarkProcessedUser("healthy-user", time.Now().Add(time.Hour))
+ healthyUser.CreationTimestamp = historical
+
+ tests := []struct {
+ name string
+ user *userv1.User
+ priority int
+ }{
+ {name: "new", user: newUser, priority: 0},
+ {name: "failed", user: failedUser, priority: 0},
+ {name: "healthy historical", user: healthyUser, priority: -100},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ q := priorityqueue.New[reconcile.Request]("user-event-test")
+ t.Cleanup(q.ShutDown)
+ userEventHandler{}.Create(
+ context.Background(),
+ event.CreateEvent{Object: tt.user},
+ q,
+ )
+
+ request, priority, shutdown := q.GetWithPriority()
+ if shutdown {
+ t.Fatal("priority queue shut down before delivering User")
+ }
+ q.Done(request)
+ if request.Name != tt.user.Name {
+ t.Fatalf("request name = %q, want %q", request.Name, tt.user.Name)
+ }
+ if priority != tt.priority {
+ t.Fatalf("priority = %d, want %d", priority, tt.priority)
+ }
+ })
+ }
+}
+
+func TestUserEventHandlerUpdatePromotesQueuedUser(t *testing.T) {
+ t.Parallel()
+
+ user := benchmarkProcessedUser("healthy-user", time.Now().Add(time.Hour))
+ user.CreationTimestamp = metav1.NewTime(time.Now().Add(-time.Hour))
+ updated := user.DeepCopy()
+ updated.Generation++
+ updated.ResourceVersion = "2"
+
+ q := priorityqueue.New[reconcile.Request]("user-event-promotion-test")
+ t.Cleanup(q.ShutDown)
+ handler := userEventHandler{}
+ handler.Create(context.Background(), event.CreateEvent{Object: user}, q)
+ handler.Update(context.Background(), event.UpdateEvent{
+ ObjectOld: user,
+ ObjectNew: updated,
+ }, q)
+
+ request, priority, shutdown := q.GetWithPriority()
+ if shutdown {
+ t.Fatal("priority queue shut down before delivering User")
+ }
+ q.Done(request)
+ if priority != 0 {
+ t.Fatalf("promoted priority = %d, want 0", priority)
+ }
+}
+
+func TestAddUserRequestUsesCallerPriority(t *testing.T) {
+ t.Parallel()
+
+ q := priorityqueue.New[reconcile.Request]("user-event-custom-priority-test")
+ t.Cleanup(q.ShutDown)
+ user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "custom-priority"}}
+
+ addUserRequest(q, user, 42)
+ request, priority, shutdown := q.GetWithPriority()
+ if shutdown {
+ t.Fatal("priority queue shut down before delivering User")
+ }
+ q.Done(request)
+ if request.Name != user.Name {
+ t.Fatalf("request name = %q, want %q", request.Name, user.Name)
+ }
+ if priority != 42 {
+ t.Fatalf("priority = %d, want caller priority 42", priority)
+ }
+}
+
+func TestIgnorePreStartCreatePredicate(t *testing.T) {
+ t.Parallel()
+
+ startedAt := time.Unix(1_000, 0)
+ predicate := ignorePreStartCreatePredicate{startedAt: startedAt}
+ tests := []struct {
+ name string
+ createdAt time.Time
+ want bool
+ }{
+ {name: "before controller start", createdAt: startedAt.Add(-time.Nanosecond)},
+ {name: "at controller start", createdAt: startedAt, want: true},
+ {name: "after controller start", createdAt: startedAt.Add(time.Second), want: true},
+ {name: "missing timestamp", want: true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ obj := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{
+ CreationTimestamp: metav1.NewTime(tt.createdAt),
+ }}
+ if got := predicate.Create(event.CreateEvent{Object: obj}); got != tt.want {
+ t.Fatalf("Create() = %t, want %t", got, tt.want)
+ }
+ })
+ }
+ if predicate.Create(event.CreateEvent{}) {
+ t.Fatal("nil Create object was accepted")
+ }
+}
+
+func TestLicenseLimitedUserRemainsNewAcrossRestart(t *testing.T) {
+ scheme := reconcileTestScheme(t)
+ stored := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "new-user", Generation: 1}}
+ cli := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithStatusSubresource(&userv1.User{}).
+ WithObjects(stored).
+ Build()
+
+ counter := usercount.NewCounter()
+ counter.Add(&userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "existing-user"}})
+ counter.MarkInitialized()
+ licensegate.SetState(true, 1)
+ t.Cleanup(func() {
+ licensegate.SetState(false, licensegate.DefaultUserLimit)
+ })
+
+ r := &UserReconciler{
+ Client: cli,
+ Recorder: record.NewFakeRecorder(2),
+ userCounter: counter,
+ }
+ user := &userv1.User{}
+ if err := cli.Get(context.Background(), client.ObjectKey{Name: stored.Name}, user); err != nil {
+ t.Fatalf("get new User: %v", err)
+ }
+ blocked, err := r.handleLicenseLimit(
+ context.Background(),
+ user,
+ user.Status.DeepCopy(),
+ )
+ if err != nil {
+ t.Fatalf("apply license limit: %v", err)
+ }
+ if !blocked {
+ t.Fatal("license-limited User was not blocked")
+ }
+
+ restartedUser := &userv1.User{}
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKey{Name: stored.Name},
+ restartedUser,
+ ); err != nil {
+ t.Fatalf("get persisted User status: %v", err)
+ }
+ if !r.isNewUser(restartedUser) {
+ t.Fatalf("license-limited User was classified as old: %#v", restartedUser.Status)
+ }
+ blocked, err = r.handleLicenseLimit(
+ context.Background(),
+ restartedUser,
+ restartedUser.Status.DeepCopy(),
+ )
+ if err != nil {
+ t.Fatalf("retry license limit: %v", err)
+ }
+ if !blocked {
+ t.Fatal("license-limited User was allowed before capacity became available")
+ }
+}
diff --git a/controllers/user/controllers/user_expiration_controller.go b/controllers/user/controllers/user_expiration_controller.go
new file mode 100644
index 000000000000..b2d0909a0d5d
--- /dev/null
+++ b/controllers/user/controllers/user_expiration_controller.go
@@ -0,0 +1,103 @@
+/*
+Copyright 2022 labring.
+
+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 controllers
+
+import (
+ "context"
+ "errors"
+
+ "github.com/go-logr/logr"
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/controllers/helper/finalizer"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/record"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// UserExpirationReconciler reconciles a Secret object
+type UserExpirationReconciler struct {
+ Logger logr.Logger
+ Recorder record.EventRecorder
+ config *rest.Config
+ *runtime.Scheme
+ client.Client
+ finalizer *finalizer.Finalizer
+}
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+// TODO(user): Modify the Reconcile function to compare the state specified by
+// the Secret object against the actual cluster state, and then
+// perform operations to make the cluster state reflect the state specified by
+// the user.
+//
+// For more details, check Reconcile and its Result here:
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile
+func (r *UserExpirationReconciler) Reconcile(
+ ctx context.Context,
+ req ctrl.Request,
+) (ctrl.Result, error) {
+ r.Logger.V(1).Info("start reconcile for users expiration")
+ user := &userv1.User{}
+ if err := r.Get(ctx, req.NamespacedName, user); err != nil {
+ return ctrl.Result{}, client.IgnoreNotFound(err)
+ }
+
+ if ok, err := r.finalizer.RemoveFinalizer(ctx, user, finalizer.DefaultFunc); ok {
+ return ctrl.Result{}, err
+ }
+
+ if ok, err := r.finalizer.AddFinalizer(ctx, user); ok {
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ return r.reconcile(ctx, user)
+ }
+ return ctrl.Result{}, errors.New("reconcile error from Finalizer")
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *UserExpirationReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ const controllerName = "user_expiration_controller"
+ if r.Client == nil {
+ r.Client = mgr.GetClient()
+ }
+ r.Logger = ctrl.Log.WithName(controllerName)
+ if r.Recorder == nil {
+ r.Recorder = mgr.GetEventRecorderFor(controllerName)
+ }
+ if r.finalizer == nil {
+ r.finalizer = finalizer.NewFinalizer(r.Client, "sealos.io/user.expiration.finalizers").
+ WithReader(mgr.GetAPIReader())
+ }
+ r.Scheme = mgr.GetScheme()
+ r.config = mgr.GetConfig()
+ r.Logger.V(1).Info("init reconcile controller user expiration")
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&userv1.User{}).
+ Complete(r)
+}
+
+func (r *UserExpirationReconciler) reconcile(
+ _ context.Context,
+ _ client.Object,
+) (ctrl.Result, error) {
+ // TODO add Expiration logic
+ return ctrl.Result{}, nil
+}
diff --git a/controllers/user/controllers/user_requeue_test.go b/controllers/user/controllers/user_requeue_test.go
new file mode 100644
index 000000000000..507d98e47ff1
--- /dev/null
+++ b/controllers/user/controllers/user_requeue_test.go
@@ -0,0 +1,52 @@
+// Copyright 2026 labring.
+//
+// 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 controllers
+
+import (
+ "testing"
+ "time"
+
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestNextKubeConfigRequeueDuration(t *testing.T) {
+ t.Parallel()
+ if got := nextKubeConfigRequeueDuration(nil, nil); got != 0 {
+ t.Fatalf("ordinary requeue duration = %v, want 0", got)
+ }
+
+ refreshAt := metav1.NewTime(time.Now().Add(time.Hour))
+ user := &userv1.User{Status: userv1.UserStatus{KubeConfigRefreshAt: &refreshAt}}
+ if got := nextKubeConfigRequeueDuration(user, nil); got < 59*time.Minute || got > time.Hour {
+ t.Fatalf("persisted refresh requeue duration = %v, want about 1h", got)
+ }
+
+ expiresAt := metav1.NewTime(time.Now().Add(10 * time.Hour))
+ state := &userReconcileState{tokenExpirationDeadline: &expiresAt}
+ if got := nextKubeConfigRequeueDuration(
+ nil,
+ state,
+ ); got < 7*time.Hour+59*time.Minute ||
+ got > 8*time.Hour {
+ t.Fatalf("new token refresh requeue duration = %v, want about 8h", got)
+ }
+
+ past := metav1.NewTime(time.Now().Add(-time.Minute))
+ user.Status.KubeConfigRefreshAt = &past
+ if got := nextKubeConfigRequeueDuration(user, nil); got != time.Second {
+ t.Fatalf("overdue refresh requeue duration = %v, want 1s", got)
+ }
+}
diff --git a/controllers/user/controllers/user_status_test.go b/controllers/user/controllers/user_status_test.go
new file mode 100644
index 000000000000..9d2ba2badbcf
--- /dev/null
+++ b/controllers/user/controllers/user_status_test.go
@@ -0,0 +1,1082 @@
+// Copyright 2026 labring.
+//
+// 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 controllers
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/controllers/helper"
+ "github.com/labring/sealos/controllers/user/controllers/helper/config"
+ corev1 "k8s.io/api/core/v1"
+ rbacv1 "k8s.io/api/rbac/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/tools/record"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+)
+
+type namespaceCreateErrorClient struct {
+ client.Client
+}
+
+func (c namespaceCreateErrorClient) Create(
+ ctx context.Context,
+ obj client.Object,
+ opts ...client.CreateOption,
+) error {
+ if _, ok := obj.(*corev1.Namespace); ok {
+ return errors.New("namespace create failed")
+ }
+ return c.Client.Create(ctx, obj, opts...)
+}
+
+type roleSyncErrorClient struct {
+ client.Client
+}
+
+func (c roleSyncErrorClient) Create(
+ ctx context.Context,
+ obj client.Object,
+ opts ...client.CreateOption,
+) error {
+ if _, ok := obj.(*rbacv1.Role); ok {
+ return errors.New("role create failed")
+ }
+ return c.Client.Create(ctx, obj, opts...)
+}
+
+func (c roleSyncErrorClient) Update(
+ ctx context.Context,
+ obj client.Object,
+ opts ...client.UpdateOption,
+) error {
+ if _, ok := obj.(*rbacv1.Role); ok {
+ return errors.New("role update failed")
+ }
+ return c.Client.Update(ctx, obj, opts...)
+}
+
+type roleBindingSyncErrorClient struct {
+ client.Client
+}
+
+func (c roleBindingSyncErrorClient) Create(
+ ctx context.Context,
+ obj client.Object,
+ opts ...client.CreateOption,
+) error {
+ if _, ok := obj.(*rbacv1.RoleBinding); ok {
+ return errors.New("role binding create failed")
+ }
+ return c.Client.Create(ctx, obj, opts...)
+}
+
+func TestUpdateStatusPreservesUncachedKubeConfig(t *testing.T) {
+ t.Parallel()
+
+ scheme := runtime.NewScheme()
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add user scheme: %v", err)
+ }
+ stored := &userv1.User{
+ ObjectMeta: metav1.ObjectMeta{Name: "user-a"},
+ Status: userv1.UserStatus{
+ Phase: userv1.UserActive,
+ KubeConfig: "existing-kubeconfig",
+ },
+ }
+ cli := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithStatusSubresource(&userv1.User{}).
+ WithObjects(stored).
+ Build()
+
+ projected := &userv1.User{}
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKeyFromObject(stored),
+ projected,
+ ); err != nil {
+ t.Fatalf("get user: %v", err)
+ }
+ projected.Status.KubeConfig = ""
+ originalStatus := projected.Status.DeepCopy()
+ projected.Status.ObservedGeneration = 1
+
+ reconciler := &UserReconciler{Client: cli}
+ if err := reconciler.updateStatus(context.Background(), projected, originalStatus); err != nil {
+ t.Fatalf("patch status: %v", err)
+ }
+
+ got := &userv1.User{}
+ if err := cli.Get(context.Background(), client.ObjectKeyFromObject(stored), got); err != nil {
+ t.Fatalf("get updated user: %v", err)
+ }
+ if got.Status.KubeConfig != stored.Status.KubeConfig {
+ t.Fatalf("kubeconfig = %q, want %q", got.Status.KubeConfig, stored.Status.KubeConfig)
+ }
+ if got.Status.ObservedGeneration != 1 {
+ t.Fatalf("observed generation = %d, want 1", got.Status.ObservedGeneration)
+ }
+}
+
+func TestOwnerAnnotationChangedPredicate(t *testing.T) {
+ t.Parallel()
+
+ oldUser := &userv1.User{ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ userv1.UserAnnotationOwnerKey: "owner-a",
+ userv1.UserAnnotationDisplayKey: "old-display",
+ },
+ }}
+ newUser := oldUser.DeepCopy()
+ newUser.Annotations[userv1.UserAnnotationDisplayKey] = "new-display"
+ predicate := OwnerAnnotationChangedPredicate{}
+ if predicate.Update(event.UpdateEvent{ObjectOld: oldUser, ObjectNew: newUser}) {
+ t.Fatal("unrelated annotation change triggered reconciliation")
+ }
+
+ newUser.Annotations[userv1.UserAnnotationOwnerKey] = "owner-b"
+ if !predicate.Update(event.UpdateEvent{ObjectOld: oldUser, ObjectNew: newUser}) {
+ t.Fatal("owner annotation change did not trigger reconciliation")
+ }
+}
+
+func TestDeletionTimestampChangedPredicate(t *testing.T) {
+ t.Parallel()
+
+ oldUser := &userv1.User{}
+ newUser := oldUser.DeepCopy()
+ predicate := DeletionTimestampChangedPredicate{}
+ if predicate.Update(event.UpdateEvent{ObjectOld: oldUser, ObjectNew: newUser}) {
+ t.Fatal("unchanged deletion timestamp triggered reconciliation")
+ }
+
+ now := metav1.Now()
+ newUser.DeletionTimestamp = &now
+ if !predicate.Update(event.UpdateEvent{ObjectOld: oldUser, ObjectNew: newUser}) {
+ t.Fatal("deletion timestamp change did not trigger reconciliation")
+ }
+}
+
+func TestNamespacePodSecurityPredicate(t *testing.T) {
+ t.Parallel()
+
+ oldNamespace := &metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{
+ Name: "ns-user-a",
+ Labels: map[string]string{
+ config.PodSecurityLabelPrefix + "enforce": "baseline",
+ },
+ }}
+ newNamespace := oldNamespace.DeepCopy()
+ delete(newNamespace.Labels, config.PodSecurityLabelPrefix+"enforce")
+ p := NamespacePodSecurityPredicate{}
+ if !p.Update(event.UpdateEvent{ObjectOld: oldNamespace, ObjectNew: newNamespace}) {
+ t.Fatal("Pod Security label deletion did not trigger reconciliation")
+ }
+
+ newNamespace = oldNamespace.DeepCopy()
+ newNamespace.Labels["unused.example/label"] = "changed"
+ if p.Update(event.UpdateEvent{ObjectOld: oldNamespace, ObjectNew: newNamespace}) {
+ t.Fatal("unrelated label change triggered reconciliation")
+ }
+
+ newNamespace = oldNamespace.DeepCopy()
+ newNamespace.Annotations = map[string]string{userv1.UserAnnotationOwnerKey: "owner-b"}
+ if !p.Update(event.UpdateEvent{ObjectOld: oldNamespace, ObjectNew: newNamespace}) {
+ t.Fatal("owner annotation change did not trigger reconciliation")
+ }
+
+ newNamespace = oldNamespace.DeepCopy()
+ newNamespace.Labels[userv1.UserLabelOwnerKey] = "owner-b"
+ if !p.Update(event.UpdateEvent{ObjectOld: oldNamespace, ObjectNew: newNamespace}) {
+ t.Fatal("owner label change did not trigger reconciliation")
+ }
+
+ newNamespace = oldNamespace.DeepCopy()
+ newNamespace.Annotations = map[string]string{userv1.UserAnnotationCreatorKey: "user-a"}
+ if !p.Update(event.UpdateEvent{ObjectOld: oldNamespace, ObjectNew: newNamespace}) {
+ t.Fatal("creator annotation change did not trigger reconciliation")
+ }
+
+ newNamespace = oldNamespace.DeepCopy()
+ newNamespace.OwnerReferences = []metav1.OwnerReference{{Name: "user-a"}}
+ if !p.Update(event.UpdateEvent{ObjectOld: oldNamespace, ObjectNew: newNamespace}) {
+ t.Fatal("owner reference change did not trigger reconciliation")
+ }
+
+ if !p.Create(event.CreateEvent{Object: oldNamespace}) {
+ t.Fatal("ns-* namespace creation did not trigger reconciliation")
+ }
+ if p.Create(event.CreateEvent{
+ Object: &metav1.PartialObjectMetadata{
+ ObjectMeta: metav1.ObjectMeta{Name: "kube-system"},
+ },
+ }) {
+ t.Fatal("unmanaged namespace creation triggered reconciliation")
+ }
+ if !p.Delete(event.DeleteEvent{Object: oldNamespace}) {
+ t.Fatal("managed namespace deletion did not trigger reconciliation")
+ }
+ if p.Delete(event.DeleteEvent{
+ Object: &metav1.PartialObjectMetadata{
+ ObjectMeta: metav1.ObjectMeta{Name: "kube-system"},
+ },
+ }) {
+ t.Fatal("unmanaged namespace deletion triggered reconciliation")
+ }
+}
+
+func TestKubeConfigSyncDueUsesPersistedRefreshAt(t *testing.T) {
+ t.Parallel()
+ r := &UserReconciler{}
+ future := metav1.NewTime(time.Now().Add(time.Hour))
+ user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}, Status: userv1.UserStatus{
+ KubeConfigRefreshAt: &future,
+ }}
+ if r.kubeConfigSyncDue(user) {
+ t.Fatal("future persisted refresh time was considered due")
+ }
+ past := metav1.NewTime(time.Now().Add(-time.Second))
+ user.Status.KubeConfigRefreshAt = &past
+ if !r.kubeConfigSyncDue(user) {
+ t.Fatal("past persisted refresh time was not considered due")
+ }
+ user.Status.KubeConfigRefreshAt = nil
+ if !r.kubeConfigSyncDue(user) {
+ t.Fatal("missing persisted refresh time was not considered due")
+ }
+}
+
+func TestCSRExpirationStatusMatchingUsesMinimumForLowValues(t *testing.T) {
+ t.Parallel()
+ minimum := userv1.DefaultCSRExpirationSeconds
+ if !csrExpirationStatusMatches(7_200, 0) {
+ t.Fatal("low spec and empty observed expiration should use the minimum")
+ }
+ if !csrExpirationStatusMatches(7_200, minimum) {
+ t.Fatal("low spec and minimum observed expiration should match")
+ }
+ if csrExpirationStatusMatches(7_200, minimum+1) {
+ t.Fatal("low spec should not match a different above-minimum observed expiration")
+ }
+ if csrExpirationStatusMatches(minimum+1, 7_200) {
+ t.Fatal("spec above minimum should detect a different effective expiration")
+ }
+ if !csrExpirationStatusMatches(minimum+1, minimum+1) {
+ t.Fatal("equal above-minimum expirations should match")
+ }
+}
+
+func TestUserStatusNeedsSyncIgnoresBelowMinimumExpirationDrift(t *testing.T) {
+ t.Parallel()
+ user := &userv1.User{
+ ObjectMeta: metav1.ObjectMeta{Name: "alice", Generation: 1},
+ Spec: userv1.UserSpec{CSRExpirationSeconds: 7_200},
+ Status: userv1.UserStatus{
+ Phase: userv1.UserActive,
+ ObservedGeneration: 1,
+ ObservedCSRExpirationSeconds: 7_200,
+ Conditions: []userv1.Condition{
+ {Type: userv1.Initialized, Status: corev1.ConditionTrue},
+ {Type: userv1.Ready, Status: corev1.ConditionTrue},
+ },
+ },
+ }
+ if userStatusNeedsSync(user) {
+ t.Fatal("below-minimum expiration drift triggered a status sync")
+ }
+ if user.Status.ObservedCSRExpirationSeconds != 7_200 {
+ t.Fatalf(
+ "observed expiration was normalized outside a kubeconfig refresh: %d",
+ user.Status.ObservedCSRExpirationSeconds,
+ )
+ }
+}
+
+func TestSetObservedCSRExpirationSecondsNormalizesOnlyDuringRefresh(t *testing.T) {
+ t.Parallel()
+ user := &userv1.User{
+ Spec: userv1.UserSpec{CSRExpirationSeconds: 7_200},
+ Status: userv1.UserStatus{
+ ObservedCSRExpirationSeconds: 7_200,
+ },
+ }
+ setObservedCSRExpirationSeconds(user)
+ if user.Status.ObservedCSRExpirationSeconds != userv1.DefaultCSRExpirationSeconds {
+ t.Fatalf(
+ "observed expiration = %d, want minimum %d",
+ user.Status.ObservedCSRExpirationSeconds,
+ userv1.DefaultCSRExpirationSeconds,
+ )
+ }
+
+ user.Spec.CSRExpirationSeconds = userv1.DefaultCSRExpirationSeconds + 1
+ setObservedCSRExpirationSeconds(user)
+ want := userv1.DefaultCSRExpirationSeconds + 1
+ if user.Status.ObservedCSRExpirationSeconds != want {
+ t.Fatalf(
+ "observed expiration = %d, want %d",
+ user.Status.ObservedCSRExpirationSeconds,
+ want,
+ )
+ }
+}
+
+func TestFailedKubeConfigSyncRetriesImmediately(t *testing.T) {
+ t.Parallel()
+ r := &UserReconciler{}
+ user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}}
+ state := &userReconcileState{kubeConfigSyncAttempted: true}
+ state.recordSyncError(errors.New("token request failed"))
+ if err := r.finishKubeConfigSync(user, state); err == nil {
+ t.Fatal("failed kubeconfig sync did not return its error")
+ }
+ value, ok := r.nextKubeConfigSync.Load("alice")
+ deadline, deadlineOK := value.(time.Time)
+ if !ok || !deadlineOK || deadline.After(time.Now()) {
+ t.Fatalf("failed kubeconfig sync deadline = %v, want due now", value)
+ }
+}
+
+func TestSuccessfulKubeConfigSyncUsesPersistedRefreshDeadline(t *testing.T) {
+ t.Parallel()
+ r := &UserReconciler{}
+ refreshAt := metav1.NewTime(time.Now().Add(30 * 24 * time.Hour))
+ user := &userv1.User{
+ ObjectMeta: metav1.ObjectMeta{Name: "alice"},
+ Status: userv1.UserStatus{
+ KubeConfigRefreshAt: &refreshAt,
+ },
+ }
+ state := &userReconcileState{
+ kubeConfigSyncAttempted: true,
+ kubeConfigSynced: true,
+ }
+
+ if err := r.finishKubeConfigSync(user, state); err != nil {
+ t.Fatalf("successful kubeconfig sync returned an error: %v", err)
+ }
+ value, ok := r.nextKubeConfigSync.Load(user.Name)
+ deadline, deadlineOK := value.(time.Time)
+ if !ok || !deadlineOK {
+ t.Fatalf("kubeconfig deadline = %v, want persisted refresh time", value)
+ }
+ if !deadline.Equal(refreshAt.Time) {
+ t.Fatalf("kubeconfig deadline = %s, want %s", deadline, refreshAt.Time)
+ }
+}
+
+func TestKubeConfigSyncAddsFailureCondition(t *testing.T) {
+ t.Parallel()
+ refreshAt := metav1.NewTime(time.Now().Add(time.Hour))
+ user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}}
+ user.Status.KubeConfig = "existing-kubeconfig"
+ user.Status.KubeConfigRefreshAt = &refreshAt
+ user.Status.ObservedCSRExpirationSeconds = userv1.DefaultCSRExpirationSeconds
+ r := &UserReconciler{Recorder: record.NewFakeRecorder(1)}
+ state := &userReconcileState{}
+ r.syncKubeConfig(context.Background(), user, state)
+ if state.syncError == nil {
+ t.Fatal("kubeconfig sync failure was not recorded")
+ }
+ condition := helper.GetCondition(
+ user.Status.Conditions,
+ &userv1.Condition{Type: kubeConfigReadyCondition},
+ )
+ if condition.Status != corev1.ConditionFalse {
+ t.Fatalf("kubeconfig condition status = %s, want False", condition.Status)
+ }
+ if condition.Reason != "SyncUserError" {
+ t.Fatalf("kubeconfig condition reason = %q, want SyncUserError", condition.Reason)
+ }
+ if user.Status.KubeConfig != "existing-kubeconfig" || user.Status.KubeConfigRefreshAt == nil ||
+ !user.Status.KubeConfigRefreshAt.Equal(&refreshAt) ||
+ user.Status.ObservedCSRExpirationSeconds != userv1.DefaultCSRExpirationSeconds {
+ t.Fatalf("kubeconfig failure cleared existing status: %#v", user.Status)
+ }
+}
+
+func TestUserResourcesNeedSyncFromReader(t *testing.T) {
+ t.Parallel()
+ scheme := runtime.NewScheme()
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add user scheme: %v", err)
+ }
+ if err := rbacv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add rbac scheme: %v", err)
+ }
+ if err := corev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add core scheme: %v", err)
+ }
+ controller := true
+ user := &userv1.User{ObjectMeta: metav1.ObjectMeta{
+ Name: "alice",
+ UID: "user-a",
+ Annotations: map[string]string{userv1.UserAnnotationOwnerKey: "owner-a"},
+ }}
+ ownerRef := metav1.OwnerReference{
+ APIVersion: userv1.GroupVersion.String(),
+ Kind: "User",
+ Name: user.Name,
+ UID: user.UID,
+ Controller: &controller,
+ }
+ ns := &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: config.GetUsersNamespace(user.Name),
+ Annotations: map[string]string{
+ userv1.UserAnnotationCreatorKey: user.Name,
+ userv1.UserAnnotationOwnerKey: "owner-a",
+ },
+ Labels: config.SetPodSecurity(
+ map[string]string{userv1.UserLabelOwnerKey: "owner-a"},
+ ),
+ OwnerReferences: []metav1.OwnerReference{ownerRef},
+ },
+ }
+ sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{
+ Name: user.Name,
+ Namespace: config.GetUserSystemNamespace(),
+ Annotations: map[string]string{
+ userv1.UserAnnotationCreatorKey: user.Name,
+ userv1.UserAnnotationOwnerKey: "owner-a",
+ },
+ OwnerReferences: []metav1.OwnerReference{ownerRef},
+ }, Secrets: []corev1.ObjectReference{{Name: "token-alice"}}}
+ secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{
+ Name: "token-alice", Namespace: config.GetUserSystemNamespace(),
+ Annotations: map[string]string{corev1.ServiceAccountNameKey: user.Name},
+ OwnerReferences: []metav1.OwnerReference{ownerRef},
+ }}
+ objects := make([]client.Object, 0, 8)
+ objects = append(objects, user, ns, sa, secret)
+ for _, roleType := range []userv1.RoleType{userv1.OwnerRoleType, userv1.ManagerRoleType, userv1.DeveloperRoleType} {
+ objects = append(objects, &rbacv1.Role{ObjectMeta: metav1.ObjectMeta{
+ Name: string(roleType),
+ Namespace: ns.Name,
+ Annotations: map[string]string{
+ userv1.UserAnnotationCreatorKey: user.Name,
+ userv1.UserAnnotationOwnerKey: "owner-a",
+ },
+ OwnerReferences: []metav1.OwnerReference{ownerRef},
+ }, Rules: config.GetUserRole(roleType)})
+ }
+ objects = append(objects, &rbacv1.RoleBinding{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: user.Name,
+ Namespace: ns.Name,
+ Annotations: map[string]string{
+ userv1.UserAnnotationCreatorKey: user.Name,
+ userv1.UserAnnotationOwnerKey: "owner-a",
+ },
+ OwnerReferences: []metav1.OwnerReference{ownerRef},
+ },
+ RoleRef: rbacv1.RoleRef{
+ APIGroup: rbacv1.GroupName,
+ Kind: "Role",
+ Name: string(userv1.OwnerRoleType),
+ },
+ Subjects: config.GetUsersSubject(user.Name),
+ })
+ reader := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
+ withFinalizer := user.DeepCopy()
+ withFinalizer.Finalizers = []string{userFinalizerName}
+ withFinalizer.Status = userv1.UserStatus{
+ Phase: userv1.UserActive,
+ ObservedGeneration: withFinalizer.Generation,
+ Conditions: []userv1.Condition{
+ {Type: userv1.Initialized, Status: corev1.ConditionTrue},
+ {Type: userv1.Ready, Status: corev1.ConditionTrue},
+ },
+ }
+ if !controllerutil.ContainsFinalizer(withFinalizer, userFinalizerName) {
+ t.Fatal("test user finalizer was not configured")
+ }
+ namespace := &corev1.Namespace{}
+ if err := reader.Get(
+ context.Background(),
+ client.ObjectKey{Name: ns.Name},
+ namespace,
+ ); err != nil {
+ t.Fatalf("get namespace: %v", err)
+ }
+ partialNamespace := &metav1.PartialObjectMetadata{ObjectMeta: namespace.ObjectMeta}
+ if !namespaceMatchesUser(partialNamespace, user, false) {
+ t.Fatal("healthy namespace did not match user")
+ }
+ serviceAccount := &corev1.ServiceAccount{}
+ if err := reader.Get(
+ context.Background(),
+ client.ObjectKey{Name: user.Name, Namespace: config.GetUserSystemNamespace()},
+ serviceAccount,
+ ); err != nil {
+ t.Fatalf("get service account: %v", err)
+ }
+ if !metadataMatchesUserResource(serviceAccount, user) {
+ t.Fatal("healthy service account did not match user")
+ }
+ if !controlledByUser(secret, user) ||
+ secret.Annotations[corev1.ServiceAccountNameKey] != user.Name {
+ t.Fatal("healthy bound secret did not match user")
+ }
+ for _, roleType := range []userv1.RoleType{userv1.OwnerRoleType, userv1.ManagerRoleType, userv1.DeveloperRoleType} {
+ if !roleMatchesUser(
+ context.Background(),
+ reader,
+ client.ObjectKey{Name: string(roleType), Namespace: ns.Name},
+ roleType,
+ user,
+ ) {
+ t.Fatalf("healthy %s role did not match user", roleType)
+ }
+ }
+ roleBinding := &rbacv1.RoleBinding{}
+ if err := reader.Get(
+ context.Background(),
+ client.ObjectKey{Name: user.Name, Namespace: ns.Name},
+ roleBinding,
+ ); err != nil {
+ t.Fatalf("get role binding: %v", err)
+ }
+ if !roleBindingMatchesUser(roleBinding, user) {
+ t.Fatal("healthy role binding did not match user")
+ }
+ role := &rbacv1.Role{}
+ if err := reader.Get(
+ context.Background(),
+ client.ObjectKey{Name: string(userv1.DeveloperRoleType), Namespace: ns.Name},
+ role,
+ ); err != nil {
+ t.Fatalf("get developer role: %v", err)
+ }
+ role.Rules = []rbacv1.PolicyRule{
+ {APIGroups: []string{"*"}, Resources: []string{"pods"}, Verbs: []string{"get"}},
+ }
+ if err := reader.Update(context.Background(), role); err != nil {
+ t.Fatalf("update developer role: %v", err)
+ }
+ if roleMatchesUser(
+ context.Background(),
+ reader,
+ client.ObjectKey{Name: string(userv1.DeveloperRoleType), Namespace: ns.Name},
+ userv1.DeveloperRoleType,
+ user,
+ ) {
+ t.Fatal("developer role rule drift was not detected")
+ }
+ role.Rules = config.GetUserRole(userv1.DeveloperRoleType)
+ if err := reader.Update(context.Background(), role); err != nil {
+ t.Fatalf("restore developer role: %v", err)
+ }
+
+ storedNamespace := &corev1.Namespace{}
+ if err := reader.Get(
+ context.Background(),
+ client.ObjectKey{Name: ns.Name},
+ storedNamespace,
+ ); err != nil {
+ t.Fatalf("get namespace: %v", err)
+ }
+ storedNamespace.Annotations[userv1.UserAnnotationOwnerKey] = "owner-b"
+ if err := reader.Update(context.Background(), storedNamespace); err != nil {
+ t.Fatalf("update namespace: %v", err)
+ }
+ partialNamespace.Annotations = storedNamespace.Annotations
+ if namespaceMatchesUser(partialNamespace, user, false) {
+ t.Fatal("namespace metadata drift was not detected")
+ }
+ storedNamespace.Annotations[userv1.UserAnnotationOwnerKey] = "owner-a"
+ if err := reader.Update(context.Background(), storedNamespace); err != nil {
+ t.Fatalf("restore namespace: %v", err)
+ }
+
+ if err := reader.Delete(context.Background(), secret); err != nil {
+ t.Fatalf("delete bound secret: %v", err)
+ }
+ if err := reader.Get(
+ context.Background(),
+ client.ObjectKey{Name: secret.Name, Namespace: secret.Namespace},
+ &corev1.Secret{},
+ ); err == nil {
+ t.Fatal("bound secret drift was not applied")
+ }
+}
+
+func TestRoleSyncFailureUpdatesRoleCondition(t *testing.T) {
+ t.Parallel()
+ scheme := runtime.NewScheme()
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add user scheme: %v", err)
+ }
+ if err := rbacv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add RBAC scheme: %v", err)
+ }
+ user := &userv1.User{
+ ObjectMeta: metav1.ObjectMeta{Name: "alice", UID: "user-a"},
+ }
+ baseClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(user).Build()
+ r := &UserReconciler{
+ Client: roleSyncErrorClient{Client: baseClient},
+ Scheme: scheme,
+ Recorder: record.NewFakeRecorder(10),
+ }
+ state := &userReconcileState{}
+ r.syncRolesIfNeeded(context.Background(), user, state)
+ if state.syncError == nil {
+ t.Fatal("role sync failure was not recorded")
+ }
+ condition := helper.GetCondition(
+ user.Status.Conditions,
+ &userv1.Condition{Type: roleSyncReadyCondition},
+ )
+ if condition.Status != corev1.ConditionFalse {
+ t.Fatalf("role condition status = %s, want False", condition.Status)
+ }
+ if condition.Reason != "SyncUserError" {
+ t.Fatalf("role condition reason = %q, want SyncUserError", condition.Reason)
+ }
+}
+
+func TestRoleBindingSyncFailureRecordsRetryableError(t *testing.T) {
+ t.Parallel()
+ scheme := runtime.NewScheme()
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add user scheme: %v", err)
+ }
+ if err := rbacv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add RBAC scheme: %v", err)
+ }
+ user := &userv1.User{
+ ObjectMeta: metav1.ObjectMeta{Name: "alice", UID: "user-a"},
+ }
+ baseClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(user).Build()
+ r := &UserReconciler{
+ Client: roleBindingSyncErrorClient{Client: baseClient},
+ Scheme: scheme,
+ Recorder: record.NewFakeRecorder(10),
+ }
+ state := &userReconcileState{}
+ r.syncRoleBindingIfNeeded(context.Background(), user, state)
+ if state.syncError == nil {
+ t.Fatal("role binding sync failure was not recorded")
+ }
+ condition := helper.GetCondition(
+ user.Status.Conditions,
+ &userv1.Condition{Type: roleBindingReadyCondition},
+ )
+ if condition.Status != corev1.ConditionFalse {
+ t.Fatalf("role binding condition status = %s, want False", condition.Status)
+ }
+}
+
+func TestNamespaceToUserRequests(t *testing.T) {
+ t.Parallel()
+ r := &UserReconciler{}
+ requests := r.namespaceToUserRequests(context.Background(), &metav1.PartialObjectMetadata{
+ ObjectMeta: metav1.ObjectMeta{Name: "ns-admin"},
+ })
+ if len(requests) != 1 || requests[0].Name != "admin" {
+ t.Fatalf("admin namespace request = %#v", requests)
+ }
+ if requests := r.namespaceToUserRequests(context.Background(), &metav1.PartialObjectMetadata{
+ ObjectMeta: metav1.ObjectMeta{Name: "kube-system"},
+ }); requests != nil {
+ t.Fatalf("unmanaged namespace requests = %#v", requests)
+ }
+}
+
+func TestReconcileStrictNamespacePodSecurity(t *testing.T) {
+ t.Parallel()
+ scheme := runtime.NewScheme()
+ if err := corev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add core scheme: %v", err)
+ }
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add user scheme: %v", err)
+ }
+ ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
+ Name: "ns-external",
+ Labels: map[string]string{"example.com/keep": "value"},
+ }}
+ cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ns).Build()
+ r := &UserReconciler{Client: cli, EnableStrictNamespacePodSecurity: true}
+ if _, err := r.Reconcile(
+ context.Background(),
+ ctrl.Request{NamespacedName: client.ObjectKey{Name: "external"}},
+ ); err != nil {
+ t.Fatalf("reconcile orphan namespace: %v", err)
+ }
+ got := &corev1.Namespace{}
+ if err := cli.Get(context.Background(), client.ObjectKey{Name: ns.Name}, got); err != nil {
+ t.Fatalf("get orphan namespace: %v", err)
+ }
+ if got.Labels[config.PodSecurityLabelPrefix+"enforce"] != "baseline" ||
+ got.Labels["example.com/keep"] != "value" {
+ t.Fatalf("orphan namespace labels = %#v", got.Labels)
+ }
+}
+
+func TestReconcileDisabledStrictNamespacePodSecurityPreservesOrphanLabels(t *testing.T) {
+ t.Parallel()
+ scheme := runtime.NewScheme()
+ if err := corev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add core scheme: %v", err)
+ }
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add user scheme: %v", err)
+ }
+ ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
+ Name: "ns-external",
+ Labels: map[string]string{config.PodSecurityLabelPrefix + "enforce": "privileged"},
+ }}
+ cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ns).Build()
+ r := &UserReconciler{Client: cli}
+ if _, err := r.Reconcile(
+ context.Background(),
+ ctrl.Request{NamespacedName: client.ObjectKey{Name: "external"}},
+ ); err != nil {
+ t.Fatalf("reconcile orphan namespace with strict mode disabled: %v", err)
+ }
+ got := &corev1.Namespace{}
+ if err := cli.Get(context.Background(), client.ObjectKey{Name: ns.Name}, got); err != nil {
+ t.Fatalf("get orphan namespace: %v", err)
+ }
+ if got.Labels[config.PodSecurityLabelPrefix+"enforce"] != "privileged" {
+ t.Fatalf("orphan namespace labels changed: %#v", got.Labels)
+ }
+}
+
+func TestAdminClusterRoleBindingPredicate(t *testing.T) {
+ t.Parallel()
+ p := AdminClusterRoleBindingPredicate{}
+ target := &metav1.PartialObjectMetadata{
+ ObjectMeta: metav1.ObjectMeta{Name: adminClusterRoleBindingName},
+ }
+ other := &metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: "other-binding"}}
+ if !p.Create(event.CreateEvent{Object: target}) ||
+ !p.Update(event.UpdateEvent{ObjectNew: target}) ||
+ !p.Delete(event.DeleteEvent{Object: target}) {
+ t.Fatal("admin cluster role binding events were not accepted")
+ }
+ if p.Create(event.CreateEvent{Object: other}) ||
+ p.Update(event.UpdateEvent{ObjectNew: other}) ||
+ p.Delete(event.DeleteEvent{Object: other}) {
+ t.Fatal("unrelated cluster role binding event was accepted")
+ }
+}
+
+func TestDesiredNamespaceLabels(t *testing.T) {
+ t.Parallel()
+ labels := map[string]string{
+ config.PodSecurityLabelPrefix + "enforce": "baseline",
+ "example.com/keep": "value",
+ }
+ adminLabels := desiredNamespaceLabels("ns-admin", labels, false)
+ if adminLabels[config.PodSecurityLabelPrefix+"enforce"] != "baseline" {
+ t.Fatal(
+ "admin namespace did not receive Pod Security labels when admin privilege is disabled",
+ )
+ }
+ privilegedLabels := desiredNamespaceLabels("ns-admin", adminLabels, true)
+ if _, ok := privilegedLabels[config.PodSecurityLabelPrefix+"enforce"]; ok {
+ t.Fatal("admin Pod Security labels were retained when admin privilege is enabled")
+ }
+ if privilegedLabels["example.com/keep"] != "value" {
+ t.Fatal("unrelated namespace label was removed")
+ }
+}
+
+func TestAdminClusterRoleBindingCleanup(t *testing.T) {
+ t.Parallel()
+ scheme := runtime.NewScheme()
+ if err := rbacv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add RBAC scheme: %v", err)
+ }
+ binding := &rbacv1.ClusterRoleBinding{
+ ObjectMeta: metav1.ObjectMeta{Name: adminClusterRoleBindingName},
+ }
+ cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(binding).Build()
+ cleanup := &adminPrivilegeMigration{client: cli}
+ if err := cleanup.Start(context.Background()); err != nil {
+ t.Fatalf("cleanup binding: %v", err)
+ }
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKey{Name: adminClusterRoleBindingName},
+ &rbacv1.ClusterRoleBinding{},
+ ); !apierrors.IsNotFound(
+ err,
+ ) {
+ t.Fatalf("binding still exists, get error = %v", err)
+ }
+}
+
+func TestReconcileMissingAdminCleansDisabledClusterRoleBinding(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ enableAdmin bool
+ bindingExists bool
+ wantBinding bool
+ }{
+ {
+ name: "disabled admin privilege removes binding",
+ bindingExists: true,
+ },
+ {
+ name: "disabled admin privilege tolerates missing binding",
+ },
+ {
+ name: "enabled admin privilege preserves binding",
+ enableAdmin: true,
+ bindingExists: true,
+ wantBinding: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ scheme := runtime.NewScheme()
+ if err := rbacv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add RBAC scheme: %v", err)
+ }
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add user scheme: %v", err)
+ }
+
+ var objects []client.Object
+ if tt.bindingExists {
+ objects = append(objects, &rbacv1.ClusterRoleBinding{
+ ObjectMeta: metav1.ObjectMeta{Name: adminClusterRoleBindingName},
+ })
+ }
+ cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
+ r := &UserReconciler{
+ Client: cli,
+ EnableAdminClusterAdmin: tt.enableAdmin,
+ }
+
+ if _, err := r.Reconcile(context.Background(), ctrl.Request{
+ NamespacedName: client.ObjectKey{Name: adminUserName},
+ }); err != nil {
+ t.Fatalf("reconcile missing admin: %v", err)
+ }
+
+ err := cli.Get(
+ context.Background(),
+ client.ObjectKey{Name: adminClusterRoleBindingName},
+ &rbacv1.ClusterRoleBinding{},
+ )
+ if tt.wantBinding {
+ if err != nil {
+ t.Fatalf("get preserved binding: %v", err)
+ }
+ return
+ }
+ if !apierrors.IsNotFound(err) {
+ t.Fatalf("binding was not removed, get error = %v", err)
+ }
+ })
+ }
+}
+
+func TestAdminPrivilegeMigrationDisablesLegacyBinding(t *testing.T) {
+ t.Parallel()
+ scheme := runtime.NewScheme()
+ if err := corev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add core scheme: %v", err)
+ }
+ if err := rbacv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add RBAC scheme: %v", err)
+ }
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add user scheme: %v", err)
+ }
+ cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
+ &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: adminUserName}},
+ &rbacv1.ClusterRoleBinding{
+ ObjectMeta: metav1.ObjectMeta{Name: adminClusterRoleBindingName},
+ },
+ ).
+ Build()
+ r := &UserReconciler{Client: cli, Scheme: scheme}
+ migration := &adminPrivilegeMigration{
+ client: cli,
+ reader: cli,
+ reconciler: r,
+ }
+ if err := migration.Start(context.Background()); err != nil {
+ t.Fatalf("disable admin privilege: %v", err)
+ }
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKey{Name: adminClusterRoleBindingName},
+ &rbacv1.ClusterRoleBinding{},
+ ); !apierrors.IsNotFound(
+ err,
+ ) {
+ t.Fatalf("legacy binding still exists, get error = %v", err)
+ }
+ ns := &corev1.Namespace{}
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKey{Name: config.GetUsersNamespace(adminUserName)},
+ ns,
+ ); err != nil {
+ t.Fatalf("get admin namespace: %v", err)
+ }
+ if ns.Labels[config.PodSecurityLabelPrefix+"enforce"] != "baseline" {
+ t.Fatalf("admin namespace labels = %#v", ns.Labels)
+ }
+}
+
+func TestAdminPrivilegeMigrationEnablesLegacyBinding(t *testing.T) {
+ t.Parallel()
+ scheme := runtime.NewScheme()
+ if err := corev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add core scheme: %v", err)
+ }
+ if err := rbacv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add RBAC scheme: %v", err)
+ }
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add user scheme: %v", err)
+ }
+ cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
+ &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: adminUserName}},
+ ).Build()
+ r := &UserReconciler{Client: cli, Scheme: scheme, EnableAdminClusterAdmin: true}
+ migration := &adminPrivilegeMigration{
+ client: cli,
+ reader: cli,
+ reconciler: r,
+ enableAdminClusterAdmin: true,
+ }
+ if err := migration.Start(context.Background()); err != nil {
+ t.Fatalf("enable admin privilege: %v", err)
+ }
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKey{Name: adminClusterRoleBindingName},
+ &rbacv1.ClusterRoleBinding{},
+ ); err != nil {
+ t.Fatalf("get restored binding: %v", err)
+ }
+ ns := &corev1.Namespace{}
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKey{Name: config.GetUsersNamespace(adminUserName)},
+ ns,
+ ); err != nil {
+ t.Fatalf("get admin namespace: %v", err)
+ }
+ if _, ok := ns.Labels[config.PodSecurityLabelPrefix+"enforce"]; ok {
+ t.Fatalf("admin namespace retained Pod Security labels: %#v", ns.Labels)
+ }
+}
+
+func TestAdminPrivilegeMigrationPropagatesSyncError(t *testing.T) {
+ t.Parallel()
+ scheme := runtime.NewScheme()
+ if err := corev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add core scheme: %v", err)
+ }
+ if err := rbacv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add RBAC scheme: %v", err)
+ }
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add user scheme: %v", err)
+ }
+ baseClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
+ &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: adminUserName}},
+ ).Build()
+ failingClient := namespaceCreateErrorClient{Client: baseClient}
+ r := &UserReconciler{
+ Client: failingClient,
+ Scheme: scheme,
+ Recorder: record.NewFakeRecorder(10),
+ }
+ migration := &adminPrivilegeMigration{
+ client: failingClient,
+ reader: baseClient,
+ reconciler: r,
+ enableAdminClusterAdmin: true,
+ }
+ if err := migration.Start(context.Background()); err == nil {
+ t.Fatal("admin privilege migration reported success after namespace sync failed")
+ }
+}
+
+func TestPatchUserOwnerPreservesUncachedAnnotations(t *testing.T) {
+ t.Parallel()
+
+ scheme := runtime.NewScheme()
+ if err := userv1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add user scheme: %v", err)
+ }
+ stored := &userv1.User{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "user-a",
+ Annotations: map[string]string{
+ userv1.UserAnnotationOwnerKey: "old-owner",
+ userv1.UserAnnotationDisplayKey: "display-name",
+ },
+ },
+ }
+ cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(stored).Build()
+
+ projected := &userv1.User{}
+ if err := cli.Get(
+ context.Background(),
+ client.ObjectKeyFromObject(stored),
+ projected,
+ ); err != nil {
+ t.Fatalf("get user: %v", err)
+ }
+ projected.Annotations = map[string]string{
+ userv1.UserAnnotationOwnerKey: projected.Annotations[userv1.UserAnnotationOwnerKey],
+ }
+
+ reconciler := &OperationReqReconciler{Client: cli}
+ if err := reconciler.patchUserOwner(context.Background(), projected, "new-owner"); err != nil {
+ t.Fatalf("patch user owner: %v", err)
+ }
+
+ got := &userv1.User{}
+ if err := cli.Get(context.Background(), client.ObjectKeyFromObject(stored), got); err != nil {
+ t.Fatalf("get updated user: %v", err)
+ }
+ if got.Annotations[userv1.UserAnnotationOwnerKey] != "new-owner" {
+ t.Fatalf("owner = %q, want new-owner", got.Annotations[userv1.UserAnnotationOwnerKey])
+ }
+ if got.Annotations[userv1.UserAnnotationDisplayKey] != "display-name" {
+ t.Fatalf(
+ "display annotation = %q, want display-name",
+ got.Annotations[userv1.UserAnnotationDisplayKey],
+ )
+ }
+}
diff --git a/controllers/user/deploy/Kubefile b/controllers/user/deploy/Kubefile
new file mode 100644
index 000000000000..e4e4c2d632f3
--- /dev/null
+++ b/controllers/user/deploy/Kubefile
@@ -0,0 +1,9 @@
+FROM scratch
+
+USER 65532:65532
+
+COPY registry registry
+COPY charts charts
+COPY drop drop
+COPY user-controller-entrypoint.sh user-controller-entrypoint.sh
+CMD ["bash user-controller-entrypoint.sh"]
diff --git a/controllers/user/deploy/README.md b/controllers/user/deploy/README.md
new file mode 100644
index 000000000000..ad95de6c5a97
--- /dev/null
+++ b/controllers/user/deploy/README.md
@@ -0,0 +1,16 @@
+### How to build image
+
+```shell
+sealos build -t docker.io/labring/sealos-user-controller:latest -f Kubefile .
+```
+
+### How to run
+
+```shell
+# 可选:使用 HELM_OPTS 传递 Helm 参数,例如覆盖云 API Server 域名/端口。
+# adminClusterAdmin.enabled 默认关闭;只有兼容旧部署时才建议显式开启。
+# strictNamespacePodSecurity.enabled 默认开启,用于给所有 ns-* Namespace 添加 PSA labels。
+# export HELM_OPTS="--set cloudAPIServerDomain=my.domain --set cloudAPIServerPort=6443 --set kubeAPI.qps=50 --set kubeAPI.burst=100 --set adminClusterAdmin.enabled=true"
+
+sealos run docker.io/labring/sealos-user-controller:latest
+```
diff --git a/controllers/user/deploy/charts/user-controller/.helmignore b/controllers/user/deploy/charts/user-controller/.helmignore
new file mode 100644
index 000000000000..5206f4e91245
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/.helmignore
@@ -0,0 +1,21 @@
+# Patterns to ignore when building packages.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/controllers/user/deploy/charts/user-controller/Chart.yaml b/controllers/user/deploy/charts/user-controller/Chart.yaml
new file mode 100644
index 000000000000..d65e133a54bd
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: user
+description: Helm chart for the sealos user controller
+type: application
+version: 0.1.0
+appVersion: "0.1.0"
diff --git a/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_deleterequests.yaml b/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_deleterequests.yaml
new file mode 100644
index 000000000000..c1ba1c5112e6
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_deleterequests.yaml
@@ -0,0 +1,69 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.14.0
+ name: deleterequests.user.sealos.io
+spec:
+ group: user.sealos.io
+ names:
+ kind: DeleteRequest
+ listKind: DeleteRequestList
+ plural: deleterequests
+ singular: deleterequest
+ scope: Cluster
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .spec.user
+ name: User
+ type: string
+ - jsonPath: .status.phase
+ name: Phase
+ type: string
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v1
+ schema:
+ openAPIV3Schema:
+ description: DeleteRequest is the Schema for the deleterequests API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: DeleteRequestSpec defines the desired state of DeleteRequest
+ properties:
+ user:
+ type: string
+ type: object
+ status:
+ description: DeleteRequestStatus defines the observed state of DeleteRequest
+ properties:
+ phase:
+ enum:
+ - Pending
+ - Processing
+ - Completed
+ - Failed
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_operationrequests.yaml b/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_operationrequests.yaml
new file mode 100644
index 000000000000..6b98fd95054a
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_operationrequests.yaml
@@ -0,0 +1,95 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.14.0
+ name: operationrequests.user.sealos.io
+spec:
+ group: user.sealos.io
+ names:
+ kind: Operationrequest
+ listKind: OperationrequestList
+ plural: operationrequests
+ singular: operationrequest
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .spec.action
+ name: Action
+ type: string
+ - jsonPath: .spec.namespace
+ name: Namespace
+ type: string
+ - jsonPath: .spec.user
+ name: User
+ type: string
+ - jsonPath: .spec.role
+ name: Role
+ type: string
+ - jsonPath: .status.phase
+ name: Phase
+ type: string
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v1
+ schema:
+ openAPIV3Schema:
+ description: Operationrequest is the Schema for the operation requests API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: OperationrequestSpec defines the desired state of Operationrequest
+ properties:
+ action:
+ enum:
+ - Grant
+ - Update
+ - Deprive
+ type: string
+ namespace:
+ description: Namespace is the workspace that needs to be operated.
+ type: string
+ role:
+ enum:
+ - Owner
+ - Manager
+ - Developer
+ type: string
+ user:
+ type: string
+ type: object
+ status:
+ description: OperationrequestStatus defines the observed state of Operationrequest
+ properties:
+ phase:
+ default: Pending
+ description: Phase is the recently observed lifecycle phase of operationrequest.
+ enum:
+ - Pending
+ - Processing
+ - Completed
+ - Failed
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_users.yaml b/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_users.yaml
new file mode 100644
index 000000000000..2913f4f3150d
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_users.yaml
@@ -0,0 +1,142 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.14.0
+ name: users.user.sealos.io
+spec:
+ group: user.sealos.io
+ names:
+ kind: User
+ listKind: UserList
+ plural: users
+ singular: user
+ scope: Cluster
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .status.phase
+ name: Phase
+ type: string
+ - jsonPath: .status.observedCSRExpirationSeconds
+ name: ExpirationSeconds
+ type: integer
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v1
+ schema:
+ openAPIV3Schema:
+ description: User is the Schema for the users API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: UserSpec defines the desired state of User
+ properties:
+ csrExpirationSeconds:
+ default: 1000000000
+ description: |-
+ expirationSeconds is the requested duration of validity of the issued
+ kubeconfig credential. The issuer may issue a credential with a different
+ validity duration so a client must check the issued credential to determine
+ the actual duration.
+
+
+ The minimum effective value for expirationSeconds is 1_000_000_000 seconds.
+ format: int32
+ type: integer
+ kubeConfigRotateAt:
+ description: kubeConfigRotateAt is a manual trigger for kubeconfig
+ rotation. When set/updated, controller will request a new token
+ and recreate kubeconfig.
+ format: date-time
+ type: string
+ type: object
+ status:
+ description: UserStatus defines the observed state of User
+ properties:
+ conditions:
+ description: Conditions contains the different condition statuses
+ for this user.
+ items:
+ properties:
+ lastHeartbeatTime:
+ description: LastHeartbeatTime is the last time this condition
+ was updated.
+ format: date-time
+ type: string
+ lastTransitionTime:
+ description: LastTransitionTime is the last time the condition
+ changed from one status to another.
+ format: date-time
+ type: string
+ message:
+ description: Message is a human-readable message indicating
+ details about the last status change.
+ type: string
+ reason:
+ description: Reason is a (brief) reason for the condition's
+ last status change.
+ type: string
+ status:
+ description: Status is the status of the condition. One of True,
+ False, Unknown.
+ type: string
+ type:
+ type: string
+ required:
+ - status
+ - type
+ type: object
+ type: array
+ kubeConfig:
+ type: string
+ observedCSRExpirationSeconds:
+ default: 1000000000
+ format: int32
+ type: integer
+ observedKubeConfigRotateAt:
+ description: ObservedKubeConfigRotateAt records the last processed
+ rotation request.
+ format: date-time
+ type: string
+ kubeConfigRefreshAt:
+ description: KubeConfigRefreshAt is the next time the controller
+ should refresh the kubeconfig token.
+ format: date-time
+ type: string
+ observedKubeConfigSecretUID:
+ description: ObservedKubeConfigSecretUID identifies the Secret bound
+ to the current kubeconfig token.
+ type: string
+ observedGeneration:
+ description: The generation observed by the user controller.
+ format: int64
+ type: integer
+ phase:
+ default: Unknown
+ description: Phase is the recently observed lifecycle phase of user
+ type: string
+ required:
+ - kubeConfig
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/controllers/user/deploy/charts/user-controller/templates/_helpers.tpl b/controllers/user/deploy/charts/user-controller/templates/_helpers.tpl
new file mode 100644
index 000000000000..d51131e97224
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/templates/_helpers.tpl
@@ -0,0 +1,60 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "user.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+*/}}
+{{- define "user.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "user.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "user.labels" -}}
+helm.sh/chart: {{ include "user.chart" . }}
+{{ include "user.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "user.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "user.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "user.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "user.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/controllers/user/deploy/charts/user-controller/templates/cert.yaml b/controllers/user/deploy/charts/user-controller/templates/cert.yaml
new file mode 100644
index 000000000000..a8334bd3d42b
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/templates/cert.yaml
@@ -0,0 +1,53 @@
+apiVersion: cert-manager.io/v1
+kind: Issuer
+metadata:
+ name: {{ include "user.fullname" . }}-selfsigned-issuer
+ labels:
+ control-plane: controller-manager
+ {{- include "user.labels" . | nindent 4 }}
+spec:
+ selfSigned: {}
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+ name: {{ include "user.fullname" . }}-serving-cert
+ labels:
+ control-plane: controller-manager
+ {{- include "user.labels" . | nindent 4 }}
+spec:
+ dnsNames:
+ - {{ include "user.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc
+ - {{ include "user.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc.cluster.local
+ issuerRef:
+ kind: Issuer
+ name: {{ include "user.fullname" . }}-selfsigned-issuer
+ secretName: webhook-server-cert
+---
+{{- if .Values.metrics.enabled }}
+apiVersion: cert-manager.io/v1
+kind: Issuer
+metadata:
+ labels:
+ {{- include "user.labels" . | nindent 4 }}
+ name: selfsigned-issuer
+spec:
+ selfSigned: {}
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+ labels:
+ {{- include "user.labels" . | nindent 4 }}
+ name: metrics-certs
+spec:
+ privateKey:
+ rotationPolicy: Always
+ dnsNames:
+ - {{ include "user.fullname" . }}-metrics.{{.Release.Namespace}}.svc
+ - {{ include "user.fullname" . }}-metrics.{{.Release.Namespace}}.svc.cluster.local
+ issuerRef:
+ kind: Issuer
+ name: selfsigned-issuer
+ secretName: {{.Values.metrics.secretName}}
+{{- end }}
\ No newline at end of file
diff --git a/controllers/user/deploy/charts/user-controller/templates/deployment.yaml b/controllers/user/deploy/charts/user-controller/templates/deployment.yaml
new file mode 100644
index 000000000000..2b48b9c12cdf
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/templates/deployment.yaml
@@ -0,0 +1,131 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "user.fullname" . }}-controller-manager
+ labels:
+ control-plane: controller-manager
+ {{- include "user.labels" . | nindent 4 }}
+spec:
+ replicas: {{ .Values.replicaCount }}
+ selector:
+ matchLabels:
+ control-plane: controller-manager
+ {{- include "user.selectorLabels" . | nindent 6 }}
+ template:
+ metadata:
+ annotations:
+ kubectl.kubernetes.io/default-container: manager
+ {{- with .Values.podAnnotations }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ labels:
+ control-plane: controller-manager
+ {{- include "user.labels" . | nindent 8 }}
+ {{- with .Values.podLabels }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ spec:
+ {{- with .Values.imagePullSecrets }}
+ imagePullSecrets:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ serviceAccountName: {{ include "user.serviceAccountName" . }}
+ securityContext:
+ {{- toYaml .Values.podSecurityContext | nindent 8 }}
+ containers:
+ - name: manager
+ command:
+ - /manager
+ args:
+ - --health-probe-bind-address=:8081
+ - --leader-elect
+ - --kube-api-qps={{ .Values.kubeAPI.qps }}
+ - --kube-api-burst={{ .Values.kubeAPI.burst }}
+ - --enable-admin-cluster-admin={{ .Values.adminClusterAdmin.enabled }}
+ - --enable-strict-namespace-pod-security={{ .Values.strictNamespacePodSecurity.enabled }}
+ {{- if .Values.metrics.enabled }}
+ - --metrics-secure=true
+ - --metrics-bind-address=:8443
+ {{- end }}
+ env:
+ - name: NAMESPACE_NAME
+ valueFrom:
+ fieldRef:
+ apiVersion: v1
+ fieldPath: metadata.namespace
+ - name: SEALOS_CLOUD_APISERVER_HOST
+ value: "{{.Values.cloudAPIServerDomain}}"
+ - name: SEALOS_CLOUD_APISERVER_PORT
+ value: "{{.Values.cloudAPIServerPort}}"
+ ports:
+ - containerPort: 9443
+ name: webhook-server
+ protocol: TCP
+ image: "{{ .Values.image }}"
+ imagePullPolicy: IfNotPresent
+ livenessProbe:
+ {{- toYaml .Values.livenessProbe | nindent 12 }}
+ readinessProbe:
+ {{- toYaml .Values.readinessProbe | nindent 12 }}
+ resources:
+ {{- toYaml .Values.resources | nindent 12 }}
+ securityContext:
+ {{- toYaml .Values.securityContext | nindent 12 }}
+ volumeMounts:
+ {{- with .Values.volumeMounts }}
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- if .Values.metrics.enabled }}
+ - name: metrics-certs
+ mountPath: {{.Values.metrics.certPath}}
+ readOnly: true
+ {{- end }}
+ - mountPath: /tmp/k8s-webhook-server/serving-certs
+ name: cert
+ readOnly: true
+ terminationGracePeriodSeconds: 10
+ affinity:
+ {{- if .Values.affinity }}
+ {{- toYaml .Values.affinity | nindent 8 }}
+ {{- else }}
+ podAntiAffinity:
+ preferredDuringSchedulingIgnoredDuringExecution:
+ - weight: 100
+ podAffinityTerm:
+ labelSelector:
+ matchExpressions:
+ - key: control-plane
+ operator: In
+ values:
+ - controller-manager
+ topologyKey: kubernetes.io/hostname
+ {{- end }}
+ {{- with .Values.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ volumes:
+ {{- if .Values.metrics.enabled }}
+ - name: metrics-certs
+ secret:
+ secretName: {{.Values.metrics.secretName}}
+ optional: false
+ items:
+ - key: ca.crt
+ path: ca.crt
+ - key: tls.crt
+ path: tls.crt
+ - key: tls.key
+ path: tls.key
+ {{- end }}
+ {{- with .Values.volumes }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ - name: cert
+ secret:
+ defaultMode: 420
+ secretName: webhook-server-cert
diff --git a/controllers/user/deploy/charts/user-controller/templates/rbac.yaml b/controllers/user/deploy/charts/user-controller/templates/rbac.yaml
new file mode 100644
index 000000000000..8382490aacf0
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/templates/rbac.yaml
@@ -0,0 +1,82 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: {{ include "user.fullname" . }}-leader-election-role
+ namespace: {{ .Release.Namespace }}
+ labels:
+ control-plane: controller-manager
+ {{- include "user.labels" . | nindent 4 }}
+rules:
+ - apiGroups:
+ - ""
+ resources:
+ - configmaps
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+ - apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+ - apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ include "user.fullname" . }}-manager-role
+rules:
+ - apiGroups:
+ - "*"
+ resources:
+ - "*"
+ verbs:
+ - "*"
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: {{ include "user.fullname" . }}-leader-election-rolebinding
+ namespace: {{ .Release.Namespace }}
+ labels:
+ control-plane: controller-manager
+ {{- include "user.labels" . | nindent 4 }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: {{ include "user.fullname" . }}-leader-election-role
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "user.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "user.fullname" . }}-manager-rolebinding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: {{ include "user.fullname" . }}-manager-role
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "user.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
diff --git a/controllers/user/deploy/charts/user-controller/templates/service.yaml b/controllers/user/deploy/charts/user-controller/templates/service.yaml
new file mode 100644
index 000000000000..73986aaf9189
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/templates/service.yaml
@@ -0,0 +1,34 @@
+{{- if .Values.metrics.enabled }}
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "user.fullname" . }}-controller-manager-metrics-service
+ labels:
+ control-plane: controller-manager
+ {{- include "user.labels" . | nindent 4 }}
+spec:
+ ports:
+ - name: https
+ port: {{ .Values.service.metrics.port }}
+ protocol: TCP
+ targetPort: https
+ selector:
+ control-plane: controller-manager
+ {{- include "user.selectorLabels" . | nindent 4 }}
+{{- end }}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "user.fullname" . }}-webhook-service
+ labels:
+ control-plane: controller-manager
+ {{- include "user.labels" . | nindent 4 }}
+spec:
+ ports:
+ - port: {{ .Values.service.webhook.port }}
+ protocol: TCP
+ targetPort: 9443
+ selector:
+ control-plane: controller-manager
+ {{- include "user.selectorLabels" . | nindent 4 }}
diff --git a/controllers/user/deploy/charts/user-controller/templates/serviceaccount.yaml b/controllers/user/deploy/charts/user-controller/templates/serviceaccount.yaml
new file mode 100644
index 000000000000..e66d5ac343b1
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/templates/serviceaccount.yaml
@@ -0,0 +1,13 @@
+{{- if .Values.serviceAccount.create }}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ include "user.serviceAccountName" . }}
+ labels:
+ {{- include "user.labels" . | nindent 4 }}
+ {{- with .Values.serviceAccount.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
+{{- end }}
diff --git a/controllers/user/deploy/charts/user-controller/templates/webhook.yaml b/controllers/user/deploy/charts/user-controller/templates/webhook.yaml
new file mode 100644
index 000000000000..42515b21fa25
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/templates/webhook.yaml
@@ -0,0 +1,96 @@
+apiVersion: admissionregistration.k8s.io/v1
+kind: MutatingWebhookConfiguration
+metadata:
+ name: {{ include "user.fullname" . }}-mutating-webhook-configuration
+ annotations:
+ cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "user.fullname" . }}-serving-cert
+webhooks:
+ - name: moperationrequest.kb.io
+ admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: {{ include "user.fullname" . }}-webhook-service
+ namespace: {{ .Release.Namespace }}
+ path: /mutate-user-sealos-io-v1-operationrequest
+ failurePolicy: Fail
+ rules:
+ - apiGroups:
+ - user.sealos.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - operationrequests
+ sideEffects: None
+ - name: muser.kb.io
+ admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: {{ include "user.fullname" . }}-webhook-service
+ namespace: {{ .Release.Namespace }}
+ path: /mutate-user-sealos-io-v1-user
+ failurePolicy: Fail
+ rules:
+ - apiGroups:
+ - user.sealos.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - users
+ sideEffects: None
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+ name: {{ include "user.fullname" . }}-validating-webhook-configuration
+ annotations:
+ cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "user.fullname" . }}-serving-cert
+webhooks:
+ - name: voperationrequest.kb.io
+ admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: {{ include "user.fullname" . }}-webhook-service
+ namespace: {{ .Release.Namespace }}
+ path: /validate-user-sealos-io-v1-operationrequest
+ failurePolicy: Fail
+ rules:
+ - apiGroups:
+ - user.sealos.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - operationrequests
+ sideEffects: None
+ - name: vuser.kb.io
+ admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: {{ include "user.fullname" . }}-webhook-service
+ namespace: {{ .Release.Namespace }}
+ path: /validate-user-sealos-io-v1-user
+ timeoutSeconds: 30
+ failurePolicy: Fail
+ rules:
+ - apiGroups:
+ - user.sealos.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - users
+ sideEffects: None
diff --git a/controllers/user/deploy/charts/user-controller/user-controller-values.yaml b/controllers/user/deploy/charts/user-controller/user-controller-values.yaml
new file mode 100644
index 000000000000..d1c20f818aa4
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/user-controller-values.yaml
@@ -0,0 +1,25 @@
+# Custom values for user controller helm chart.
+# This file contains user-customizable configurations.
+
+replicaCount: 1
+
+resources:
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ requests:
+ cpu: 10m
+ memory: 64Mi
+
+kubeAPI:
+ qps: 50
+ burst: 100
+
+# Preserve the legacy cluster-admin binding for the admin user.
+# When disabled, the admin namespace uses Pod Security Admission labels and
+# any sealos-cloudadmin ClusterRoleBinding is removed.
+adminClusterAdmin:
+ enabled: false
+
+strictNamespacePodSecurity:
+ enabled: true
diff --git a/controllers/user/deploy/charts/user-controller/values.yaml b/controllers/user/deploy/charts/user-controller/values.yaml
new file mode 100644
index 000000000000..f179518fc324
--- /dev/null
+++ b/controllers/user/deploy/charts/user-controller/values.yaml
@@ -0,0 +1,80 @@
+# Default values for user controller helm chart.
+
+image: ghcr.io/labring/sealos-user-controller:latest
+
+imagePullSecrets: []
+fullnameOverride: "user"
+
+serviceAccount:
+ create: true
+ automount: true
+ annotations: {}
+ name: "user-controller-manager"
+
+podAnnotations: {}
+podLabels: {}
+
+podSecurityContext:
+ runAsNonRoot: true
+
+securityContext:
+ runAsNonRoot: true
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - "ALL"
+
+service:
+ metrics:
+ port: 8443
+ webhook:
+ port: 443
+
+livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 8081
+ initialDelaySeconds: 15
+ periodSeconds: 20
+
+readinessProbe:
+ httpGet:
+ path: /readyz
+ port: 8081
+ initialDelaySeconds: 5
+ periodSeconds: 10
+
+kubeAPI:
+ # Kubernetes API client-side rate limit used by the controller manager.
+ qps: 50
+ burst: 100
+
+# Preserve the legacy cluster-admin binding for the admin user.
+# When disabled, the admin namespace uses Pod Security Admission labels and
+# any sealos-cloudadmin ClusterRoleBinding is removed.
+adminClusterAdmin:
+ enabled: false
+
+# Apply Pod Security labels to all ns-* namespaces, including orphan namespaces.
+# Disabling this leaves existing labels unchanged.
+strictNamespacePodSecurity:
+ enabled: true
+
+# Cloud API server configuration
+# cloudAPIServerDomain is auto-configured by entrypoint.sh from sealos-config ConfigMap
+cloudAPIServerDomain: 127.0.0.1.nip.io
+cloudAPIServerPort: "6443"
+
+metrics:
+ # Enable metrics endpoint
+ enabled: false
+ # Path to the metrics certificate
+ certPath: /tmp/k8s-metrics-server/metrics-certs
+ secretName: metrics-server-cert
+
+nodeSelector: {}
+
+tolerations: []
+
+# If no affinity is provided a default podAntiAffinity is used to spread controllers.
+affinity: {}
diff --git a/controllers/user/deploy/drop/deploy.yaml b/controllers/user/deploy/drop/deploy.yaml
new file mode 100644
index 000000000000..c26bf3f19993
--- /dev/null
+++ b/controllers/user/deploy/drop/deploy.yaml
@@ -0,0 +1,470 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: user-controller-manager
+ namespace: user-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: user-leader-election-role
+ namespace: user-system
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - configmaps
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+- apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: user-manager-role
+rules:
+- apiGroups:
+ - '*'
+ resources:
+ - '*'
+ verbs:
+ - '*'
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - deleterequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - deleterequests/finalizers
+ verbs:
+ - update
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - deleterequests/status
+ verbs:
+ - get
+ - patch
+ - update
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - operationrequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - operationrequests/finalizers
+ verbs:
+ - update
+- apiGroups:
+ - user.sealos.io
+ resources:
+ - operationrequests/status
+ verbs:
+ - get
+ - patch
+ - update
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: user-metrics-reader
+rules:
+- nonResourceURLs:
+ - /metrics
+ verbs:
+ - get
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: user-proxy-role
+rules:
+- apiGroups:
+ - authentication.k8s.io
+ resources:
+ - tokenreviews
+ verbs:
+ - create
+- apiGroups:
+ - authorization.k8s.io
+ resources:
+ - subjectaccessreviews
+ verbs:
+ - create
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: user-leader-election-rolebinding
+ namespace: user-system
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: user-leader-election-role
+subjects:
+- kind: ServiceAccount
+ name: user-controller-manager
+ namespace: user-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: user-manager-rolebinding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: user-manager-role
+subjects:
+- kind: ServiceAccount
+ name: user-controller-manager
+ namespace: user-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: user-proxy-rolebinding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: user-proxy-role
+subjects:
+- kind: ServiceAccount
+ name: user-controller-manager
+ namespace: user-system
+---
+apiVersion: v1
+data:
+ controller_manager_config.yaml: |
+ apiVersion: controller-runtime.sigs.k8s.io/v1alpha1
+ kind: ControllerManagerConfig
+ health:
+ healthProbeBindAddress: :8081
+ metrics:
+ bindAddress: 127.0.0.1:8080
+ webhook:
+ port: 9443
+ leaderElection:
+ leaderElect: true
+ resourceName: 785548a1.sealos.io
+ # leaderElectionReleaseOnCancel defines if the leader should step down volume
+ # when the Manager ends. This requires the binary to immediately end when the
+ # Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
+ # speeds up voluntary leader transitions as the new leader don't have to wait
+ # LeaseDuration time first.
+ # In the default scaffold provided, the program ends immediately after
+ # the manager stops, so would be fine to enable this option. However,
+ # if you are doing or is intended to do any operation such as perform cleanups
+ # after the manager stops then its usage might be unsafe.
+ # leaderElectionReleaseOnCancel: true
+
+kind: ConfigMap
+metadata:
+ name: user-manager-config
+ namespace: user-system
+---
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ control-plane: controller-manager
+ name: user-controller-manager-metrics-service
+ namespace: user-system
+spec:
+ ports:
+ - name: https
+ port: 8443
+ protocol: TCP
+ targetPort: https
+ selector:
+ control-plane: controller-manager
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: user-webhook-service
+ namespace: user-system
+spec:
+ ports:
+ - port: 443
+ protocol: TCP
+ targetPort: 9443
+ selector:
+ control-plane: controller-manager
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ labels:
+ control-plane: controller-manager
+ name: user-controller-manager
+ namespace: user-system
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ control-plane: controller-manager
+ template:
+ metadata:
+ annotations:
+ kubectl.kubernetes.io/default-container: manager
+ labels:
+ control-plane: controller-manager
+ spec:
+ containers:
+ - args:
+ - --health-probe-bind-address=:8081
+ - --metrics-bind-address=127.0.0.1:8080
+ - --leader-elect
+ - --kube-api-qps=5
+ - --kube-api-burst=10
+ command:
+ - /manager
+ env:
+ - name: NAMESPACE_NAME
+ valueFrom:
+ fieldRef:
+ apiVersion: v1
+ fieldPath: metadata.namespace
+ image: ghcr.io/labring/sealos-user-controller:latest
+ imagePullPolicy: Always
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 8081
+ initialDelaySeconds: 15
+ periodSeconds: 20
+ name: manager
+ ports:
+ - containerPort: 9443
+ name: webhook-server
+ protocol: TCP
+ readinessProbe:
+ httpGet:
+ path: /readyz
+ port: 8081
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ resources:
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ requests:
+ cpu: 10m
+ memory: 64Mi
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - ALL
+ runAsNonRoot: true
+ volumeMounts:
+ - mountPath: /tmp/k8s-webhook-server/serving-certs
+ name: cert
+ readOnly: true
+ - args:
+ - --secure-listen-address=0.0.0.0:8443
+ - --upstream=http://127.0.0.1:8080/
+ - --logtostderr=true
+ - --v=0
+ image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0
+ name: kube-rbac-proxy
+ ports:
+ - containerPort: 8443
+ name: https
+ protocol: TCP
+ resources:
+ limits:
+ cpu: 1000m
+ memory: 1024Mi
+ requests:
+ cpu: 5m
+ memory: 64Mi
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - ALL
+ runAsNonRoot: true
+ securityContext:
+ runAsNonRoot: true
+ serviceAccountName: user-controller-manager
+ terminationGracePeriodSeconds: 10
+ affinity:
+ podAntiAffinity:
+ preferredDuringSchedulingIgnoredDuringExecution:
+ - weight: 100
+ podAffinityTerm:
+ labelSelector:
+ matchExpressions:
+ - key: "control-plane"
+ operator: In
+ values:
+ - controller-manager
+ topologyKey: "kubernetes.io/hostname"
+ volumes:
+ - name: cert
+ secret:
+ defaultMode: 420
+ secretName: webhook-server-cert
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+ name: user-serving-cert
+ namespace: user-system
+spec:
+ dnsNames:
+ - user-webhook-service.user-system.svc
+ - user-webhook-service.user-system.svc.cluster.local
+ issuerRef:
+ kind: Issuer
+ name: user-selfsigned-issuer
+ secretName: webhook-server-cert
+---
+apiVersion: cert-manager.io/v1
+kind: Issuer
+metadata:
+ name: user-selfsigned-issuer
+ namespace: user-system
+spec:
+ selfSigned: {}
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: MutatingWebhookConfiguration
+metadata:
+ annotations:
+ cert-manager.io/inject-ca-from: user-system/user-serving-cert
+ name: user-mutating-webhook-configuration
+webhooks:
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: user-webhook-service
+ namespace: user-system
+ path: /mutate-user-sealos-io-v1-operationrequest
+ failurePolicy: Fail
+ name: moperationrequest.kb.io
+ rules:
+ - apiGroups:
+ - user.sealos.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - operationrequests
+ sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: user-webhook-service
+ namespace: user-system
+ path: /mutate-user-sealos-io-v1-user
+ failurePolicy: Fail
+ name: muser.kb.io
+ rules:
+ - apiGroups:
+ - user.sealos.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - users
+ sideEffects: None
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+ annotations:
+ cert-manager.io/inject-ca-from: user-system/user-serving-cert
+ name: user-validating-webhook-configuration
+webhooks:
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: user-webhook-service
+ namespace: user-system
+ path: /validate-user-sealos-io-v1-operationrequest
+ failurePolicy: Fail
+ name: voperationrequest.kb.io
+ rules:
+ - apiGroups:
+ - user.sealos.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - operationrequests
+ sideEffects: None
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: user-webhook-service
+ namespace: user-system
+ path: /validate-user-sealos-io-v1-user
+ timeoutSeconds: 30
+ failurePolicy: Fail
+ name: vuser.kb.io
+ rules:
+ - apiGroups:
+ - user.sealos.io
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - users
+ sideEffects: None
diff --git a/controllers/user/deploy/drop/rbac.yaml b/controllers/user/deploy/drop/rbac.yaml
new file mode 100644
index 000000000000..33012a2237ba
--- /dev/null
+++ b/controllers/user/deploy/drop/rbac.yaml
@@ -0,0 +1,102 @@
+# Copyright © 2023 sealos.
+#
+# 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.
+
+# permissions for end users to edit users.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: user-editor-role
+rules:
+ - apiGroups:
+ - user.sealos.io
+ resources:
+ - users
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - user.sealos.io
+ resources:
+ - users/status
+ verbs:
+ - get
+---
+# permissions for end users to edit operationrequests.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: clusterrole
+ app.kubernetes.io/instance: operationrequest-editor-role
+ app.kubernetes.io/component: rbac
+ app.kubernetes.io/created-by: user
+ app.kubernetes.io/part-of: user
+ app.kubernetes.io/managed-by: kustomize
+ name: operationrequest-editor-role
+rules:
+ - apiGroups:
+ - user.sealos.io
+ resources:
+ - operationrequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - user.sealos.io
+ resources:
+ - operationrequests/status
+ verbs:
+ - get
+---
+# permissions for end users to edit deleterequests.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: clusterrole
+ app.kubernetes.io/instance: deleterequest-editor-role
+ app.kubernetes.io/component: rbac
+ app.kubernetes.io/created-by: user
+ app.kubernetes.io/part-of: user
+ app.kubernetes.io/managed-by: kustomize
+ name: deleterequest-editor-role
+rules:
+ - apiGroups:
+ - user.sealos.io
+ resources:
+ - deleterequests
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - user.sealos.io
+ resources:
+ - deleterequests/status
+ verbs:
+ - get
diff --git a/controllers/user/deploy/user-controller-entrypoint.sh b/controllers/user/deploy/user-controller-entrypoint.sh
new file mode 100644
index 000000000000..dafc54de1dad
--- /dev/null
+++ b/controllers/user/deploy/user-controller-entrypoint.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+set -eo pipefail
+
+HELM_OPTS=${HELM_OPTS:-""}
+HELM_OPTIONS=${HELM_OPTIONS:-""}
+RELEASE_NAME=${RELEASE_NAME:-"user-controller"}
+RELEASE_NAMESPACE=${RELEASE_NAMESPACE:-"user-system"}
+CHART_PATH=${CHART_PATH:-"./charts/user-controller"}
+HELM_SET_ARGS=()
+
+get_cm_value() {
+ local namespace="$1"
+ local name="$2"
+ local key="$3"
+ kubectl get configmap "${name}" -n "${namespace}" -o "jsonpath={.data.${key}}" 2>/dev/null || true
+}
+
+add_set_string() {
+ local key="$1"
+ local value="$2"
+ value=${value//\\/\\\\}
+ value=${value//,/\\,}
+ HELM_SET_ARGS+=(--set-string "${key}=${value}")
+}
+
+# Clean up old resources
+kubectl delete -f ./drop/ --ignore-not-found
+
+# Auto configure from sealos-config, then fall back to chart defaults.
+DEFAULT_SEALOS_CLOUD_DOMAIN="127.0.0.1.nip.io"
+DEFAULT_SEALOS_CLOUD_APISERVER_PORT="6443"
+
+SEALOS_CLOUD_DOMAIN=${SEALOS_CLOUD_DOMAIN:-"${cloudDomain:-$(get_cm_value sealos-system sealos-config cloudDomain)}"}
+SEALOS_CLOUD_DOMAIN=${SEALOS_CLOUD_DOMAIN:-"${DEFAULT_SEALOS_CLOUD_DOMAIN}"}
+SEALOS_CLOUD_APISERVER_PORT=${SEALOS_CLOUD_APISERVER_PORT:-"${apiserverPort:-$(get_cm_value sealos-system sealos-config apiserverPort)}"}
+SEALOS_CLOUD_APISERVER_PORT=${SEALOS_CLOUD_APISERVER_PORT:-"${DEFAULT_SEALOS_CLOUD_APISERVER_PORT}"}
+
+add_set_string cloudAPIServerDomain "${SEALOS_CLOUD_DOMAIN}"
+add_set_string cloudAPIServerPort "${SEALOS_CLOUD_APISERVER_PORT}"
+
+# Prepare values files
+SERVICE_NAME="user-controller"
+USER_VALUES_PATH="/root/.sealos/cloud/values/core/${SERVICE_NAME}-values.yaml"
+
+# Copy user values template if not exists
+if [ ! -f "${USER_VALUES_PATH}" ]; then
+ mkdir -p "$(dirname "${USER_VALUES_PATH}")"
+ cp "./charts/${SERVICE_NAME}/${SERVICE_NAME}-values.yaml" "${USER_VALUES_PATH}"
+fi
+
+# Deploy Helm chart
+helm upgrade -i "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" --create-namespace "${CHART_PATH}" \
+ -f "./charts/${SERVICE_NAME}/values.yaml" \
+ -f "${USER_VALUES_PATH}" \
+ "${HELM_SET_ARGS[@]}" \
+ ${HELM_OPTIONS} \
+ ${HELM_OPTS}
+
+# Apply CRDs
+helm show crds ./charts/${SERVICE_NAME} | kubectl apply -f - --server-side --force-conflicts
diff --git a/controllers/user/go.mod b/controllers/user/go.mod
new file mode 100644
index 000000000000..057d316db6bb
--- /dev/null
+++ b/controllers/user/go.mod
@@ -0,0 +1,72 @@
+module github.com/labring/sealos/controllers/user
+
+go 1.24.0
+
+require (
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
+ github.com/go-logr/logr v1.4.2
+ github.com/onsi/ginkgo v1.16.5
+ github.com/onsi/gomega v1.36.1
+ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
+ golang.org/x/time v0.7.0
+ gopkg.in/yaml.v3 v3.0.1
+ k8s.io/api v0.32.1
+ k8s.io/apimachinery v0.32.1
+ k8s.io/client-go v0.32.1
+ k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
+ sigs.k8s.io/controller-runtime v0.20.4
+)
+
+require (
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/emicklei/go-restful/v3 v3.11.0 // indirect
+ github.com/evanphx/json-patch v5.6.0+incompatible // indirect
+ github.com/evanphx/json-patch/v5 v5.9.11 // indirect
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/fxamacker/cbor/v2 v2.7.0 // indirect
+ github.com/go-logr/zapr v1.3.0 // indirect
+ github.com/go-openapi/jsonpointer v0.21.0 // indirect
+ github.com/go-openapi/jsonreference v0.20.2 // indirect
+ github.com/go-openapi/swag v0.23.0 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/btree v1.1.3 // indirect
+ github.com/google/gnostic-models v0.6.8 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
+ github.com/google/gofuzz v1.2.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/nxadm/tail v1.4.8 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/prometheus/client_golang v1.19.1 // indirect
+ github.com/prometheus/client_model v0.6.1 // indirect
+ github.com/prometheus/common v0.55.0 // indirect
+ github.com/prometheus/procfs v0.15.1 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ go.uber.org/zap v1.27.0 // indirect
+ golang.org/x/net v0.30.0 // indirect
+ golang.org/x/oauth2 v0.23.0 // indirect
+ golang.org/x/sync v0.8.0 // indirect
+ golang.org/x/sys v0.26.0 // indirect
+ golang.org/x/term v0.25.0 // indirect
+ golang.org/x/text v0.19.0 // indirect
+ gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
+ google.golang.org/protobuf v1.35.1 // indirect
+ gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+ gopkg.in/inf.v0 v0.9.1 // indirect
+ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
+ k8s.io/apiextensions-apiserver v0.32.1 // indirect
+ k8s.io/klog/v2 v2.130.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
+ sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
+ sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
+ sigs.k8s.io/yaml v1.4.0 // indirect
+)
diff --git a/controllers/user/go.sum b/controllers/user/go.sum
new file mode 100644
index 000000000000..993353c578c1
--- /dev/null
+++ b/controllers/user/go.sum
@@ -0,0 +1,239 @@
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
+github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
+github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
+github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
+github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
+github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
+golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
+golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
+gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc=
+k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k=
+k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw=
+k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto=
+k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs=
+k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU=
+k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
+k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
+k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
+sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/controllers/user/hack/boilerplate.go.txt b/controllers/user/hack/boilerplate.go.txt
new file mode 100644
index 000000000000..fb1b985cb87d
--- /dev/null
+++ b/controllers/user/hack/boilerplate.go.txt
@@ -0,0 +1,15 @@
+/*
+Copyright 2022 labring.
+
+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.
+*/
\ No newline at end of file
diff --git a/controllers/user/main.go b/controllers/user/main.go
new file mode 100644
index 000000000000..78f9da188299
--- /dev/null
+++ b/controllers/user/main.go
@@ -0,0 +1,332 @@
+/*
+Copyright 2022 labring.
+
+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 main
+
+import (
+ "crypto/tls"
+ "flag"
+ "os"
+ "time"
+
+ licensev1 "github.com/labring/sealos/controllers/license/api/v1"
+ userv1 "github.com/labring/sealos/controllers/user/api/v1"
+ "github.com/labring/sealos/controllers/user/controllers"
+ usercache "github.com/labring/sealos/controllers/user/controllers/cache"
+ ratelimiter "github.com/labring/sealos/controllers/user/controllers/helper/ratelimiter"
+ "k8s.io/apimachinery/pkg/runtime"
+ utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+ // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
+ // to ensure that exec-entrypoint and run can make use of them.
+ _ "k8s.io/client-go/plugin/pkg/client/auth"
+ "k8s.io/client-go/rest"
+ "k8s.io/utils/ptr"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/config"
+ "sigs.k8s.io/controller-runtime/pkg/healthz"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+ "sigs.k8s.io/controller-runtime/pkg/metrics/filters"
+ //+kubebuilder:scaffold:imports
+ metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+)
+
+var (
+ scheme = runtime.NewScheme()
+ setupLog = ctrl.Log.WithName("setup")
+)
+
+func init() {
+ utilruntime.Must(clientgoscheme.AddToScheme(scheme))
+
+ utilruntime.Must(licensev1.AddToScheme(scheme))
+ utilruntime.Must(userv1.AddToScheme(scheme))
+ //+kubebuilder:scaffold:scheme
+}
+
+func main() {
+ var (
+ metricsAddr string
+ pprofBindAddress string
+ enableLeaderElection bool
+ gracefulShutdownTimeout time.Duration
+ probeAddr string
+ rateLimiterOptions ratelimiter.RateLimiterOptions
+ syncPeriod time.Duration
+ minRequeueDuration time.Duration
+ maxRequeueDuration time.Duration
+ operationReqExpirationTime time.Duration
+ restartPredicateDuration time.Duration
+ operationReqRetentionTime time.Duration
+ kubeAPIQPS float64
+ kubeAPIBurst int
+ secureMetrics bool
+ enableHTTP2 bool
+ enableAdminClusterAdmin bool
+ enableStrictNamespacePSA bool
+ tlsOpts []func(*tls.Config)
+ )
+ flag.StringVar(
+ &metricsAddr,
+ "metrics-bind-address",
+ ":8080",
+ "The address the metric endpoint binds to.",
+ )
+ flag.StringVar(
+ &probeAddr,
+ "health-probe-bind-address",
+ ":8081",
+ "The address the probe endpoint binds to.",
+ )
+ flag.StringVar(
+ &pprofBindAddress,
+ "pprof-bind-address",
+ "",
+ "The address the pprof endpoint binds to. Empty disables pprof.",
+ )
+ flag.BoolVar(&enableLeaderElection, "leader-elect", false,
+ "Enable leader election for controller manager. "+
+ "Enabling this will ensure there is only one active controller manager.")
+ flag.DurationVar(
+ &gracefulShutdownTimeout,
+ "graceful-shutdown-timeout",
+ 8*time.Second,
+ "Maximum time to stop manager runnables before releasing leadership.",
+ )
+ flag.DurationVar(
+ &syncPeriod,
+ "sync-period",
+ 0,
+ "Informer synthetic resync period. Zero disables periodic resync.",
+ )
+ flag.DurationVar(
+ &minRequeueDuration,
+ "min-requeue-duration",
+ time.Hour*24,
+ "Retry interval for a User blocked by the license limit.",
+ )
+ flag.DurationVar(
+ &maxRequeueDuration,
+ "max-requeue-duration",
+ time.Hour*24*2,
+ "Deprecated: retained for compatibility and ignored.",
+ )
+ flag.DurationVar(
+ &operationReqExpirationTime,
+ "operation-req-expiration-time",
+ time.Minute*3,
+ "Sets the expiration time duration for an operation request. By default, the duration is set to 3 minutes.",
+ )
+ flag.DurationVar(
+ &operationReqRetentionTime,
+ "operation-req-retention-time",
+ time.Minute*3,
+ "Sets the retention time duration for an operation request. By default, the duration is set to 3 minutes.",
+ )
+ flag.DurationVar(
+ &restartPredicateDuration,
+ "restart-predicate-time",
+ time.Hour*2,
+ "Deprecated: retained for compatibility and ignored. User startup reconciliation processes all cached Users.",
+ )
+ flag.Float64Var(
+ &kubeAPIQPS,
+ "kube-api-qps",
+ float64(rest.DefaultQPS),
+ "Sets the Kubernetes API client QPS used by the manager and reconciler clients.",
+ )
+ flag.IntVar(
+ &kubeAPIBurst,
+ "kube-api-burst",
+ rest.DefaultBurst,
+ "Sets the Kubernetes API client burst used by the manager and reconciler clients.",
+ )
+ flag.BoolVar(
+ &secureMetrics,
+ "metrics-secure",
+ true,
+ "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.",
+ )
+ flag.BoolVar(&enableHTTP2, "enable-http2", false,
+ "If set, HTTP/2 will be enabled for the metrics and webhook servers")
+ flag.BoolVar(
+ &enableAdminClusterAdmin,
+ "enable-admin-cluster-admin",
+ false,
+ "Preserve the legacy cluster-admin binding and privileged admin namespace labels.",
+ )
+ flag.BoolVar(
+ &enableStrictNamespacePSA,
+ "enable-strict-namespace-pod-security",
+ true,
+ "Apply Pod Security labels to all ns-* namespaces, including namespaces without a Sealos User.",
+ )
+ rateLimiterOptions.BindFlags(flag.CommandLine)
+ opts := zap.Options{
+ Development: true,
+ }
+ opts.BindFlags(flag.CommandLine)
+ flag.Parse()
+
+ ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
+
+ // if the enable-http2 flag is false (the default), http/2 should be disabled
+ // due to its vulnerabilities. More specifically, disabling http/2 will
+ // prevent from being vulnerable to the HTTP/2 Stream Cancellation and
+ // Rapid Reset CVEs. For more information see:
+ // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
+ // - https://github.com/advisories/GHSA-4374-p667-p6c8
+ disableHTTP2 := func(c *tls.Config) {
+ setupLog.Info("disabling http/2")
+ c.NextProtos = []string{"http/1.1"}
+ }
+
+ if !enableHTTP2 {
+ tlsOpts = append(tlsOpts, disableHTTP2)
+ }
+ // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
+ // More info:
+ // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
+ // - https://book.kubebuilder.io/reference/metrics.html
+ metricsServerOptions := metricsserver.Options{
+ BindAddress: metricsAddr,
+ SecureServing: secureMetrics,
+ TLSOpts: tlsOpts,
+ }
+
+ if secureMetrics {
+ // FilterProvider is used to protect the metrics endpoint with authn/authz.
+ // These configurations ensure that only authorized users and service accounts
+ // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
+ // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
+ metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
+
+ // TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
+ // generate self-signed certificates for the metrics server. While convenient for development and testing,
+ // this setup is not recommended for production.
+ }
+
+ cfg := ctrl.GetConfigOrDie()
+ cfg.QPS = float32(kubeAPIQPS)
+ cfg.Burst = kubeAPIBurst
+ setupLog.Info("configured Kubernetes API client rate limit", "qps", cfg.QPS, "burst", cfg.Burst)
+
+ mgr, err := ctrl.NewManager(cfg, ctrl.Options{
+ Scheme: scheme,
+ Metrics: metricsServerOptions,
+ PprofBindAddress: pprofBindAddress,
+ Cache: usercache.Options(&syncPeriod),
+ Client: client.Options{Cache: &client.CacheOptions{
+ DisableFor: usercache.UncachedObjects(),
+ }},
+ // WebhookServer: webhook.NewServer(webhook.Options{
+ // Port: 9443,
+ // }),
+ HealthProbeBindAddress: probeAddr,
+ LeaderElection: enableLeaderElection,
+ LeaderElectionID: "785548a1.sealos.io",
+ LeaderElectionReleaseOnCancel: true,
+ GracefulShutdownTimeout: &gracefulShutdownTimeout,
+ Controller: config.Controller{
+ UsePriorityQueue: ptr.To(true),
+ },
+ })
+ if err != nil {
+ setupLog.Error(err, "unable to start manager")
+ os.Exit(1)
+ }
+ if err := controllers.SetupLicenseGate(mgr); err != nil {
+ setupLog.Error(err, "unable to set up license gate")
+ os.Exit(1)
+ }
+ userCounter, err := controllers.SetupUserCount(mgr)
+ if err != nil {
+ setupLog.Error(err, "unable to set up user count cache")
+ os.Exit(1)
+ }
+
+ if err = (&controllers.UserReconciler{
+ EnableAdminClusterAdmin: enableAdminClusterAdmin,
+ EnableStrictNamespacePodSecurity: enableStrictNamespacePSA,
+ }).SetupWithManager(
+ mgr,
+ rateLimiterOptions,
+ minRequeueDuration,
+ maxRequeueDuration,
+ restartPredicateDuration,
+ userCounter,
+ ); err != nil {
+ setupLog.Error(err, "unable to create controller", "controller", "User")
+ os.Exit(1)
+ }
+
+ if os.Getenv("DISABLE_WEBHOOKS") == "true" {
+ setupLog.Info("disable all webhooks")
+ } else {
+ if err = (&userv1.User{}).SetupWebhookWithManager(mgr, userCounter); err != nil {
+ setupLog.Error(err, "unable to create webhook", "webhook", "User")
+ os.Exit(1)
+ }
+ }
+
+ if err = (&controllers.OperationReqReconciler{}).SetupWithManager(
+ mgr,
+ rateLimiterOptions,
+ operationReqExpirationTime,
+ operationReqRetentionTime,
+ ); err != nil {
+ setupLog.Error(err, "unable to create controller", "controller", "Operationrequest")
+ os.Exit(1)
+ }
+ if os.Getenv("DISABLE_WEBHOOKS") == "true" {
+ setupLog.Info("disable all webhooks")
+ } else {
+ if err = (&userv1.Operationrequest{}).SetupWebhookWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create webhook", "webhook", "Operationrequest")
+ os.Exit(1)
+ }
+ }
+ if err = (&controllers.DeleteRequestReconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ }).SetupWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create controller", "controller", "DeleteRequest")
+ os.Exit(1)
+ }
+ // if err = (&controllers.AdaptRoleBindingReconciler{
+ // Client: mgr.GetClient(),
+ // Scheme: mgr.GetScheme(),
+ // }).SetupWithManager(mgr); err != nil {
+ // setupLog.Error(err, "unable to create controller", "controller", "AdaptRoleBinding")
+ // os.Exit(1)
+ // }
+ //+kubebuilder:scaffold:builder
+
+ if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
+ setupLog.Error(err, "unable to set up health check")
+ os.Exit(1)
+ }
+ if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
+ setupLog.Error(err, "unable to set up ready check")
+ os.Exit(1)
+ }
+ setupLog.Info("starting manager")
+ if err = mgr.Start(ctrl.SetupSignalHandler()); err != nil {
+ setupLog.Error(err, "failed to running manager")
+ os.Exit(1)
+ }
+}
diff --git a/controllers/user/pkg/licensegate/gate.go b/controllers/user/pkg/licensegate/gate.go
new file mode 100644
index 000000000000..d7a9c1b35ae1
--- /dev/null
+++ b/controllers/user/pkg/licensegate/gate.go
@@ -0,0 +1,122 @@
+// Copyright © 2026 sealos.
+//
+// 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 licensegate
+
+import (
+ "context"
+ "sync/atomic"
+ "time"
+
+ licensev1 "github.com/labring/sealos/controllers/license/api/v1"
+ licensepkg "github.com/labring/sealos/controllers/pkg/license"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const DefaultUserLimit = 1
+
+var (
+ activeFlag atomic.Uint32
+ initializedFlag atomic.Uint32
+ userLimit atomic.Int64
+)
+
+func Initialized() bool {
+ return initializedFlag.Load() == 1
+}
+
+func HasActiveLicense() bool {
+ return activeFlag.Load() == 1
+}
+
+func AllowNewUser(currentCount int) bool {
+ if !Initialized() {
+ return false
+ }
+ limit := UserLimit()
+ if limit < 0 {
+ return true
+ }
+ return currentCount < limit
+}
+
+func UserLimit() int {
+ return int(userLimit.Load())
+}
+
+func LimitMessage() string {
+ if HasActiveLicense() {
+ return "license active: user limit reached"
+ }
+ return "license inactive: user limit reached"
+}
+
+func SetActive(active bool) {
+ if active {
+ activeFlag.Store(1)
+ } else {
+ activeFlag.Store(0)
+ }
+ initializedFlag.Store(1)
+}
+
+func SetUserLimit(limit int) {
+ userLimit.Store(int64(limit))
+ initializedFlag.Store(1)
+}
+
+func SetState(active bool, limit int) {
+ SetActive(active)
+ SetUserLimit(limit)
+}
+
+func Refresh(ctx context.Context, reader client.Reader) error {
+ licenseList := &licensev1.LicenseList{}
+ if err := reader.List(ctx, licenseList); err != nil {
+ return err
+ }
+ var selected *licensev1.License
+ for i := range licenseList.Items {
+ license := &licenseList.Items[i]
+ if license.Status.Phase != licensev1.LicenseStatusPhaseActive {
+ continue
+ }
+ if selected == nil || licenseTimestamp(license).After(licenseTimestamp(selected)) {
+ selected = license
+ }
+ }
+ if selected == nil {
+ SetState(false, DefaultUserLimit)
+ return nil
+ }
+ claims, err := licensepkg.GetClaimsFromLicense(selected)
+ if err != nil {
+ SetState(false, DefaultUserLimit)
+ return err
+ }
+ clusterData := &licensepkg.ClusterClaimData{}
+ if err := claims.Data.SwitchToClusterData(clusterData); err != nil {
+ SetState(false, DefaultUserLimit)
+ return err
+ }
+ SetState(true, clusterData.UserCount)
+ return nil
+}
+
+func licenseTimestamp(license *licensev1.License) time.Time {
+ if !license.Status.ActivationTime.IsZero() {
+ return license.Status.ActivationTime.Time
+ }
+ return license.CreationTimestamp.Time
+}
diff --git a/controllers/user/pkg/licensegate/gate_test.go b/controllers/user/pkg/licensegate/gate_test.go
new file mode 100644
index 000000000000..cc7f0e3df393
--- /dev/null
+++ b/controllers/user/pkg/licensegate/gate_test.go
@@ -0,0 +1,146 @@
+// Copyright © 2026 sealos.
+//
+// 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 licensegate
+
+import (
+ "context"
+ "encoding/base64"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/golang-jwt/jwt/v4"
+ licensev1 "github.com/labring/sealos/controllers/license/api/v1"
+ licensepkg "github.com/labring/sealos/controllers/pkg/license"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func newTestLicense(
+ t *testing.T,
+ userCount int,
+ phase licensev1.LicenseStatusPhase,
+ activationTime time.Time,
+) *licensev1.License {
+ t.Helper()
+ if os.Getenv("LICENSE_KEY") == "" {
+ t.Skip("requires LICENSE_KEY to sign test licenses")
+ }
+ decodeKey, err := base64.StdEncoding.DecodeString(licensepkg.GetEncryptionKey())
+ if err != nil {
+ t.Fatalf("decode encryption key failed: %v", err)
+ }
+ privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(decodeKey)
+ if err != nil {
+ t.Fatalf("parse private key failed: %v", err)
+ }
+ claims := &licensepkg.Claims{
+ Type: licensev1.ClusterLicenseType,
+ Data: licensepkg.ClaimData{
+ "userCount": userCount,
+ },
+ RegisteredClaims: jwt.RegisteredClaims{
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
+ },
+ }
+ token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
+ signed, err := token.SignedString(privateKey)
+ if err != nil {
+ t.Fatalf("sign token failed: %v", err)
+ }
+ return &licensev1.License{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-license",
+ },
+ Spec: licensev1.LicenseSpec{
+ Token: signed,
+ Type: claims.Type,
+ },
+ Status: licensev1.LicenseStatus{
+ Phase: phase,
+ ActivationTime: metav1.NewTime(activationTime),
+ },
+ }
+}
+
+func TestRefreshActiveLicense(t *testing.T) {
+ scheme := runtime.NewScheme()
+ if err := licensev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add scheme failed: %v", err)
+ }
+ activeLicense := newTestLicense(t, 10, licensev1.LicenseStatusPhaseActive, time.Now())
+ client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(activeLicense).Build()
+ if err := Refresh(context.Background(), client); err != nil {
+ t.Fatalf("refresh failed: %v", err)
+ }
+ if !HasActiveLicense() {
+ t.Fatalf("expected active license")
+ }
+ if UserLimit() != 10 {
+ t.Fatalf("expected user limit 10, got %d", UserLimit())
+ }
+}
+
+func TestRefreshNoActiveLicense(t *testing.T) {
+ scheme := runtime.NewScheme()
+ if err := licensev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add scheme failed: %v", err)
+ }
+ inactiveLicense := newTestLicense(t, 10, licensev1.LicenseStatusPhaseFailed, time.Now())
+ client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(inactiveLicense).Build()
+ if err := Refresh(context.Background(), client); err != nil {
+ t.Fatalf("refresh failed: %v", err)
+ }
+ if HasActiveLicense() {
+ t.Fatalf("expected inactive license")
+ }
+ if UserLimit() != DefaultUserLimit {
+ t.Fatalf("expected default user limit %d, got %d", DefaultUserLimit, UserLimit())
+ }
+}
+
+func TestAllowNewUserUnlimited(t *testing.T) {
+ scheme := runtime.NewScheme()
+ if err := licensev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add scheme failed: %v", err)
+ }
+ activeLicense := newTestLicense(t, -1, licensev1.LicenseStatusPhaseActive, time.Now())
+ client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(activeLicense).Build()
+ if err := Refresh(context.Background(), client); err != nil {
+ t.Fatalf("refresh failed: %v", err)
+ }
+ if !AllowNewUser(1000) {
+ t.Fatalf("expected unlimited user allow")
+ }
+}
+
+func TestRefreshUsesLatestActiveLicense(t *testing.T) {
+ scheme := runtime.NewScheme()
+ if err := licensev1.AddToScheme(scheme); err != nil {
+ t.Fatalf("add scheme failed: %v", err)
+ }
+ older := newTestLicense(t, 5, licensev1.LicenseStatusPhaseActive, time.Now().Add(-time.Hour))
+ newer := newTestLicense(t, 20, licensev1.LicenseStatusPhaseActive, time.Now())
+ older.Name = "older-license"
+ newer.Name = "newer-license"
+ client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(older, newer).Build()
+ if err := Refresh(context.Background(), client); err != nil {
+ t.Fatalf("refresh failed: %v", err)
+ }
+ if UserLimit() != 20 {
+ t.Fatalf("expected user limit 20, got %d", UserLimit())
+ }
+}
diff --git a/controllers/user/pkg/usercount/count.go b/controllers/user/pkg/usercount/count.go
new file mode 100644
index 000000000000..e69131949656
--- /dev/null
+++ b/controllers/user/pkg/usercount/count.go
@@ -0,0 +1,160 @@
+// Copyright © 2026 sealos.
+//
+// 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 usercount
+
+import (
+ "sync"
+ "sync/atomic"
+
+ "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ toolscache "k8s.io/client-go/tools/cache"
+)
+
+// Counter tracks users that are present and not being deleted. It stores only
+// object names so quota checks do not retain or copy User status fields.
+type Counter struct {
+ mu sync.RWMutex
+ users map[string]struct{}
+ count atomic.Int64
+ initialized atomic.Bool
+}
+
+func NewCounter() *Counter {
+ return &Counter{users: make(map[string]struct{})}
+}
+
+func (c *Counter) Initialized() bool {
+ return c != nil && c.initialized.Load()
+}
+
+func (c *Counter) Count() int {
+ if c == nil {
+ return 0
+ }
+ return int(c.count.Load())
+}
+
+func (c *Counter) CountExcluding(name string) int {
+ if c == nil {
+ return 0
+ }
+
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ count := int(c.count.Load())
+ if _, ok := c.users[name]; ok {
+ return count - 1
+ }
+ return count
+}
+
+// MarkInitialized marks the counter ready after the informer's initial events
+// have been delivered to the counter's event handler.
+func (c *Counter) MarkInitialized() {
+ if c == nil {
+ return
+ }
+ c.initialized.Store(true)
+}
+
+func (c *Counter) Add(obj any) {
+ metadata, ok := objectMetadata(obj)
+ if !ok {
+ return
+ }
+ c.set(metadata.GetName(), isQuotaUser(metadata))
+}
+
+func (c *Counter) Update(oldObj, newObj any) {
+ oldMetadata, oldOK := objectMetadata(oldObj)
+ newMetadata, newOK := objectMetadata(newObj)
+ if oldOK && newOK && oldMetadata.GetName() != newMetadata.GetName() {
+ c.set(oldMetadata.GetName(), false)
+ }
+ if !newOK {
+ return
+ }
+ c.set(newMetadata.GetName(), isQuotaUser(newMetadata))
+}
+
+func (c *Counter) Delete(obj any) {
+ metadata, ok := objectMetadata(obj)
+ if !ok {
+ return
+ }
+ c.set(metadata.GetName(), false)
+}
+
+func (c *Counter) set(name string, present bool) {
+ if c == nil || name == "" {
+ return
+ }
+
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if c.users == nil {
+ c.users = make(map[string]struct{})
+ }
+ if present {
+ if _, ok := c.users[name]; ok {
+ return
+ }
+ c.users[name] = struct{}{}
+ c.count.Add(1)
+ return
+ }
+ if _, ok := c.users[name]; !ok {
+ return
+ }
+ delete(c.users, name)
+ c.count.Add(-1)
+}
+
+func isQuotaUser(obj metav1.Object) bool {
+ return obj.GetName() != "" &&
+ (obj.GetDeletionTimestamp() == nil || obj.GetDeletionTimestamp().IsZero())
+}
+
+func objectMetadata(obj any) (metav1.Object, bool) {
+ switch tombstone := obj.(type) {
+ case toolscache.DeletedFinalStateUnknown:
+ obj = tombstone.Obj
+ case *toolscache.DeletedFinalStateUnknown:
+ obj = tombstone.Obj
+ }
+ metadata, err := meta.Accessor(obj)
+ return metadata, err == nil && metadata != nil
+}
+
+// The following process-local functions are retained for the license
+// controller, which refreshes its own user count independently.
+var (
+ processUserCount atomic.Int64
+ processUserCountInitialized atomic.Uint32
+)
+
+func Initialized() bool {
+ return processUserCountInitialized.Load() == 1
+}
+
+func Get() int {
+ return int(processUserCount.Load())
+}
+
+func Set(count int) {
+ processUserCount.Store(int64(count))
+ processUserCountInitialized.Store(1)
+}
diff --git a/controllers/user/pkg/usercount/count_test.go b/controllers/user/pkg/usercount/count_test.go
new file mode 100644
index 000000000000..f30be3d39337
--- /dev/null
+++ b/controllers/user/pkg/usercount/count_test.go
@@ -0,0 +1,75 @@
+// Copyright © 2026 sealos.
+//
+// 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 usercount
+
+import (
+ "testing"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ toolscache "k8s.io/client-go/tools/cache"
+)
+
+func TestCounterTracksInformerEvents(t *testing.T) {
+ counter := NewCounter()
+ deletionTimestamp := metav1.Now()
+ active := &metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: "active-user"}}
+ deleting := &metav1.PartialObjectMetadata{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "active-user",
+ DeletionTimestamp: &deletionTimestamp,
+ },
+ }
+
+ counter.Add(active)
+ counter.MarkInitialized()
+ if !counter.Initialized() || counter.Count() != 1 {
+ t.Fatalf(
+ "initialized counter = (%t, %d), want (true, 1)",
+ counter.Initialized(),
+ counter.Count(),
+ )
+ }
+ if got := counter.CountExcluding("active-user"); got != 0 {
+ t.Fatalf("CountExcluding() = %d, want 0", got)
+ }
+
+ counter.Update(active, deleting)
+ if got := counter.Count(); got != 0 {
+ t.Fatalf("count after deletion update = %d, want 0", got)
+ }
+
+ counter.Add(&metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: "new-user"}})
+ if got := counter.Count(); got != 1 {
+ t.Fatalf("count after add = %d, want 1", got)
+ }
+ counter.Delete(&metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: "new-user"}})
+ if got := counter.Count(); got != 0 {
+ t.Fatalf("count after delete = %d, want 0", got)
+ }
+}
+
+func TestCounterIgnoresDeletedFinalStateUnknown(t *testing.T) {
+ counter := NewCounter()
+ counter.Add(&metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: "user"}})
+ counter.MarkInitialized()
+
+ counter.Delete(toolscache.DeletedFinalStateUnknown{
+ Key: "user",
+ Obj: &metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: "user"}},
+ })
+ if got := counter.Count(); got != 0 {
+ t.Fatalf("count after tombstone delete = %d, want 0", got)
+ }
+}
diff --git a/controllers/user/user.env b/controllers/user/user.env
new file mode 100644
index 000000000000..a4f5f190c97d
--- /dev/null
+++ b/controllers/user/user.env
@@ -0,0 +1,2 @@
+KUBECONFIG=kubeconfig.test
+DISABLE_WEBHOOKS=true
diff --git a/docs/4.0/README.md b/docs/4.0/README.md
deleted file mode 100644
index cc872d5f5192..000000000000
--- a/docs/4.0/README.md
+++ /dev/null
@@ -1,157 +0,0 @@
-
-
-
-
-
-
-
-
- 让云原生简单普及!
-
-
-
- [](https://github.com/labring/sealos)
- [](https://github.com/labring/sealos/actions)
- [](https://sealyun.com)
- [](https://goreportcard.com/report/github.com/labring/sealos)
- [](https://t.me/gsealyun)
-
-
-
-
----
-
-**文档: _[官方文档](https://www.sealyun.com), [English Docs](/README_en.md), [博客](https://fuckcloudnative.io)_**
-
-**加入组织: 钉钉群(35371178), [Telegram](https://t.me/gsealyun), QQ群(98488045),作者微信:fangnux**
-
-## sealos 是什么
-
-**sealos 是以kubernetes为内核的云操作系统发行版**
-
-早期单机操作系统也是分层架构,后来才演变成 linux windows这种内核架构,云操作系统从容器诞生之日起分层架构被击穿,未来也会朝着高内聚的"云内核"架构迁移
-
-
-
-* 从现在开始,把你数据中心所有机器想象成一台"抽象"的超级计算机,sealos就是用来管理这台超级计算机的操作系统,kubernetes就是这个操作系统的内核!
-* 云计算从此刻起再无IaaS PaaS SaaS之分,只有云操作系统驱动(CSI CNI CRI实现) 云操作系统内核(kubernetes) 和 分布式应用组成
-
-> 核心能力
-
-* 集群镜像 - 实现整个集群的Build Ship Run,把docker的理念衍生到集群纬度,实现任意分布式软件的自由定义一键运行
-* hub.sealos - 集群镜像仓库,这里你可以找到绝大多数已经制作好的分布式应用如kubernetes基础集群镜像,pgsql高可用集群镜像,minio高可用集群镜像等
-* desktop.sealos - 云操作系统的桌面,并非传统意义上的云桌面,它长得像macOS但是管理的集群和分布式应用
-* 分布式应用矩阵 - 也就是各种你需要用的存储/网络/高可用数据库/消息队列/监控等,所有这些只需要点下鼠标或者sealos run即可获得
-
-## sealos 愿景
-
-* 让所有企业使用基于kubernetes的云操作系统像使用macOS一样简单
-* 让任何人都可以用一条命令或动动鼠标即可构建复杂的云服务
-* 让所有企业只需要雇佣一个实习生即可维护整个云
-* 让所有企业能拥有更开放的AWS, 公有云与私有云可以有完全一致性的体验
-* 让任何分布式软件都可以一键在系统中运行并实现自运维
-
-## sealos 能干啥
-
-* 对集群生命周期进行管理,一键安装高可用kubernetes集群,增删节点清理集群自恢复等
-* 通过sealos hub 下载和使用完全兼容OCI标准的各种分布式软件如openebs,minio,ingress,pgsql,mysql,redis等
-* 通过sealos desktop 像使用macOS一样管理整个集群,以及管理系统上跑的分布式应用
-* sealos 可以管理kubernetes但并不是一个kubernetes管理器, 而是一个抽象的云操作系统。要管理kubernetes下载一个对应管理应用即可。
-* sealos 可以安装kubernetes但是并不是一个安装工具,安装只是sealos 的一个boot的最基本的能力。
-
-## sealos 适合谁用
-
-* 小白用户 - 针对小白连kubernetes单词也不会拼写的人也可以通过命令或者图形界面完全无障碍使用sealos,获取一些需要的软件,如点击一下即可获得一个高可用数据库。
-* SaaS应用开发者 - 你可能需要的也是一个数据库,一个高可用消息队列或者一个开发环境,一条命令即可让你获得所需要的服务。你也不想关心底层如何实现,你只要结果。
-* 集群维护者 - 你可能很懂kubernetes,sealos市场里提供各种管理应用如lens官方dashboard等等,针对极客还提供webterminal, 各种云原生生态软件监控系统也是信手拈来。
-* 云操作系统开发者 - 你可能很擅长operator开发,那么恭喜你可以编写sealos的应用,并提交到sealos hub上供其他所有用户使用你的产品。
-* 私有云交付人员 - sealos集群镜像机制可以保证在离线环境中的高度一致性,是私有云交付之王,也能极好的封装SaaS应用,实现一键交付到客户环境中。
-* 企业用户 - 你可以直接使用sealos公有云服务,也可以分钟级在自己的机房中运行出一个一模一样的私有云为整个企业提供服务, 还可以把sealos运行到各大公有云IaaS上,再也不用受厂商绑定之苦,想切就切。
-
-## sealos 为什么不一样
-
-> kubernetes是手段不是目的
-
-对于大众用户来说kubernetes并不重要,重要的是kubernetes上面跑了什么东西,这些东西才是用户最终关心的,中间过程并不关心。
-当然熟悉kubernetes的极客不用担心,你同样会有非常好的使用体验。
-
-> 化整为零,不同的应用,不同的形态
-
-sealos 最简单的版本几乎不包含任何东西,除了最最基本的集群镜像的能力,其它能力几乎都是通过应用云扩展的,最基础的sealos除了一个很"裸"的kuberentes不包含其他任何东西。
-这使得sealos可以很简单,也可以很强大,可以个人使用,也可以服务公有云这种庞大的多租户应用场景,可以在一台机器上玩,也可以在数千台服务器上大规模运行。
-
-> 包容性
-
-意味着不管你是什么样的喜好都可以在sealos上得到完美使用体验,比如以CI/CD场景为例,有些用户喜欢drone 有些喜欢argo,这两类用户只需要自己安装不同应用即可,sealos不会深度集成某一款CI/CD工具
-用户自由的安装卸载。
-
-sealos不会追求系统上分布式应用风格的统一,就像macOS上的office软件和Email软件不可能一致一样,这样的好处是给不同的分布式软件最大发挥空间,不然sealos会花非常大的代价让他们统一,一旦某个技术
-被淘汰意味着极大的替换成本。
-
-sealos也不会追求各种软件账户信息的统一,因为不同的分布式软件有不同账户管理方式,这些管理方式对其应用自身的适配性是最强的。
-
-> 不同的用户不同的使用方式
-
-和macOS很类似,普通大众用户用图形界面,开发者终端敲敲命令,系统应用开发者调用system API
-sealos的大众用户用GUI或者简单的一键使用,云原生从业者kubectl 各种dashboard 所有apiserver, 开发者直接基于kubernetes开发operator
-
-> 简单不失强大
-
-sealos提供的是最基础的系统框架,其强大的能力都由上层应用提供,sealos的职责是管理好这些应用,所以系统复杂度不会随着功能的增加而变复杂。
-
-## sealos 实践案例
-
-Boss:我司需要紧跟云原生浪潮,需要构建一个基于kubernetes云平台,要有存储,要有paas,要有ci/cd,还要有云开发,还要有数据库,还要有。。。xx总监你来评估一下成本
-
-CTO(掰掰手指头): kubernetes 3人,存储专家 1人,开发1人,paas 3人, ci/cd 3人, 云开发5人, 运维3人。。。 老板我仅需要15人 给我半年就做出来!
-
-众人议论纷纷,此时角落传来一个声音:
-
-小张(默默举手): 我了解一个开源软件,貌似一键就可以搞定,小张共享了一下屏幕,默默敲下:
-
-```shell script
-sealos run labring/kubernetes:v1.24.0 labring/openebs:v1.9.0 labring/mysql:v8.0 labring/minio:v4.4.16 labring/ingress:v4.1.0 labring/laf:v0.8.0
- -m 192.168.0.2 -n 192.168.0.3 -p 123456
-```
-
-会没结束,任务完成。。。
-
-后面的故事大家都知道了,除了小张 CTO往下都被裁了。。。
-
-## 快速开始
-
-> 安装一个高可用的kubernetes集群,并用calico作为网络插件
-
-这里的 `kubernetes:v1.24.0` 和 `calico:v3.22.1` 就是存在registry里的集群镜像,完全兼容OCI标准, 当然聪明同学立马想到是不是可以用flannel,答案是当然!
-
-```shell script
-# 下载并安装sealos, sealos是个golang的二进制工具,直接下载拷贝到bin目录即可, release页面也可下载
-wget -c https://sealyun-home.oss-cn-beijing.aliyuncs.com/sealos-4.0/latest/sealos-amd64 -O sealos && \
- chmod +x sealos && mv sealos /usr/bin
-# 创建一个集群
-sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 \
- --masters 192.168.64.2,192.168.64.22,192.168.64.20 \
- --nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd]
-```
-
-> 构建一个自定义集群镜像
-
-[构建一个ingress集群镜像](https://github.com/labring/sealos/blob/main/docs/4.0/build-example-ingress-helm.md)
-
-> 存储/消息/数据库 等
-
-接下来请不要震惊:
-
-```shell script
-sealos run labring/helm:v3.8.2 # 安装helm
-sealos run labring/openebs:v1.9.0 # 安装openebs
-sealos run labring/minio-operator:v4.4.16 labring/ingress-nginx:4.1.0 \
- labring/mysql-operator:8.0.23-14.1 labring/redis-operator:3.1.4 # 喜欢的话可以把它们写一起
-```
-
-然后你就啥都有了
-
-## 其它链接
-
-* [sealos 3.0 老版本](https://github.com/labring/sealos/tree/release-v3.3.9#readme) 老版本用户访问这里,4.0全面掉打老版本,请尽快升级。
-* [buildah](https://github.com/containers/buildah) 本着不重复造轮子,sealos 4.0中使用了大量buildah的构建能力,使集群镜像完全兼容容器镜像和docker registry。
diff --git a/docs/4.0/build-example-ingress-helm.md b/docs/4.0/build-example-ingress-helm.md
deleted file mode 100644
index 731add255347..000000000000
--- a/docs/4.0/build-example-ingress-helm.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# Example for build a CloudImage from helm
-
-This is an example for build nginx-ingress CloudImage using helm.
-
-## Download helm chart
-
-```shell script
-mkdir ingress-nginx && cd ingress-nginx
-helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
-helm pull ingress-nginx/ingress-nginx
-```
-
-Then you will got the chart:
-```shell script
-root@iZj6ceuntkc5q5p95bbqb8Z:~/nginx-ingress# ls
-ingress-nginx-4.1.0.tgz
-```
-
-## Add image list
-
-sealos will download images in image list, and cache it into registry dir.
-The dir must be `images/shim/[your image list filename]`
-
-```shell script
-root@iZj6ceuntkc5q5p95bbqb8Z:~/nginx-ingress# cat images/shim/nginxImages
-k8s.gcr.io/ingress-nginx/controller:v1.2.0
-k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1
-```
-
-
-## Add a Dockerfile
-
-```shell script
-root@iZj6ceuntkc5q5p95bbqb8Z:~/nginx-ingress# cat Dockerfile
-FROM scratch
-COPY . .
-CMD ["helm install ingress-nginx ingress-nginx-4.1.0.tgz --namespace ingress-nginx --create-namespace"]
-```
-
-## Build it
-
-```shell script
-sealos build -f Dockerfile -t docker.io/fanux/ingress-nginx:v1.2.0 .
-```
-
-Then push it into registry
-
-```shell script
-sealos login docker.io
-sealos push docker.io/fanux/ingress-nginx:v1.2.0
-```
diff --git a/docs/4.0/build.md b/docs/4.0/build.md
deleted file mode 100644
index e42255e3d85e..000000000000
--- a/docs/4.0/build.md
+++ /dev/null
@@ -1,122 +0,0 @@
-## build calico image in offline module
-
-```
-.
-├── Kubefile
-├── cni
-│ ├── custom-resources.yaml
-│ └── tigera-operator.yaml
-├── images
-│ └── shim
-│ └── CalicoImageList
-└── registry
- └── docker
- └── registry
-```
-
-run one image build func
-
-```dockerfile
-FROM labring/kubernetes:v1.24.0-amd64
-COPY cni ./cni
-COPY images ./images
-COPY registry ./registry
-CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"]
-```
-
-run multi image build func
-
-```dockerfile
-FROM scratch
-COPY cni ./cni
-COPY images ./images
-COPY registry ./registry
-COPY manifests ./manifests
-CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"]
-```
-
-
-1. the `CalicoImageList` is offline image list file.
-2. the cni dir is kubectl apply config files
-3. the registry is the registry data dir
-4. exec `buildah build -t kubernetes-calico:1.24.0-amd64 --arch amd64 --os linux -f Kubefile .` build the oci image
-5. manifests dis parse yaml images to offline image list
-
-## build calico image in online module
-
-```
-.
-├── Kubefile
-├── cni
-│ ├── custom-resources.yaml
-│ └── tigera-operator.yaml
-
-```
-
-run one image build func
-
-```dockerfile
-FROM labring/kubernetes:v1.24.0-amd64
-COPY cni ./cni
-CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"]
-```
-
-run multi image build func
-
-```dockerfile
-FROM scratch
-COPY cni ./cni
-CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"]
-```
-
-1. the cni dir is kubectl apply config files
-2. exec `buildah build -t kubernetes-calico:1.24.0-amd64 --arch amd64 --os linux -f Kubefile .` build the oci image
-
-
-## build openebs image in online module
-
-```
-.
-├── Kubefile
-└── manifests
- └── openebs-operator.yaml
-
-```
-
-run one image build func
-
-```dockerfile
-FROM labring/oci-kubernetes-calico:1.24.0-amd64
-COPY manifests ./manifests
-CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml","kubectl apply -f manifests/openebs-operator.yaml"]
-```
-
-run multi image build func
-
-```dockerfile
-FROM scratch
-COPY manifests ./manifests
-CMD ["kubectl apply -f manifests/openebs-operator.yaml"]
-```
-
-
-1. the cni dir is kubectl apply config files
-2. exec `buildah build -t labring/kubernetes-calico-openebs:1.24.0-amd64 --arch amd64 --os linux -f Kubefile .` build the oci image
-
-tips: you need add calico cmd to openebs cmd layer,because the dockerfile override to old layer in run one image func
-
-
-## build multi-architecture image
-
-```shell
-buildah build -t $prefix/oci-kubernetes:$version-amd64 --arch amd64 --os linux -f Kubefile .
-buildah build -t $prefix/oci-kubernetes:$version-arm64 --arch arm64 --os linux -f Kubefile .
-
-buildah login --username $username --password $password $domain
-buildah push $prefix/oci-kubernetes:$version-amd64
-buildah push $prefix/oci-kubernetes:$version-arm64
-buildah manifest create $prefix/oci-kubernetes:$version
-buildah manifest add $prefix/oci-kubernetes:$version docker://$prefix/oci-kubernetes:$version-amd64
-buildah manifest add $prefix/oci-kubernetes:$version docker://$prefix/oci-kubernetes:$version-arm64
-buildah manifest push --all $prefix/oci-kubernetes:$version docker://$prefix/oci-kubernetes:$version
-```
diff --git a/docs/4.0/config.md b/docs/4.0/config.md
deleted file mode 100644
index 1c27438d1ba5..000000000000
--- a/docs/4.0/config.md
+++ /dev/null
@@ -1,35 +0,0 @@
-### config
-
-Clusterfile
-
-```yaml
-...
----
-apiVersion: apps.sealos.io/v1beta1
-kind: Config
-metadata:
- name: calico
-spec:
- strategy: merge
- path: manifests/calico.yaml
- data: |
- apiVersion: operator.tigera.io/v1
- kind: Installation
- metadata:
- name: default
- spec:
- # Configures Calico networking.
- calicoNetwork:
- # Note: The ipPools section cannot be modified post-install.
- ipPools:
- - blockSize: 26
- cidr: 100.63.0.0/10
- encapsulation: IPIP
- natOutgoing: Enabled
- nodeSelector: all()
- nodeAddressAutodetectionV4:
- # Interface enables IP auto-detection based on interfaces that match the given regex.
- interface: "eth.*|en.*"
-
-
-```
diff --git a/docs/4.0/design/registry-image.md b/docs/4.0/design/registry-image.md
deleted file mode 100644
index 4936f92cc9a4..000000000000
--- a/docs/4.0/design/registry-image.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# 镜像打包与分发设计
-
-## 背景
-
-当前镜像管理方式有几个问题:
-
-1. registry模块与filesystem耦合,应该单独拆出一个管理模块
-2. docker镜像放在集群镜像里面,会导致集群镜像大小非常大,而且推送到仓库中实际上是有很多多余文件,浪费空间
-3. 分发的时候通过scp分发非常低效,也会存在部分多余分发的情况
-4. 无法充分利用nydus的特性对集群镜像分发,需要作一些转换,集成nydus方式也不够干净
-5. 在线和离线两种场景都把所有东西拉下来,在可以联网的服务上没有这个必要
-6. 多架构分发需要自己去判断目标机器系统架构
-
-## 方案设计
-
-### 构建过程
-
-1. Build的时候对集群镜像里面的Docker镜像不作任何处理,以保障集群镜像"很小"
-2. CloudImage完全兼容OCI
-
-### Save过程
-
-1. 把之前在Build过程中缓存容器镜像的动作后置到Save命令中,此时去解析manifests目录,chart目录以及imageList,然后把容器镜像单独放到 registry目录.
-2. 把集群镜像也保存到registry目录中.
-3. Save的产物就是registry目录和一些配置信息的打包.
-
-### Run 过程
-
-1. registry module根据registry的配置拉起registry.
-2. 根据CloudImage Name在所有节点拉取集群镜像
-3. 启动k8s和guest
-4. kubelet自动拉起其它镜像
-5. 可以利用运行时自身能力去判断多架构并拉取对应架构的镜像
-
-### 加速方案
-
-在此方案基础上就可以直接制作containerd+nydus的runtime镜像,直接使用nydus的能力,同时提升集群镜像与容器镜像的分发速度
\ No newline at end of file
diff --git a/docs/4.0/design/rootfs.md b/docs/4.0/design/rootfs.md
deleted file mode 100644
index 0120fb414f96..000000000000
--- a/docs/4.0/design/rootfs.md
+++ /dev/null
@@ -1,98 +0,0 @@
-### rootfs
-
-```
-.
-├── Kubefile
-├── README.md
-├── bin
-│ ├── conntrack
-│ ├── kubeadm
-│ ├── kubectl
-│ └── kubelet
-├── cri
-│ ├── cri-containerd-linux.tar.gz
-│ ├── image-cri-shim
-│ ├── lib64
-│ │ ├── libseccomp.so.2
-│ │ └── libseccomp.so.2.3.1
-│ └── nerdctl
-├── etc
-│ ├── 10-kubeadm.conf
-│ ├── config.toml
-│ ├── containerd.service
-│ ├── crictl.yaml
-│ ├── hosts.toml
-│ ├── image-cri-shim.service
-│ ├── image-cri-shim.yaml.tmpl
-│ ├── kubelet.service
-│ ├── registry.yml.tmpl
-│ └── registry_config.yml
-├── images
-│ ├── registry.tar
-│ └── shim
-│ └── DefaultImageList
-├── opt
-│ └── sealctl
-├── registry
-│ └── docker
-│ └── registry
-├── scripts
-│ ├── auth.sh
-│ ├── check.sh
-│ ├── clean-containerd.sh
-│ ├── clean-kube.sh
-│ ├── clean-registry.sh
-│ ├── clean-shim.sh
-│ ├── clean.sh
-│ ├── common.sh
-│ ├── init-containerd.sh
-│ ├── init-kube.sh
-│ ├── init-registry.sh
-│ ├── init-shim.sh
-│ ├── init.sh
-│ ├── kubelet-post-stop.sh
-│ └── kubelet-pre-start.sh
-└── statics
- └── audit-policy.yml
-```
-
-```dockerfile
-FROM scratch
-MAINTAINER sealyun
-LABEL init="init.sh \$criData \$registryDomain \$registryPort \$registryUsername \$registryPassword"
-LABEL version="v1.24.0"
-LABEL image=""
-LABEL clean="clean.sh \$criData"
-LABEL check="check.sh"
-LABEL init-registry="init-registry.sh \$registryPort \$registryData \$registryConfig"
-LABEL clean-registry="clean-registry.sh \$registryData \$registryConfig"
-LABEL auth="auth.sh"
-ENV criData=/var/lib/containerd
-ENV registryData=/var/lib/registry
-ENV registryConfig=/etc/registry
-ENV registryDomain=sealos.hub
-ENV registryPort=5000
-ENV registryUsername=admin
-ENV registryPassword=passw0rd
-COPY .. .
-```
-
-
-image-cri-shim can replace image to registry addr
-
-- offline
- if offline module,you need add imageList file in dir `images/shim`
- example ImageList file is :
- `
- ghcr.io/sealyun/lvscare:v1.1.3-beta.2
- k8s.gcr.io/kube-apiserver:v1.24.0
- k8s.gcr.io/kube-controller-manager:v1.24.0
- k8s.gcr.io/kube-scheduler:v1.24.0
- k8s.gcr.io/kube-proxy:v1.24.0
- k8s.gcr.io/pause:3.5
- k8s.gcr.io/etcd:3.5.0-0
- k8s.gcr.io/coredns/coredns:v1.8.4
- `
- shim found image in imageList ,the `k8s.gcr.io/kube-apiserver:v1.24.0` replace to `sealos.hub:5000/kube-apiserver:v1.24.0`
-- online (default module)
-
diff --git a/docs/4.0/images.md b/docs/4.0/images.md
deleted file mode 100644
index 75b0a15ce75c..000000000000
--- a/docs/4.0/images.md
+++ /dev/null
@@ -1,11 +0,0 @@
-## All image list
-
-- labring/kubernetes:v1.24.0
-- labring/calico:v3.22.1
-- labring/helm:v3.8.2
-- labring/openebs:v1.9.0
-- labring/minio-operator:v4.4.16
-- labring/ingress-nginx:4.1.0
-- labring/mysql-operator:8.0.23-14.1
-- labring/redis-operator:3.1.4
-- labring/coredns:v0.0.1
diff --git a/docs/4.0/install-sealos.md b/docs/4.0/install-sealos.md
deleted file mode 100644
index 59c0e302a964..000000000000
--- a/docs/4.0/install-sealos.md
+++ /dev/null
@@ -1,23 +0,0 @@
-### install buildah
-
-#### amd64
-
-```shell
-wget https://sealyun-home.oss-accelerate.aliyuncs.com/images/buildah.linux.amd64 --no-check-certificate -O buildah
-chmod a+x buildah && mv buildah /usr/bin
-```
-
-#### arm64
-
-```shell
-wget https://sealyun-home.oss-accelerate.aliyuncs.com/images/buildah.linux.arm64 --no-check-certificate -O buildah
-chmod a+x buildah && mv buildah /usr/bin
-```
-
-
-### install sealos
-
-```shell
-wget -c https://sealyun-home.oss-cn-beijing.aliyuncs.com/sealos-4.0/latest/sealos-amd64 -O sealos && \
-chmod +x sealos && mv sealos /usr/bin
-```
diff --git a/docs/4.0/reset.md b/docs/4.0/reset.md
deleted file mode 100644
index 53c47c9420e8..000000000000
--- a/docs/4.0/reset.md
+++ /dev/null
@@ -1,5 +0,0 @@
-### reset cluster
-
-```shell
-sealos reset
-```
diff --git a/docs/4.0/run.md b/docs/4.0/run.md
deleted file mode 100644
index d9b5133f593c..000000000000
--- a/docs/4.0/run.md
+++ /dev/null
@@ -1,38 +0,0 @@
-### run calico use one image
-
-```shell
-sealos run labring/oci-kubernetes-calico:1.24.0-amd64 \
- --masters 192.168.64.2,192.168.64.22,192.168.64.20 \
- --nodes 192.168.64.21,192.168.64.19
-```
-
-### run calico use multi image
-
-```shell
-sealos run labring/kubernetes:v1.24.0 \
- labring/calico:v3.22.1 \
---masters 192.168.64.2,192.168.64.22,192.168.64.20 \
---nodes 192.168.64.21,192.168.64.19
-```
-
-
-
-### run openebs use one image
-
-```shell
-sealos run labring/oci-kubernetes-calico-openebs:1.24.0-amd64 \
- --masters 192.168.64.2,192.168.64.22,192.168.64.20 \
- --nodes 192.168.64.21,192.168.64.19
-```
-
-
-### run openebs use multi image
-
-```shell
-sealos run labring/kubernetes:v1.24.0 \
- --masters 192.168.64.2,192.168.64.22,192.168.64.20 \
- --nodes 192.168.64.21,192.168.64.19
-
-sealos run labring/calico:v3.22.1
-sealos run labring/openebs:3.1.0
-```
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000000..1239323d0495
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,10 @@
+# Documentation Migration
+
+The documentation for Sealos has been moved to [https://github.com/labring/sealos.io/](https://github.com/labring/sealos.io/).
+
+Please refer to the new location for the latest guides and references.
+
+---
+
+**Notice:**
+Documentation for previous versions (`3.0`, `4.0`, and `5.0`) has been archived and is no longer actively maintained in this repository.
\ No newline at end of file
diff --git a/docs/api/sealos.md b/docs/api/sealos.md
deleted file mode 100644
index ba2d0bb4de83..000000000000
--- a/docs/api/sealos.md
+++ /dev/null
@@ -1,39 +0,0 @@
-## sealos
-
-simplest way install kubernetes tools.
-
-### Options
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
- -h, --help help for sealos
-```
-
-### SEE ALSO
-
-* [sealos add](sealos_add.md) - add some node
-* [sealos apply](sealos_apply.md) - apply a kubernetes cluster
-* [sealos build](sealos_build.md) - build an cloud image from a Kubefile
-* [sealos completion](sealos_completion.md) - Generate the autocompletion script for the specified shell
-* [sealos create](sealos_create.md) - Create a cluster without running the CMD
-* [sealos delete](sealos_delete.md) - delete some node
-* [sealos docs](sealos_docs.md) - generate API reference
-* [sealos exec](sealos_exec.md) - exec a shell command or script on all node.
-* [sealos gen](sealos_gen.md) - Generate a Clusterfile
-* [sealos images](sealos_images.md) - list cloud image
-* [sealos load](sealos_load.md) - load cloud image
-* [sealos login](sealos_login.md) - login image repository
-* [sealos logout](sealos_logout.md) - logout image repository
-* [sealos prune](sealos_prune.md) - prune image
-* [sealos pull](sealos_pull.md) - pull cloud image
-* [sealos push](sealos_push.md) - push cloud image
-* [sealos reset](sealos_reset.md) - Simplest way to reset your cluster
-* [sealos rmi](sealos_rmi.md) - Remove one or more cloud images
-* [sealos run](sealos_run.md) - Simplest way to run your kubernetes HA cluster
-* [sealos save](sealos_save.md) - save cloud image to a tar file
-* [sealos scp](sealos_scp.md) - copy local file to remote on all node.
-* [sealos tag](sealos_tag.md) - tag a image as a new one
-* [sealos version](sealos_version.md) - version
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_add.md b/docs/api/sealos_add.md
deleted file mode 100644
index 0078bfc81f37..000000000000
--- a/docs/api/sealos_add.md
+++ /dev/null
@@ -1,42 +0,0 @@
-## sealos add
-
-add some node
-
-```
-sealos add [flags]
-```
-
-### Examples
-
-```
-
-add to nodes :
- sealos add --nodes x.x.x.x
-
-add to default cluster:
- sealos add --masters x.x.x.x --nodes x.x.x.x
- sealos add --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y
-
-```
-
-### Options
-
-```
- -c, --cluster string delete a kubernetes cluster with cluster name (default "default")
- -h, --help help for add
- -m, --masters string reduce Count or IPList to masters
- -n, --nodes string reduce Count or IPList to nodes
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_apply.md b/docs/api/sealos_apply.md
deleted file mode 100644
index a026288b79a4..000000000000
--- a/docs/api/sealos_apply.md
+++ /dev/null
@@ -1,33 +0,0 @@
-## sealos apply
-
-apply a kubernetes cluster
-
-```
-sealos apply [flags]
-```
-
-### Examples
-
-```
-sealos apply -f Clusterfile
-```
-
-### Options
-
-```
- -f, --Clusterfile string apply a kubernetes cluster (default "Clusterfile")
- -h, --help help for apply
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_build.md b/docs/api/sealos_build.md
deleted file mode 100644
index 16e8d3864aa0..000000000000
--- a/docs/api/sealos_build.md
+++ /dev/null
@@ -1,37 +0,0 @@
-## sealos build
-
-build an cloud image from a Kubefile
-
-```
-sealos build [flags] PATH
-```
-
-### Examples
-
-```
-sealos build -t labring/kubernetes:v1.24.0 .
-```
-
-### Options
-
-```
- --basic-auth pull image auth policy,default is token auth
- -h, --help help for build
- -f, --kubefile string kubefile filepath (default "Kubefile")
- -m, --max-pull-procs int maximum number of goroutines for pulling (default 5)
- --platform string set the OS/ARCH/VARIANT of the image to the provided value instead of the current operating system and architecture of the host (for example linux/arm) (default "linux/arm64")
- -t, --tag string tagged name to apply to the built image
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_completion.md b/docs/api/sealos_completion.md
deleted file mode 100644
index 0171f17fb476..000000000000
--- a/docs/api/sealos_completion.md
+++ /dev/null
@@ -1,32 +0,0 @@
-## sealos completion
-
-Generate the autocompletion script for the specified shell
-
-### Synopsis
-
-Generate the autocompletion script for sealos for the specified shell.
-See each sub-command's help for details on how to use the generated script.
-
-
-### Options
-
-```
- -h, --help help for completion
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-* [sealos completion bash](sealos_completion_bash.md) - Generate the autocompletion script for bash
-* [sealos completion fish](sealos_completion_fish.md) - Generate the autocompletion script for fish
-* [sealos completion powershell](sealos_completion_powershell.md) - Generate the autocompletion script for powershell
-* [sealos completion zsh](sealos_completion_zsh.md) - Generate the autocompletion script for zsh
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_completion_bash.md b/docs/api/sealos_completion_bash.md
deleted file mode 100644
index 18382ceafede..000000000000
--- a/docs/api/sealos_completion_bash.md
+++ /dev/null
@@ -1,51 +0,0 @@
-## sealos completion bash
-
-Generate the autocompletion script for bash
-
-### Synopsis
-
-Generate the autocompletion script for the bash shell.
-
-This script depends on the 'bash-completion' package.
-If it is not installed already, you can install it via your OS's package manager.
-
-To load completions in your current shell session:
-
- source <(sealos completion bash)
-
-To load completions for every new session, execute once:
-
-#### Linux:
-
- sealos completion bash > /etc/bash_completion.d/sealos
-
-#### macOS:
-
- sealos completion bash > /usr/local/etc/bash_completion.d/sealos
-
-You will need to start a new shell for this setup to take effect.
-
-
-```
-sealos completion bash
-```
-
-### Options
-
-```
- -h, --help help for bash
- --no-descriptions disable completion descriptions
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos completion](sealos_completion.md) - Generate the autocompletion script for the specified shell
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_completion_fish.md b/docs/api/sealos_completion_fish.md
deleted file mode 100644
index f6596d086919..000000000000
--- a/docs/api/sealos_completion_fish.md
+++ /dev/null
@@ -1,42 +0,0 @@
-## sealos completion fish
-
-Generate the autocompletion script for fish
-
-### Synopsis
-
-Generate the autocompletion script for the fish shell.
-
-To load completions in your current shell session:
-
- sealos completion fish | source
-
-To load completions for every new session, execute once:
-
- sealos completion fish > ~/.config/fish/completions/sealos.fish
-
-You will need to start a new shell for this setup to take effect.
-
-
-```
-sealos completion fish [flags]
-```
-
-### Options
-
-```
- -h, --help help for fish
- --no-descriptions disable completion descriptions
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos completion](sealos_completion.md) - Generate the autocompletion script for the specified shell
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_completion_powershell.md b/docs/api/sealos_completion_powershell.md
deleted file mode 100644
index b7a554636583..000000000000
--- a/docs/api/sealos_completion_powershell.md
+++ /dev/null
@@ -1,39 +0,0 @@
-## sealos completion powershell
-
-Generate the autocompletion script for powershell
-
-### Synopsis
-
-Generate the autocompletion script for powershell.
-
-To load completions in your current shell session:
-
- sealos completion powershell | Out-String | Invoke-Expression
-
-To load completions for every new session, add the output of the above command
-to your powershell profile.
-
-
-```
-sealos completion powershell [flags]
-```
-
-### Options
-
-```
- -h, --help help for powershell
- --no-descriptions disable completion descriptions
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos completion](sealos_completion.md) - Generate the autocompletion script for the specified shell
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_completion_zsh.md b/docs/api/sealos_completion_zsh.md
deleted file mode 100644
index 4df9ded0517b..000000000000
--- a/docs/api/sealos_completion_zsh.md
+++ /dev/null
@@ -1,49 +0,0 @@
-## sealos completion zsh
-
-Generate the autocompletion script for zsh
-
-### Synopsis
-
-Generate the autocompletion script for the zsh shell.
-
-If shell completion is not already enabled in your environment you will need
-to enable it. You can execute the following once:
-
- echo "autoload -U compinit; compinit" >> ~/.zshrc
-
-To load completions for every new session, execute once:
-
-#### Linux:
-
- sealos completion zsh > "${fpath[1]}/_sealos"
-
-#### macOS:
-
- sealos completion zsh > /usr/local/share/zsh/site-functions/_sealos
-
-You will need to start a new shell for this setup to take effect.
-
-
-```
-sealos completion zsh [flags]
-```
-
-### Options
-
-```
- -h, --help help for zsh
- --no-descriptions disable completion descriptions
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos completion](sealos_completion.md) - Generate the autocompletion script for the specified shell
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_create.md b/docs/api/sealos_create.md
deleted file mode 100644
index 574394dc6d50..000000000000
--- a/docs/api/sealos_create.md
+++ /dev/null
@@ -1,42 +0,0 @@
-## sealos create
-
-Create a cluster without running the CMD
-
-### Synopsis
-
-sealos create [image name] [args]
-
-```
-sealos create [flags]
-```
-
-### Examples
-
-```
-
-create a mysql cluster:
- sealos create mysql:8.0
-with custom cluster name:
- sealos create mysql:8.0 -c mysql
-
-```
-
-### Options
-
-```
- -c, --cluster-name string set custom cluster name (default "default")
- -h, --help help for create
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_delete.md b/docs/api/sealos_delete.md
deleted file mode 100644
index 783f4b0f29e9..000000000000
--- a/docs/api/sealos_delete.md
+++ /dev/null
@@ -1,51 +0,0 @@
-## sealos delete
-
-delete some node
-
-```
-sealos delete [flags]
-```
-
-### Examples
-
-```
-
-delete nodes:
- sealos delete --nodes x.x.x.x
- if accidentally deleted;
- Use 'sealos add' to recover:
- sealos add --nodes x.x.x.x
-
-delete masters:
- sealos delete --masters x.x.x.x
-
-delete masters and nodes:
- sealos delete --masters x.x.x.x --nodes x.x.x.x
- sealos delete --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y
-
-Please note that sealos will delete your master if the --masters parameter is specified.
-
-```
-
-### Options
-
-```
- -c, --cluster string delete a kubernetes cluster with cluster name (default "default")
- --force We also can input an --force flag to delete cluster by force
- -h, --help help for delete
- -m, --masters string reduce Count or IPList to masters
- -n, --nodes string reduce Count or IPList to nodes
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_docs.md b/docs/api/sealos_docs.md
deleted file mode 100644
index 7ad0488c08dd..000000000000
--- a/docs/api/sealos_docs.md
+++ /dev/null
@@ -1,33 +0,0 @@
-## sealos docs
-
-generate API reference
-
-```
-sealos docs [flags]
-```
-
-### Examples
-
-```
-sealos docs
-```
-
-### Options
-
-```
- -h, --help help for docs
- -p, --path string path to output docs (default "./docs/api")
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_exec.md b/docs/api/sealos_exec.md
deleted file mode 100644
index ba2068765958..000000000000
--- a/docs/api/sealos_exec.md
+++ /dev/null
@@ -1,44 +0,0 @@
-## sealos exec
-
-exec a shell command or script on all node.
-
-```
-sealos exec [flags]
-```
-
-### Examples
-
-```
-
-exec to default cluster: default
- sealos exec "cat /etc/hosts"
-specify the cluster name(If there is only one cluster in the $HOME/.sealos directory, it should be applied. ):
- sealos exec -c my-cluster "cat /etc/hosts"
-set role label to exec cmd:
- sealos exec -c my-cluster -r master,slave,node1 "cat /etc/hosts"
-set ips to exec cmd:
- sealos exec -c my-cluster --ips 172.16.1.38 "cat /etc/hosts"
-
-```
-
-### Options
-
-```
- -c, --cluster-name string submit one cluster name (default "default")
- -h, --help help for exec
- --ips strings ssh ips list on node
- -r, --roles string set role label to roles
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_gen.md b/docs/api/sealos_gen.md
deleted file mode 100644
index bdc6a0dbfbee..000000000000
--- a/docs/api/sealos_gen.md
+++ /dev/null
@@ -1,59 +0,0 @@
-## sealos gen
-
-Generate a Clusterfile
-
-### Synopsis
-
-Generate a Clusterfile of the kubernetes cluster, which can be applied by 'sealos apply' command
-
-```
-sealos gen [flags]
-```
-
-### Examples
-
-```
-
-generate a cluster with multi images, specify masters and nodes:
- sealos gen labring/kubernetes:v1.24.0 labring/calico:v3.22.1 \
- --masters 192.168.0.2,192.168.0.3,192.168.0.4 \
- --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd xxx
-
-specify server InfraSSH port:
- all servers use the same InfraSSH port:
- sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \
- --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd xxx
- different InfraSSH port numbers:
- sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3:23,192.168.0.4:24 \
- --nodes 192.168.0.5:25,192.168.0.6:25,192.168.0.7:27 --passwd xxx
-
-```
-
-### Options
-
-```
- --cmd strings set cmd for image cmd instruction
- -e, --env strings set custom environment variables
- -h, --help help for gen
- -m, --masters string set Count or IPList to masters
- --name string set cluster name variables (default "default")
- -n, --nodes string set Count or IPList to nodes
- -p, --passwd string set cloud provider or baremetal server password
- --pk string set baremetal server private key (default "/home/ubuntu/.ssh/id_rsa")
- --pk-passwd string set baremetal server private key password
- --port uint16 set the sshd service port number for the server (default 22)
- -u, --user string set baremetal server username (default "root")
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_images.md b/docs/api/sealos_images.md
deleted file mode 100644
index 5270311b41b5..000000000000
--- a/docs/api/sealos_images.md
+++ /dev/null
@@ -1,32 +0,0 @@
-## sealos images
-
-list cloud image
-
-```
-sealos images [flags]
-```
-
-### Examples
-
-```
-sealos images
-```
-
-### Options
-
-```
- -h, --help help for images
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_load.md b/docs/api/sealos_load.md
deleted file mode 100644
index 0e1c40daa07d..000000000000
--- a/docs/api/sealos_load.md
+++ /dev/null
@@ -1,33 +0,0 @@
-## sealos load
-
-load cloud image
-
-```
-sealos load [flags]
-```
-
-### Examples
-
-```
-sealos load -i kubernetes.tar
-```
-
-### Options
-
-```
- -h, --help help for load
- -i, --input string read image from tar archive file
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_login.md b/docs/api/sealos_login.md
deleted file mode 100644
index 865a63f0289d..000000000000
--- a/docs/api/sealos_login.md
+++ /dev/null
@@ -1,34 +0,0 @@
-## sealos login
-
-login image repository
-
-```
-sealos login [flags]
-```
-
-### Examples
-
-```
-sealos login registry.cn-qingdao.aliyuncs.com -u [username] -p [password]
-```
-
-### Options
-
-```
- -h, --help help for login
- -p, --passwd string password for login registry
- -u, --username string user name for login registry
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_logout.md b/docs/api/sealos_logout.md
deleted file mode 100644
index a0b34aa35766..000000000000
--- a/docs/api/sealos_logout.md
+++ /dev/null
@@ -1,32 +0,0 @@
-## sealos logout
-
-logout image repository
-
-```
-sealos logout [flags]
-```
-
-### Examples
-
-```
-sealos logout registry.cn-qingdao.aliyuncs.com
-```
-
-### Options
-
-```
- -h, --help help for logout
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_prune.md b/docs/api/sealos_prune.md
deleted file mode 100644
index 4b4f91794557..000000000000
--- a/docs/api/sealos_prune.md
+++ /dev/null
@@ -1,32 +0,0 @@
-## sealos prune
-
-prune image
-
-```
-sealos prune [flags]
-```
-
-### Examples
-
-```
-sealos prune
-```
-
-### Options
-
-```
- -h, --help help for prune
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_pull.md b/docs/api/sealos_pull.md
deleted file mode 100644
index 6e675f5ccab9..000000000000
--- a/docs/api/sealos_pull.md
+++ /dev/null
@@ -1,32 +0,0 @@
-## sealos pull
-
-pull cloud image
-
-```
-sealos pull [flags]
-```
-
-### Examples
-
-```
-sealos pull labring/kubernetes:v1.24.0
-```
-
-### Options
-
-```
- -h, --help help for pull
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_push.md b/docs/api/sealos_push.md
deleted file mode 100644
index fc14f179ed1e..000000000000
--- a/docs/api/sealos_push.md
+++ /dev/null
@@ -1,32 +0,0 @@
-## sealos push
-
-push cloud image
-
-```
-sealos push [flags]
-```
-
-### Examples
-
-```
-sealos push labring/kubernetes:v1.24.0
-```
-
-### Options
-
-```
- -h, --help help for push
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_reset.md b/docs/api/sealos_reset.md
deleted file mode 100644
index 2ed9e2c937c8..000000000000
--- a/docs/api/sealos_reset.md
+++ /dev/null
@@ -1,48 +0,0 @@
-## sealos reset
-
-Simplest way to reset your cluster
-
-### Synopsis
-
-sealos reset --name [arg]
-
-```
-sealos reset [flags]
-```
-
-### Examples
-
-```
-
-reset you current cluster:
- sealos reset --name xxx [--force]
-
-```
-
-### Options
-
-```
- --force we also can input an --force flag to reset cluster by force
- -h, --help help for reset
- -m, --masters string set Count or IPList to masters
- --name string set cluster name variables (default "default")
- -n, --nodes string set Count or IPList to nodes
- -p, --passwd string set cloud provider or baremetal server password
- --pk string set baremetal server private key (default "/home/ubuntu/.ssh/id_rsa")
- --pk-passwd string set baremetal server private key password
- --port uint16 set the sshd service port number for the server (default 22)
- -u, --user string set baremetal server username (default "root")
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_rmi.md b/docs/api/sealos_rmi.md
deleted file mode 100644
index c56520712817..000000000000
--- a/docs/api/sealos_rmi.md
+++ /dev/null
@@ -1,33 +0,0 @@
-## sealos rmi
-
-Remove one or more cloud images
-
-```
-sealos rmi [flags]
-```
-
-### Examples
-
-```
-sealos rmi [-f] labring/kubernetes:v1.24.0
-```
-
-### Options
-
-```
- -f, --force force removal all of the image
- -h, --help help for rmi
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_run.md b/docs/api/sealos_run.md
deleted file mode 100644
index 2d7b4cb7e8e6..000000000000
--- a/docs/api/sealos_run.md
+++ /dev/null
@@ -1,65 +0,0 @@
-## sealos run
-
-Simplest way to run your kubernetes HA cluster
-
-### Synopsis
-
-sealos run labring/kubernetes:v1.24.0 --masters [arg] --nodes [arg]
-
-```
-sealos run [flags]
-```
-
-### Examples
-
-```
-
-create cluster to your baremetal server, appoint the iplist:
- sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \
- --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd xxx
- multi image:
- sealos run labring/kubernetes:v1.24.0 calico:v3.22.1 \
- --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19
- Specify server InfraSSH port :
- All servers use the same InfraSSH port (default port: 22):
- sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \
- --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd xxx
- Different InfraSSH port numbers exist:
- sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3:23,192.168.0.4:24 \
- --nodes 192.168.0.5:25,192.168.0.6:25,192.168.0.7:27 --passwd xxx
-
-create a cluster with custom environment variables:
- sealos run -e DashBoardPort=8443 mydashboard:latest --masters 192.168.0.2,192.168.0.3,192.168.0.4 \
- --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd xxx
-
-```
-
-### Options
-
-```
- --cmd strings set cmd for image cmd instruction
- -e, --env strings set custom environment variables
- -f, --force we also can input an --force flag to run app in this cluster by force
- -h, --help help for run
- -m, --masters string set Count or IPList to masters
- --name string set cluster name variables (default "default")
- -n, --nodes string set Count or IPList to nodes
- -p, --passwd string set cloud provider or baremetal server password
- --pk string set baremetal server private key (default "/home/ubuntu/.ssh/id_rsa")
- --pk-passwd string set baremetal server private key password
- --port uint16 set the sshd service port number for the server (default 22)
- -u, --user string set baremetal server username (default "root")
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_save.md b/docs/api/sealos_save.md
deleted file mode 100644
index 0ef24c475216..000000000000
--- a/docs/api/sealos_save.md
+++ /dev/null
@@ -1,33 +0,0 @@
-## sealos save
-
-save cloud image to a tar file
-
-```
-sealos save [flags]
-```
-
-### Examples
-
-```
-sealos save -o kubernetes.tar labring/kubernetes:v1.24.0
-```
-
-### Options
-
-```
- -h, --help help for save
- -o, --output string read image from tar archive file
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_scp.md b/docs/api/sealos_scp.md
deleted file mode 100644
index 17d6f27fd285..000000000000
--- a/docs/api/sealos_scp.md
+++ /dev/null
@@ -1,44 +0,0 @@
-## sealos scp
-
-copy local file to remote on all node.
-
-```
-sealos scp [flags]
-```
-
-### Examples
-
-```
-
-copy file to default cluster: default
- sealos scp "/root/aa.txt" "/root/dd.txt"
-specify the cluster name(If there is only one cluster in the $HOME/.sealos directory, it should be applied. ):
- sealos scp -c my-cluster "/root/aa.txt" "/root/dd.txt"
-set role label to copy file:
- sealos scp -c my-cluster -r master,slave,node1 "cat /etc/hosts"
-set ips to copy file:
- sealos scp -c my-cluster --ips 172.16.1.38 "/root/aa.txt" "/root/dd.txt"
-
-```
-
-### Options
-
-```
- -c, --cluster-name string submit one cluster name (default "default")
- -h, --help help for scp
- --ips strings ssh ips list on node
- -r, --roles string set role label to roles
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_tag.md b/docs/api/sealos_tag.md
deleted file mode 100644
index 038e50617c76..000000000000
--- a/docs/api/sealos_tag.md
+++ /dev/null
@@ -1,32 +0,0 @@
-## sealos tag
-
-tag a image as a new one
-
-```
-sealos tag [flags]
-```
-
-### Examples
-
-```
-sealos tag labring/kubernetes:v1.24.0 oci-kubernetes:v1.24.0
-```
-
-### Options
-
-```
- -h, --help help for tag
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/api/sealos_version.md b/docs/api/sealos_version.md
deleted file mode 100644
index 829896a689ba..000000000000
--- a/docs/api/sealos_version.md
+++ /dev/null
@@ -1,33 +0,0 @@
-## sealos version
-
-version
-
-```
-sealos version [flags]
-```
-
-### Examples
-
-```
-sealos version
-```
-
-### Options
-
-```
- -h, --help help for version
- --short if true, print just the version number.
-```
-
-### Options inherited from parent commands
-
-```
- --cluster-root string cluster root directory (default "/var/lib/sealos")
- --debug enable debug logger
-```
-
-### SEE ALSO
-
-* [sealos](sealos.md) - simplest way install kubernetes tools.
-
-###### Auto generated by spf13/cobra on 1-Jul-2022
diff --git a/docs/3.0/README_en.md b/docs/archived/3.0/README_en.md
similarity index 100%
rename from docs/3.0/README_en.md
rename to docs/archived/3.0/README_en.md
diff --git a/docs/3.0/README_zh.md b/docs/archived/3.0/README_zh.md
similarity index 100%
rename from docs/3.0/README_zh.md
rename to docs/archived/3.0/README_zh.md
diff --git a/docs/3.0/command/cloud.md b/docs/archived/3.0/command/cloud.md
similarity index 100%
rename from docs/3.0/command/cloud.md
rename to docs/archived/3.0/command/cloud.md
diff --git a/docs/3.0/command/etcd.md b/docs/archived/3.0/command/etcd.md
similarity index 100%
rename from docs/3.0/command/etcd.md
rename to docs/archived/3.0/command/etcd.md
diff --git a/docs/3.0/command/exec.md b/docs/archived/3.0/command/exec.md
similarity index 100%
rename from docs/3.0/command/exec.md
rename to docs/archived/3.0/command/exec.md
diff --git a/docs/3.0/command/route.md b/docs/archived/3.0/command/route.md
similarity index 100%
rename from docs/3.0/command/route.md
rename to docs/archived/3.0/command/route.md
diff --git a/docs/3.0/command/upgrade.md b/docs/archived/3.0/command/upgrade.md
similarity index 100%
rename from docs/3.0/command/upgrade.md
rename to docs/archived/3.0/command/upgrade.md
diff --git a/docs/3.0/design/design.md b/docs/archived/3.0/design/design.md
similarity index 100%
rename from docs/3.0/design/design.md
rename to docs/archived/3.0/design/design.md
diff --git a/docs/3.0/history/v2.0.md b/docs/archived/3.0/history/v2.0.md
similarity index 100%
rename from docs/3.0/history/v2.0.md
rename to docs/archived/3.0/history/v2.0.md
diff --git a/docs/3.0/history/v3.0.md b/docs/archived/3.0/history/v3.0.md
similarity index 100%
rename from docs/3.0/history/v3.0.md
rename to docs/archived/3.0/history/v3.0.md
diff --git a/docs/3.0/plan/join_master_failed.md b/docs/archived/3.0/plan/join_master_failed.md
similarity index 100%
rename from docs/3.0/plan/join_master_failed.md
rename to docs/archived/3.0/plan/join_master_failed.md
diff --git a/docs/3.0/plan/multi_network_install.md b/docs/archived/3.0/plan/multi_network_install.md
similarity index 100%
rename from docs/3.0/plan/multi_network_install.md
rename to docs/archived/3.0/plan/multi_network_install.md
diff --git a/docs/3.0/plan/recovery_sealos.md b/docs/archived/3.0/plan/recovery_sealos.md
similarity index 100%
rename from docs/3.0/plan/recovery_sealos.md
rename to docs/archived/3.0/plan/recovery_sealos.md
diff --git a/docs/3.0/quick_start.md b/docs/archived/3.0/quick_start.md
similarity index 100%
rename from docs/3.0/quick_start.md
rename to docs/archived/3.0/quick_start.md
diff --git a/docs/3.0/user_guide.md b/docs/archived/3.0/user_guide.md
similarity index 100%
rename from docs/3.0/user_guide.md
rename to docs/archived/3.0/user_guide.md
diff --git a/docs/archived/4.0/code.json b/docs/archived/4.0/code.json
new file mode 100644
index 000000000000..7f236043c689
--- /dev/null
+++ b/docs/archived/4.0/code.json
@@ -0,0 +1,646 @@
+{
+ "theme.ErrorPageContent.title": {
+ "message": "页面已崩溃。",
+ "description": "The title of the fallback page when the page crashed"
+ },
+ "theme.ErrorPageContent.tryAgain": {
+ "message": "重试",
+ "description": "The label of the button to try again rendering when the React error boundary captures an error"
+ },
+ "theme.NotFound.title": {
+ "message": "找不到页面",
+ "description": "The title of the 404 page"
+ },
+ "theme.NotFound.p1": {
+ "message": "我们找不到您要找的页面。",
+ "description": "The first paragraph of the 404 page"
+ },
+ "theme.NotFound.p2": {
+ "message": "请联系原始链接来源网站的所有者,并告知他们链接已损坏。",
+ "description": "The 2nd paragraph of the 404 page"
+ },
+ "theme.AnnouncementBar.closeButtonAriaLabel": {
+ "message": "关闭",
+ "description": "The ARIA label for close button of announcement bar"
+ },
+ "theme.BackToTopButton.buttonAriaLabel": {
+ "message": "回到顶部",
+ "description": "The ARIA label for the back to top button"
+ },
+ "theme.blog.paginator.navAriaLabel": {
+ "message": "博文列表分页导航",
+ "description": "The ARIA label for the blog pagination"
+ },
+ "theme.blog.paginator.newerEntries": {
+ "message": "较新的博文",
+ "description": "The label used to navigate to the newer blog posts page (previous page)"
+ },
+ "theme.blog.paginator.olderEntries": {
+ "message": "较旧的博文",
+ "description": "The label used to navigate to the older blog posts page (next page)"
+ },
+ "theme.blog.archive.title": {
+ "message": "历史博文",
+ "description": "The page & hero title of the blog archive page"
+ },
+ "theme.blog.archive.description": {
+ "message": "历史博文",
+ "description": "The page & hero description of the blog archive page"
+ },
+ "theme.blog.post.readingTime.plurals": {
+ "message": "{readingTime} 分钟阅读",
+ "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
+ },
+ "theme.blog.post.readMoreLabel": {
+ "message": "阅读 {title} 的全文",
+ "description": "The ARIA label for the link to full blog posts from excerpts"
+ },
+ "theme.blog.post.readMore": {
+ "message": "阅读更多",
+ "description": "The label used in blog post item excerpts to link to full blog posts"
+ },
+ "theme.blog.post.paginator.navAriaLabel": {
+ "message": "博文分页导航",
+ "description": "The ARIA label for the blog posts pagination"
+ },
+ "theme.blog.post.paginator.newerPost": {
+ "message": "较新一篇",
+ "description": "The blog post button label to navigate to the newer/previous post"
+ },
+ "theme.blog.post.paginator.olderPost": {
+ "message": "较旧一篇",
+ "description": "The blog post button label to navigate to the older/next post"
+ },
+ "theme.blog.post.plurals": {
+ "message": "{count} 篇博文",
+ "description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
+ },
+ "theme.blog.tagTitle": {
+ "message": "{nPosts} 含有标签「{tagName}」",
+ "description": "The title of the page for a blog tag"
+ },
+ "theme.tags.tagsPageLink": {
+ "message": "查看所有标签",
+ "description": "The label of the link targeting the tag list page"
+ },
+ "theme.colorToggle.ariaLabel": {
+ "message": "切换浅色/暗黑模式(当前为{mode})",
+ "description": "The ARIA label for the navbar color mode toggle"
+ },
+ "theme.colorToggle.ariaLabel.mode.dark": {
+ "message": "暗黑模式",
+ "description": "The name for the dark color mode"
+ },
+ "theme.colorToggle.ariaLabel.mode.light": {
+ "message": "浅色模式",
+ "description": "The name for the light color mode"
+ },
+ "theme.docs.breadcrumbs.home": {
+ "message": "主页面",
+ "description": "The ARIA label for the home page in the breadcrumbs"
+ },
+ "theme.docs.breadcrumbs.navAriaLabel": {
+ "message": "页面路径",
+ "description": "The ARIA label for the breadcrumbs"
+ },
+ "theme.docs.DocCard.categoryDescription": {
+ "message": "{count} 个项目",
+ "description": "The default description for a category card in the generated index about how many items this category includes"
+ },
+ "theme.docs.paginator.navAriaLabel": {
+ "message": "文档分页导航",
+ "description": "The ARIA label for the docs pagination"
+ },
+ "theme.docs.paginator.previous": {
+ "message": "上一页",
+ "description": "The label used to navigate to the previous doc"
+ },
+ "theme.docs.paginator.next": {
+ "message": "下一页",
+ "description": "The label used to navigate to the next doc"
+ },
+ "theme.docs.tagDocListPageTitle.nDocsTagged": {
+ "message": "{count} 篇文档带有标签",
+ "description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
+ },
+ "theme.docs.tagDocListPageTitle": {
+ "message": "{nDocsTagged}「{tagName}」",
+ "description": "The title of the page for a docs tag"
+ },
+ "theme.docs.versionBadge.label": {
+ "message": "版本:{versionLabel}"
+ },
+ "theme.docs.versions.unreleasedVersionLabel": {
+ "message": "此为 {siteTitle} {versionLabel} 版尚未发行的文档。",
+ "description": "The label used to tell the user that he's browsing an unreleased doc version"
+ },
+ "theme.docs.versions.unmaintainedVersionLabel": {
+ "message": "此为 {siteTitle} {versionLabel} 版的文档,现已不再积极维护。",
+ "description": "The label used to tell the user that he's browsing an unmaintained doc version"
+ },
+ "theme.docs.versions.latestVersionSuggestionLabel": {
+ "message": "最新的文档请参阅 {latestVersionLink} ({versionLabel})。",
+ "description": "The label used to tell the user to check the latest version"
+ },
+ "theme.docs.versions.latestVersionLinkLabel": {
+ "message": "最新版本",
+ "description": "The label used for the latest version suggestion link label"
+ },
+ "theme.common.editThisPage": {
+ "message": "编辑此页",
+ "description": "The link label to edit the current page"
+ },
+ "theme.common.headingLinkTitle": {
+ "message": "标题的直接链接",
+ "description": "Title for link to heading"
+ },
+ "theme.lastUpdated.atDate": {
+ "message": "于 {date} ",
+ "description": "The words used to describe on which date a page has been last updated"
+ },
+ "theme.lastUpdated.byUser": {
+ "message": "由 {user} ",
+ "description": "The words used to describe by who the page has been last updated"
+ },
+ "theme.lastUpdated.lastUpdatedAtBy": {
+ "message": "最后{byUser}{atDate}更新",
+ "description": "The sentence used to display when a page has been last updated, and by who"
+ },
+ "theme.navbar.mobileVersionsDropdown.label": {
+ "message": "选择版本",
+ "description": "The label for the navbar versions dropdown on mobile view"
+ },
+ "theme.common.skipToMainContent": {
+ "message": "跳到主要内容",
+ "description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation"
+ },
+ "theme.tags.tagsListLabel": {
+ "message": "标签:",
+ "description": "The label alongside a tag list"
+ },
+ "theme.blog.sidebar.navAriaLabel": {
+ "message": "最近博文导航",
+ "description": "The ARIA label for recent posts in the blog sidebar"
+ },
+ "theme.CodeBlock.copied": {
+ "message": "复制成功",
+ "description": "The copied button label on code blocks"
+ },
+ "theme.CodeBlock.copyButtonAriaLabel": {
+ "message": "复制代码到剪贴板",
+ "description": "The ARIA label for copy code blocks button"
+ },
+ "theme.CodeBlock.copy": {
+ "message": "复制",
+ "description": "The copy button label on code blocks"
+ },
+ "theme.CodeBlock.wordWrapToggle": {
+ "message": "切换自动换行",
+ "description": "The title attribute for toggle word wrapping button of code block lines"
+ },
+ "theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": {
+ "message": "打开/收起侧边栏菜单「{label}」",
+ "description": "The ARIA label to toggle the collapsible sidebar category"
+ },
+ "theme.navbar.mobileLanguageDropdown.label": {
+ "message": "选择语言",
+ "description": "The label for the mobile language switcher dropdown"
+ },
+ "theme.TOCCollapsible.toggleButtonLabel": {
+ "message": "本页总览",
+ "description": "The label used by the button on the collapsible TOC component"
+ },
+ "theme.docs.sidebar.collapseButtonTitle": {
+ "message": "收起侧边栏",
+ "description": "The title attribute for collapse button of doc sidebar"
+ },
+ "theme.docs.sidebar.collapseButtonAriaLabel": {
+ "message": "收起侧边栏",
+ "description": "The title attribute for collapse button of doc sidebar"
+ },
+ "theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": {
+ "message": "← 回到主菜单",
+ "description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)"
+ },
+ "theme.docs.sidebar.expandButtonTitle": {
+ "message": "展开侧边栏",
+ "description": "The ARIA label and title attribute for expand button of doc sidebar"
+ },
+ "theme.docs.sidebar.expandButtonAriaLabel": {
+ "message": "展开侧边栏",
+ "description": "The ARIA label and title attribute for expand button of doc sidebar"
+ },
+ "Simple": {
+ "message": "简单",
+ "description": "homepage simple"
+ },
+ "Any highly available distributed application on Kubernetes can be easily installed with just one click.": {
+ "message": "像使用个人电脑一样在 Kubernetes 上一键安装任意高可用分布式应用程序,几乎不需要任何专业的交付和运维成本",
+ "description": "homepage simple"
+ },
+ "Flexible": {
+ "message": "灵活",
+ "description": "homepage flexible"
+ },
+ "Easily customize the cloud you need by freely combining various distributed applications.": {
+ "message": "利用独特的集群镜像能力将任意分布式应用打包成 OCI 镜像,自由组合各种分布式应用程序,轻松订制所需的云",
+ "description": "homepage flexible intro"
+ },
+ "Powerful": {
+ "message": "强大",
+ "description": "homepage powerful"
+ },
+ "The cloud services can be easily found and acquired in the application marketplace, offering simplicity and power.": {
+ "message": "通过应用商店来灵活满足各类用户的需求,形成强大的应用生态",
+ "description": "homepage flexible intro"
+ },
+ "Kernel Arch": {
+ "message": "以 kubernetes 为云内核架构"
+ },
+ "Cloud Driver": {
+ "message": "云驱动"
+ },
+ "Cloud Kernel": {
+ "message": "云内核"
+ },
+ "Distributed Applications": {
+ "message": "分布式应用程序"
+ },
+ "Used By": {
+ "message": "客户列表"
+ },
+ "theme.SearchBar.seeAll": {
+ "message": "查看全部 {count} 个结果"
+ },
+ "theme.SearchBar.label": {
+ "message": "搜索",
+ "description": "The ARIA label and placeholder for search button"
+ },
+ "theme.SearchPage.documentsFound.plurals": {
+ "message": "找到 {count} 份文件",
+ "description": "Pluralized label for \"{count} documents found\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
+ },
+ "theme.SearchPage.existingResultsTitle": {
+ "message": "「{query}」的搜索结果",
+ "description": "The search page title for non-empty query"
+ },
+ "theme.SearchPage.emptyResultsTitle": {
+ "message": "在文档中搜索",
+ "description": "The search page title for empty query"
+ },
+ "theme.SearchPage.inputPlaceholder": {
+ "message": "在此输入搜索字词",
+ "description": "The placeholder for search page input"
+ },
+ "theme.SearchPage.inputLabel": {
+ "message": "搜索",
+ "description": "The ARIA label for search page input"
+ },
+ "theme.SearchPage.algoliaLabel": {
+ "message": "通过 Algolia 搜索",
+ "description": "The ARIA label for Algolia mention"
+ },
+ "theme.SearchPage.noResultsText": {
+ "message": "未找到任何结果",
+ "description": "The paragraph for empty search result"
+ },
+ "theme.SearchPage.fetchingNewResults": {
+ "message": "正在获取新的搜索结果...",
+ "description": "The paragraph for fetching new search results"
+ },
+ "Contact": {
+ "message": "联系我们"
+ },
+ "Contact US": {
+ "message": "联系我们"
+ },
+ "theme.admonition.note": {
+ "message": "备注",
+ "description": "The default label used for the Note admonition (:::note)"
+ },
+ "theme.admonition.tip": {
+ "message": "提示",
+ "description": "The default label used for the Tip admonition (:::tip)"
+ },
+ "theme.admonition.danger": {
+ "message": "危险",
+ "description": "The default label used for the Danger admonition (:::danger)"
+ },
+ "theme.admonition.info": {
+ "message": "信息",
+ "description": "The default label used for the Info admonition (:::info)"
+ },
+ "theme.admonition.caution": {
+ "message": "警告",
+ "description": "The default label used for the Caution admonition (:::caution)"
+ },
+ "theme.docs.sidebar.closeSidebarButtonAriaLabel": {
+ "message": "Close navigation bar",
+ "description": "The ARIA label for close button of mobile sidebar"
+ },
+ "theme.docs.sidebar.toggleSidebarButtonAriaLabel": {
+ "message": "Toggle navigation bar",
+ "description": "The ARIA label for hamburger menu button of mobile navigation"
+ },
+ "theme.SearchModal.searchBox.resetButtonTitle": {
+ "message": "清除查询",
+ "description": "The label and ARIA label for search box reset button"
+ },
+ "theme.SearchModal.searchBox.cancelButtonText": {
+ "message": "取消",
+ "description": "The label and ARIA label for search box cancel button"
+ },
+ "theme.SearchModal.startScreen.recentSearchesTitle": {
+ "message": "最近搜索",
+ "description": "The title for recent searches"
+ },
+ "theme.SearchModal.startScreen.noRecentSearchesText": {
+ "message": "没有最近搜索",
+ "description": "The text when no recent searches"
+ },
+ "theme.SearchModal.startScreen.saveRecentSearchButtonTitle": {
+ "message": "保存这个搜索",
+ "description": "The label for save recent search button"
+ },
+ "theme.SearchModal.startScreen.removeRecentSearchButtonTitle": {
+ "message": "从历史记录中删除这个搜索",
+ "description": "The label for remove recent search button"
+ },
+ "theme.SearchModal.startScreen.favoriteSearchesTitle": {
+ "message": "收藏",
+ "description": "The title for favorite searches"
+ },
+ "theme.SearchModal.startScreen.removeFavoriteSearchButtonTitle": {
+ "message": "从收藏列表中删除这个搜索",
+ "description": "The label for remove favorite search button"
+ },
+ "theme.SearchModal.errorScreen.titleText": {
+ "message": "无法获取结果",
+ "description": "The title for error screen of search modal"
+ },
+ "theme.SearchModal.errorScreen.helpText": {
+ "message": "你可能需要检查网络连接。",
+ "description": "The help text for error screen of search modal"
+ },
+ "theme.SearchModal.footer.selectText": {
+ "message": "选中",
+ "description": "The explanatory text of the action for the enter key"
+ },
+ "theme.SearchModal.footer.selectKeyAriaLabel": {
+ "message": "Enter 键",
+ "description": "The ARIA label for the Enter key button that makes the selection"
+ },
+ "theme.SearchModal.footer.navigateText": {
+ "message": "导航",
+ "description": "The explanatory text of the action for the Arrow up and Arrow down key"
+ },
+ "theme.SearchModal.footer.navigateUpKeyAriaLabel": {
+ "message": "向上键",
+ "description": "The ARIA label for the Arrow up key button that makes the navigation"
+ },
+ "theme.SearchModal.footer.navigateDownKeyAriaLabel": {
+ "message": "向下键",
+ "description": "The ARIA label for the Arrow down key button that makes the navigation"
+ },
+ "theme.SearchModal.footer.closeText": {
+ "message": "关闭",
+ "description": "The explanatory text of the action for Escape key"
+ },
+ "theme.SearchModal.footer.closeKeyAriaLabel": {
+ "message": "Esc 键",
+ "description": "The ARIA label for the Escape key button that close the modal"
+ },
+ "theme.SearchModal.footer.searchByText": {
+ "message": "搜索提供",
+ "description": "The text explain that the search is making by Algolia"
+ },
+ "theme.SearchModal.noResultsScreen.noResultsText": {
+ "message": "没有结果:",
+ "description": "The text explains that there are no results for the following search"
+ },
+ "theme.SearchModal.noResultsScreen.suggestedQueryText": {
+ "message": "试试搜索",
+ "description": "The text for the suggested query when no results are found for the following search"
+ },
+ "theme.SearchModal.noResultsScreen.reportMissingResultsText": {
+ "message": "认为这个查询应该有结果?",
+ "description": "The text for the question where the user thinks there are missing results"
+ },
+ "theme.SearchModal.noResultsScreen.reportMissingResultsLinkText": {
+ "message": "请告知我们。",
+ "description": "The text for the link to report missing results"
+ },
+ "theme.SearchModal.placeholder": {
+ "message": "搜索文档",
+ "description": "The placeholder of the input of the DocSearch pop-up modal"
+ },
+ "theme.tags.tagsPageTitle": {
+ "message": "标签",
+ "description": "The title of the tag list page"
+ },
+ "Run Your Business on Sealos Cloud": {
+ "message": "Sealos - 以 kubernetes 为内核的云操作系统发行版"
+ },
+ "Features": {
+ "message": "特性"
+ },
+ "Sealos is a Kubernetes distribution, a general-purpose cloud operating system for managing cloud-native applications.": {
+ "message": "Sealos 以云原生的方式,抛弃了传统的云计算架构,转向以 Kubernetes 为云内核的新架构,实现一个开源的、可随处运行的 AWS 替代方案"
+ },
+ "Copy": {
+ "message": "复制"
+ },
+ "START NOW": {
+ "message": "在线使用"
+ },
+ "CONTACT US NOW": {
+ "message": "联系我们"
+ },
+ "Sealos cloud-native App Store": {
+ "message": "Sealos 应用商店"
+ },
+ "Fully compatible with Docker registry, enjoy seamless one-click installation of various cloud-native distributed applications": {
+ "message": "Sealos 应用商店中的每一个应用都是一个集群镜像,集群镜像中包含了分布式应用的所有依赖(例如 Helm Chart、容器镜像和二进制文件),可一键安装。"
+ },
+ "Sealos PostgreSQL Database": {
+ "message": "Sealos PostgreSQL 数据库"
+ },
+ "One-click creation of a highly available PostgreSQL database, with multi-database cluster management and backup and recovery capabilities": {
+ "message": "Sealos 注重为数据库用户提供沉浸式的使用体验,可一键创建高可用 PostgreSQL 数据库,并提供多数据库集群的管理、备份和恢复功能。"
+ },
+ "Sealos Cloud Provider": {
+ "message": "Sealos Cloud Provider"
+ },
+ "Effortlessly establish an independent Kubernetes cluster within minutes on AWS or other public clouds, and effortlessly manage multiple clusters": {
+ "message": "可在 AWS 等公有云上数分钟内创建一个独立的 Kubernetes 集群并管理多个集群,启动性能高出绝大多数公有云的三倍以上,而价格仅为 AWS 的 80%。"
+ },
+ "Examples": {
+ "message": "使用样例"
+ },
+ "Anything else as can be freely combined as Cloud OS Applications, allowing for unlimited combinations to fulfill a wide range of requirements and scenarios.": {
+ "message": "任意分布式应用都可以作为云操作系统的应用进行自由组合,以满足各种需求和场景。"
+ },
+ "By utilizing Kubernetes as the core of the Cloud OS, we can abstract underlying resources, define resource interfaces, and standardize application management.": {
+ "message": "使用 Kubernetes 作为云操作系统内核,抽象底层资源 / 定义资源接口 / 标准化应用程序管理。"
+ },
+ "The Cloud OS leverages CRI, CNI, and CSI as drivers and utilizes technologies such as Containerd, Calico, and OpenEBS to pool resources and ensure seamless access across the compute, storage, and network domains.": {
+ "message": "使用 CRI、CNI、CSI 作为云操作系统驱动程序,并利用 Containerd、Calico、OpenEBS 等插件将所有的计算、存储和网络等资源池化。"
+ },
+ "Comprehensive Kubernetes Cluster management throughout its entire lifecycle, including Installation, Scaling, Backup, Recovery, and Upgrades": {
+ "message": "管理 Kubernetes 集群的整个生命周期,包括集群的包括安装、扩容、备份、恢复和升级。"
+ },
+ "Storage": {
+ "message": "存储"
+ },
+ "Supports block storage, object storage, and file storage with one-click operation": {
+ "message": "一键安装各种分布式存储。"
+ },
+ "Network": {
+ "message": "网络"
+ },
+ "Supports network plugins such as Calico, Flannel, and Cilium with the freedom of customization and choice": {
+ "message": "支持 Calico、Flannel 和 Cilium 等网络插件,可自由选择和定制。"
+ },
+ "Supports both relational and non-relational databases with high availability, automatic backups, and multi-database instance management capabilities": {
+ "message": "支持关系型数据库和非关系型数据库,并提供高可用、自动备份和多数据库实例管理等功能。"
+ },
+ "Monitoring": {
+ "message": "可观测性"
+ },
+ "Easily deploy a monitoring and alarm system with just one click, without the need for additional configuration, and enjoy a simple and intuitive visual experience": {
+ "message": "一键轻松部署监控和告警系统,无需额外配置,享受简单直观的可视化体验。"
+ },
+ "One-click construction of GPU drivers, device discovery, GPU resource monitoring, and the easy creation of a deep learning platform with AI capabilities": {
+ "message": "一键安装 GPU 驱动,监控 GPU 资源,轻松打造具备 AI 能力的深度学习平台。"
+ },
+ "MQ": {
+ "message": "消息队列"
+ },
+ "Supports various mainstream message queues with high availability and automatic monitoring": {
+ "message": "支持各种主流消息队列,并提供高可用和自动监控功能。"
+ },
+ "Dashboard": {
+ "message": "可视化"
+ },
+ "Supports various dashboards, enabling users to manage Kubernetes clusters with ease": {
+ "message": "一键安装各种 Kubernetes 可视化管理工具,轻松管理 Kubernetes 集群。"
+ },
+ "Platform": {
+ "message": "容器管理平台"
+ },
+ "Support various Container Platforms, enabling users to manage Kubernetes clusters with ease": {
+ "message": "一键安装各种 企业级容器管理平台,轻松管理 Kubernetes 集群。"
+ },
+ "Support one-click operation for mainstream CI/CD systems such as Argo CD and Drone": {
+ "message": "一键安装 Argo CD 和 Drone 等主流 DevOps 系统。"
+ },
+ "More than 4,000 Companies and 100,000+ Individuals!": {
+ "message": "已被 4000 多家公司和 10w+ 个人用户采用!"
+ },
+ "Start": {
+ "message": "Home"
+ },
+ "GitOps": {
+ "message": "GitOps"
+ },
+ "Kubernetes": {
+ "message": "Kubernetes"
+ },
+ "GPU": {
+ "message": "GPU"
+ },
+ "theme.NavBar.navAriaLabel": {
+ "message": "主导航",
+ "description": "The ARIA label for the main navigation"
+ },
+ "theme.docs.sidebar.navAriaLabel": {
+ "message": "文档侧边栏",
+ "description": "The ARIA label for the sidebar navigation"
+ },
+ "Docs": {
+ "message": "文档",
+ "description": "The Header Documentation"
+ },
+ "Community": {
+ "message": "社区",
+ "description": "The Header Community"
+ },
+ "Join Us": {
+ "message": "加入我们"
+ },
+ "The Capabilities of Sealos": {
+ "message": "Sealos 的能力"
+ },
+ "Application Management": {
+ "message": "应用管理"
+ },
+ "Easy management and quick release of publicly accessible distributed applications in the app store.": {
+ "message": "在应用商店中轻松管理并快速发布可公网访问的分布式应用。"
+ },
+ "Database": {
+ "message": "数据库管理"
+ },
+ "Create high-availability databases in seconds, offering support for MySQL, PostgreSQL, MongoDB, and Redis.": {
+ "message": "秒级创建高可用数据库,支持 MySQL、PostgreSQL、MongoDB 和 Redis。"
+ },
+ "Cloud Universality": {
+ "message": "公私一致"
+ },
+ "Equally effective in both public and private cloud, enabling a seamless transition of traditional applications to the cloud.": {
+ "message": "即是公有云也是私有云,支持传统应用无缝迁移到云环境。"
+ },
+ "Why Choose Sealos?": {
+ "message": "为什么选择 Sealos"
+ },
+ "Efficient & Cost-Effective": {
+ "message": "高效 & 经济"
+ },
+ "Universal & User-Friendly": {
+ "message": "通用性强,无心智负担"
+ },
+ "Flexible & Secure": {
+ "message": "灵活 & 安全"
+ },
+ "Pay only for the containers you use. Auto-scaling prevents wasted resources, saving you a lot of money.": {
+ "message": "仅需为容器付费,自动伸缩杜绝资源浪费,大幅度节省成本。"
+ },
+ "Focus on your business without worrying about complexity. There is almost no learning curve.": {
+ "message": "专注于业务本身,无需担心复杂性,几乎没有学习成本。"
+ },
+ "The multi-tenant sharing mechanism ensures security while providing resource isolation and efficient collaboration.": {
+ "message": "多租户共享机制在确保安全的同时,实现资源隔离与高效协作。"
+ },
+ "Get early access to the latest Sealos versions and stay connected with developers and users on Discord.": {
+ "message": "加入 Sealos 社区,在这里您可以在第一时间体验 Sealos 的最新版本,以及在 Discord 或微信群与开发者和用户保持沟通"
+ },
+ "Who are Using Sealos": {
+ "message": "谁在使用 Sealos"
+ },
+ "Join Sealos Community": {
+ "message": "加入 Sealos 社区"
+ },
+ "Experience the latest version of Sealos for the first time and communicate with developers and users in Discord!": {
+ "message": "在这里您可以在第一时间体验 Sealos 的最新版本,以及在 Discord 或微信群与开发者和用户保持沟通"
+ },
+ "Start Now": {
+ "message": "立即使用"
+ },
+ "for all applications": {
+ "message": "托管所有分布式应用"
+ },
+ "Hosting": {
+ "message": "私有云"
+ },
+ "A cloud operating system based on the Kubernetes kernel": {
+ "message": "以 Kubernetes 为内核的云操作系统"
+ },
+ "Explore": {
+ "message": "了解更多"
+ },
+ "Blog": {
+ "message": "博客"
+ },
+ "App Store": {
+ "message": "应用商店"
+ }
+}
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/Community.md b/docs/archived/4.0/docs/Community.md
new file mode 100644
index 000000000000..2f1b01d67106
--- /dev/null
+++ b/docs/archived/4.0/docs/Community.md
@@ -0,0 +1,7 @@
+# Community
+
+Sealos is an open source project that is driven by the participation of users and contributors. If you have questions or suggestions about using the product while reading this documentation, please try the following ways to seek support. Our team and community will do their best to help you.
+
++ 💬 Join our [Discord server](https://discord.gg/qzBmGGZGk7) is to chat with Sealos developers and other Sealos users. This is a good place to learn about Sealos and Kubernetes, ask questions, and share your experiences.
++ 🐦 Tweet at @Sailos_io on [Twitter](https://twitter.com/Sailos_io) and follow us.
++ 🐞 Create [GitHub Issues](https://github.com/labring/sealos/issues/new/choose) for bug reports and feature requests.
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/Intro.md b/docs/archived/4.0/docs/Intro.md
new file mode 100644
index 000000000000..3278cb7a7108
--- /dev/null
+++ b/docs/archived/4.0/docs/Intro.md
@@ -0,0 +1,53 @@
+import Highlight from '@site/src/components/Highlight'
+
+# What is Sealos?
+
+
+
+The vastness and complexity of the cloud-native ecosystem undoubtedly leaves many businesses in disarray, yearning for an efficient, ready-to-use distribution, which this ecosystem sorely lacks. The cloud-native market is in dire need of a cloud operating system to further reduce barriers and costs.
+
+The advent of Sealos opens a new window for enterprises. It provides a novel choice for businesses and developers alike, necessitating only the installation of a cloud operating system, regardless of public or private cloud use. It allows various applications to operate stably and securely, tackling all sorts of dependency issues they might require.
+
+Sealos adheres to the philosophy that **cloud usage can be as simple as Linux**. You can commence usage right after installation, as you would with Linux, without entanglement in excessive complexities. The sole difference is that while Linux is installed on a single server, Sealos is installed across an entire data center.
+
+In Sealos's architecture, **there's no essential difference between public and private clouds**. They should be separate instances of the same codebase, the only distinction lying in their configuration and installed applications. Installing it on an intranet renders it a private cloud, while offering services on the public internet converts it into a public cloud.
+
+We firmly believe that with the continuous development and advancement of Sealos, the future of cloud computing will be more open, more straightforward. The benefits of cloud computing should belong to all providers of computational power, and the value of the cloud should be shared by all participants of cloud computing. **They will be able to enjoy the various conveniences of cloud computing in a more economical fashion in a diversified market environment.**
+
+## Overview
+
+Sealos is a **cloud operating system distribution** based on the Kubernetes kernel. It adopts a cloud-native approach, discarding traditional cloud computing architecture, and shifting towards a new architecture with Kubernetes as the cloud kernel. This allows enterprises to use the cloud **as effortlessly as they would use a personal computer**.
+
+Users will be able to install any high-availability distributed application on Kubernetes with the click of a button, similar to using a personal computer. It practically requires no professional delivery or operational costs. Simultaneously, utilizing unique cluster imaging capabilities, users can package any distributed application into an OCI image, freely combine a variety of distributed applications, and easily customize their desired cloud. The powerful and flexible app store function can meet the diverse needs of various users.
+
+
+
+## Scenarios & Advantages
+
+Sealos is a powerful business operating platform, providing flawless support for various applications such as Java, Go, Python, PHP, and more, unrestricted by the programming language. The platform offers a stable operating environment for applications and resolves backend dependency issues like databases, object storage, and messaging queues. Further, it can flexibly handle application configuration management, service discovery, public network exposure, and automatic scaling issues.
+
+### Public Cloud
+
+If your business requires operation within a public cloud environment, you can directly use the [public cloud services provided by Sealos](https://cloud.sealos.io).
+
+#### Capabilities of Sealos
+
+- 🚀 **Application Management**: Easy management and quick release of publicly accessible distributed applications in the app store.
+- 🗄️ **Database Management**: Create high-availability databases in seconds, offering support for MySQL, PostgreSQL, MongoDB, and Redis.
+- 🌥️ **Cloud Universality**: Equally effective in both public and private cloud, enabling a seamless transition of traditional applications to the cloud.
+
+#### Advantages of Sealos
+
+- 💰 **Efficient & Economical**: Pay solely for the containers you utilize; automatic scaling prevents resource squandering and substantially reduces costs.
+- 🌐 **High Universality & Ease of Use**: Concentrate on your core business activities without worrying about system complexities; negligible learning costs involved.
+- 🛡️ **Agility & Security**: The distinctive multi-tenancy sharing model ensures both effective resource segmentation and collaboration, all under a secure framework.
+
+### Private Cloud
+
+The Sealos cloud operating system is **100% open source**. You can find all source code on [GitHub](https://github.com/labring/sealos), which means all features of the **Sealos Public Cloud** can also be deployed in a private cloud environment to meet more diversified business needs.
+
+### Strong Application Delivery Capability
+
+- Sealos has excellent Kubernetes life cycle management capabilities, and you can freely customize the Kubernetes environment.
+- Sealos can package an entire cluster and deliver it to the customer environment with a single click. All services and businesses can be delivered as a whole.
+- Unlike Docker, which mainly focuses on single-machine imaging, Sealos goes further, packaging the **entire cluster** or a distributed application.
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/advanced-guide/Architecture/Architecture.md b/docs/archived/4.0/docs/advanced-guide/Architecture/Architecture.md
new file mode 100644
index 000000000000..afd8f35841bf
--- /dev/null
+++ b/docs/archived/4.0/docs/advanced-guide/Architecture/Architecture.md
@@ -0,0 +1,23 @@
+# Architecture Description
+
+Sealos is built using the approach of application separation and application interconnection, where different applications can be deployed independently or combined with other applications to provide services.
+
+All services are authenticated using `kubeconfig` as the application identity, ensuring a consistent user experience whether in a browser, using the `sealos` command line locally, or even with a third-party client.
+
+## Overall Architecture
+
+
+
+## Application Architecture
+
+Applications on Sealos adopt a front-end and back-end separation architecture, and the front-end can provide `SSR` capabilities, allowing applications to provide services independently to the outside world without being bound to the Sealos monolith.
+
+Using the following diagram as an example:
+
+**App Launchpad** is a application deployment tool provided by Sealos, and **Terminal** is a terminal application for the Sealos system, providing command-line services consistent like a single-machine operating system terminal.
+For various applications on Sealos, they can interact directly with `Kubernetes` services, or interact with various CRD controllers such as Terminal Controller and Ingress-Nginx Controller, etc.
+Applications can also call each other, for example, a database created by a user in the **Database** application can directly invoke the **Terminal** application and automatically connect to the database for debugging.
+
+For more information, please refer to the [link](../../guides) documentation.
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/advanced-guide/Architecture/images/application_dark.png b/docs/archived/4.0/docs/advanced-guide/Architecture/images/application_dark.png
new file mode 100644
index 000000000000..755a293e3468
Binary files /dev/null and b/docs/archived/4.0/docs/advanced-guide/Architecture/images/application_dark.png differ
diff --git a/docs/archived/4.0/docs/advanced-guide/Architecture/images/application_light.png b/docs/archived/4.0/docs/advanced-guide/Architecture/images/application_light.png
new file mode 100644
index 000000000000..b735ab6cd70f
Binary files /dev/null and b/docs/archived/4.0/docs/advanced-guide/Architecture/images/application_light.png differ
diff --git a/docs/archived/4.0/docs/advanced-guide/Architecture/images/architecture_dark.png b/docs/archived/4.0/docs/advanced-guide/Architecture/images/architecture_dark.png
new file mode 100644
index 000000000000..8a007f9727ca
Binary files /dev/null and b/docs/archived/4.0/docs/advanced-guide/Architecture/images/architecture_dark.png differ
diff --git a/docs/archived/4.0/docs/advanced-guide/Architecture/images/architecture_light.png b/docs/archived/4.0/docs/advanced-guide/Architecture/images/architecture_light.png
new file mode 100644
index 000000000000..1607eabd53b6
Binary files /dev/null and b/docs/archived/4.0/docs/advanced-guide/Architecture/images/architecture_light.png differ
diff --git a/docs/archived/4.0/docs/advanced-guide/how-to-deploy-the-application-to-desktop.md b/docs/archived/4.0/docs/advanced-guide/how-to-deploy-the-application-to-desktop.md
new file mode 100644
index 000000000000..e4934c8f05fb
--- /dev/null
+++ b/docs/archived/4.0/docs/advanced-guide/how-to-deploy-the-application-to-desktop.md
@@ -0,0 +1,39 @@
+# How to deploy the application to desktop
+
+## Use Terminal to write app.yaml
+
+### Key Information
+
+- type: iframe, identified as a web application
+- spec name is the name displayed on the desktop
+- url application address
+- icon is the icon displayed on the desktop
+
+```yaml
+apiVersion: app.sealos.io/v1
+kind: App
+metadata:
+ name: app-sample
+spec:
+ name: Image Hub Demo
+ icon:
+ type: iframe
+ data:
+ url: https://hub.sealos.io/
+ desc:
+ icon: https://cloud.sealos.io/logo.svg
+ menuData:
+ displayType: normal
+```
+
+
+
+### apply yaml
+
+```
+kubectl apply -f app.yaml
+```
+
+
+
+### Refresh the browser, you can see the app on the desktop
diff --git a/docs/archived/4.0/docs/advanced-guide/images/app-apply-command.png b/docs/archived/4.0/docs/advanced-guide/images/app-apply-command.png
new file mode 100644
index 000000000000..f8a5fe3e636f
Binary files /dev/null and b/docs/archived/4.0/docs/advanced-guide/images/app-apply-command.png differ
diff --git a/docs/archived/4.0/docs/advanced-guide/images/app-yaml.png b/docs/archived/4.0/docs/advanced-guide/images/app-yaml.png
new file mode 100644
index 000000000000..54ac66a7bfe1
Binary files /dev/null and b/docs/archived/4.0/docs/advanced-guide/images/app-yaml.png differ
diff --git a/docs/archived/4.0/docs/examples/ai-applications/imgs/sealos1.png b/docs/archived/4.0/docs/examples/ai-applications/imgs/sealos1.png
new file mode 100644
index 000000000000..7313d36f5487
Binary files /dev/null and b/docs/archived/4.0/docs/examples/ai-applications/imgs/sealos1.png differ
diff --git a/docs/archived/4.0/docs/examples/ai-applications/imgs/sealos2.png b/docs/archived/4.0/docs/examples/ai-applications/imgs/sealos2.png
new file mode 100644
index 000000000000..32f0fb67f319
Binary files /dev/null and b/docs/archived/4.0/docs/examples/ai-applications/imgs/sealos2.png differ
diff --git a/docs/archived/4.0/docs/examples/ai-applications/install-fastgpt-on-desktop.md b/docs/archived/4.0/docs/examples/ai-applications/install-fastgpt-on-desktop.md
new file mode 100644
index 000000000000..c9557b00a946
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/ai-applications/install-fastgpt-on-desktop.md
@@ -0,0 +1,19 @@
+---
+sidebar_position: 1
+---
+
+# Quick Installation of FastGpt AI Knowledge Base
+
+No server, no magic, no domain name, click to deploy 👇
+
+[](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dfastgpt)
+
+The database needs to be deployed. After the deployment, you need to wait 2 to 4 minutes to access the database. The minimum configuration is used by default, and the first access is a little slow.
+
+
+
+## Run
+
+Click on the [external address] provided by sealos to use it. The login user name is root, and the password is the newly set environment variable (1234 in the figure above)
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/examples/baas/install-fireboom.md b/docs/archived/4.0/docs/examples/baas/install-fireboom.md
new file mode 100644
index 000000000000..4b9708a73a73
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/baas/install-fireboom.md
@@ -0,0 +1,103 @@
+# Quick Installation of Fireboom
+
+[Fireboom](https://fireboom.io) is a visual API development platform designed for developers, providing a declarative development framework, a visual editing suite, and a hook mechanism.
+
+
+Official website: https://fireboom.io
+
+**Features**
+
+- Minute-level delivery: Reduce the interface development time from 2 days to 2 minutes in traditional mode.
+
+- Front-end to full-stack: Visual development significantly reduces the threshold for interface development, allowing front-end developers to easily become full-stack developers.
+
+- Back-end without bricklaying: Declarative programming avoids copy and paste, reduces the probability of bugs, and allows back-end developers to focus on business development.
+
+## Step 1: Create a New Application
+
+- Open the "Application Management" application in the [Sealos](https://cloud.sealos.io) desktop environment.
+
+
+
+- In "Application Management", click "Create New Application" to create a new application.
+
+
+## Step 2: Application Deployment
+
+- Basic Configuration:
+
+ - Application Name (custom): fireboom
+
+ - Image Name (default latest version): fireboomapi/fireboom_server:latest
+
+ - CPU (recommended): 2 Core (minimum 1 Core)
+
+ - Memory (recommended): 4 G (minimum 512 M)
+
+- Deployment Mode:
+
+ - Number of Instances (custom): 1
+
+
+
+- Network Configuration:
+
+ - Control Panel: 9123
+ - API Endpoint: 9991, enable external access
+
+
+
+- Advanced Configuration:
+
+ - Command Settings:
+ - Parameter Settings: dev (dev for development mode, start for production mode)
+
+ - Persistent Storage:
+
+ - Metadata: /fbserver/store
+ - Uploaded Files: /fbserver/upload
+
+
+
+
+## Step 3: Deploy the Application
+
+1. Click "Deploy Application" to start deploying the application.
+
+
+
+2. Click "Details" to check. When the STATUS of the application changes from Pending to Running, it means that the application has been successfully launched.
+
+
+
+## Step 4: Access the Application
+
+- Access the Control Panel: Domain corresponding to 9123
+
+
+
+- Access the API Endpoint: Domain corresponding to 9991
+
+```
+status ok
+```
+
+## Step 5: Configure Fireboom (optional)
+
+After setting, you can easily use the Swagger document~
+
+1. View the public address of the API: Settings -> System
+
+
+
+2. Modify it to the public domain of the API endpoint, corresponding to 9991
+
+ - Static Value: Select static value and set it to the public domain of the **API Endpoint**
+ - Environment Variable: Go to Environment Variables, find FB_API_PUBLIC_URL and set it to the public domain of the **API Endpoint**
+
+
+
+
+3. Use the Swagger document to test the API
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/examples/blog-platform/install-halo.md b/docs/archived/4.0/docs/examples/blog-platform/install-halo.md
new file mode 100644
index 000000000000..1f81e39bc787
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/blog-platform/install-halo.md
@@ -0,0 +1,156 @@
+---
+sidebar_position: 2
+---
+
+# Quick Installation of Halo Blogging Platform
+
+Deploy Halo Blogging Platform swiftly on Sealos
+
+
+
+[Halo](https://github.com/halo-dev/halo) is a powerful and easy-to-use open-source website building tool.
+This article will introduce how to deploy Halo on Sealos, while deploying PostgreSQL on Sealos can help you easily manage and maintain the database to meet different business needs.
+
+## Step 1: Deploy PostgreSQL on Sealos
+
+### First, open [Sealos](https://cloud.sealos.io) and click on "More Apps" to enter the PostgreSQL deployment interface:
+
+
+
+### Fill in the configuration
+
+In the PostgreSQL deployment interface, click on "Create Cluster" and fill in the relevant configuration information as prompted.
+
+
+
+After entering the corresponding configuration, click on "Create Cluster":
+
+
+
+### Check the database configuration:
+
+
+
+Save the generated configuration containing the database username, password, and connection method for use in the following Halo configuration:
+
+
+
+> The PostgreSQL configuration can be seen in the image as follows:
+>
+> Username: root
+>
+> Password: jxidRwmY82eeuFa01tHN28msb86woounM0QMbyl1jhwzKxT9IDqlNkFfyy4R34G3
+>
+> pg DNS name: acid-halo-pg.ns-sy32q9p9.svc.cluster.local:5432
+
+## Step 2: Deploy Halo on Sealos
+
+### Open App Launchpad
+
+
+
+
+
+### Fill in the configuration
+
+- Customize the application name
+
+- Set the image name to `halohub/halo:2.5`
+
+- CPU and storage should be configured according to actual conditions, memory requires 500Mi to start Halo, you can refer to the official [prepare](https://docs.halo.run/getting-started/prepare)
+
+- Expose port should be 8090. At the same time, setting it to external network access will automatically configure an exit domain for the application for external network access, and you also need to configure this address in Halo
+
+- Environment variables include database configuration and other related configurations. It is important to note that you need to configure the database connection information based on the username, password, and DNS you set in Step 1. Additionally, you need to configure the username and password for the super administrator of Halo.
+
+Configure Halo environment variables as follows:
+
+```Bash
+spring.sql.init.platform=postgresql
+spring.r2dbc.url=r2dbc:pool:postgresql://acid-halo-pg.ns-sy32q9p9.svc.cluster.local:5432/halo
+spring.r2dbc.username=root
+spring.r2dbc.password=jxidRwmY82eeuFa01tHN28msb86woounM0QMbyl1jhwzKxT9IDqlNkFfyy4R34G3
+halo.external-url=tmtdvfjiyyfh.cloud.sealos.io
+halo.security.initializer.superadminusername=root
+halo.security.initializer.superadminpassword=sealos
+```
+
+Detailed explanation of environment variable configuration:
+
+| Parameter Name | Description |
+|------------------------------------------------|----------------------------------------------------------------------------------------------------------|
+| spring.r2dbc.url | Database connection address, see Database Link Format below for details |
+| spring.r2dbc.username | Database username |
+| spring.r2dbc.password | Database password |
+| spring.sql.init.platform | Database platform name, supports postgresql, mysql, h2 |
+| halo.external-url | External access link, if you need public network access, you need to configure the actual access address |
+| halo.security.initializer.superadminusername | Initial super administrator username |
+| halo.security.initializer.superadminpassword | Initial super administrator password |
+
+Database link format (here we use the postgresql format):
+
+| Connection Method | Connection Address Format | spring.sql.init.platform |
+|-------------------|------------------------------------------------------------------------------------|--------------------------|
+| PostgreSQL | r2dbc:pool:postgresql://`{HOST}`:`{PORT}`/`{DATABASE}` | postgresql |
+| MySQL | r2dbc:pool:mysql://`{HOST}`:`{PORT}`/`{DATABASE}` | mysql |
+| MariaDB | r2dbc:pool:mariadb://`{HOST}`:`{PORT}`/`{DATABASE}` | mysql |
+| H2 Database | r2dbc:h2:file:///`${halo.work-dir}`/db/halo-next?MODE=MySQL&DB_CLOSE_ON_EXIT=FALSE | h2 |
+
+### Network configuration
+
+
+
+### Environment variable configuration
+
+
+
+### Configure Persistent Storage Volume
+
+Mount `/root/.halo2` directory to persist Halo data:
+
+
+
+## Step 3: Access Halo via the Public Network
+
+After successfully launching the application, you can access Halo through the public network address for configuration:
+
+
+
+
+
+### **Initialize Halo**
+
+
+
+
+
+### **First Halo:**
+
+
+
+
+
+
+
+## FAQ
+
+### Forgot Password
+
+#### Enter the App Launchpad and click on "Logs":
+
+
+
+
+
+You can see the following information in the logs: Username: `admin` Password: `QTu2J2xgUQ3ngqUo`
+
+#### Alternatively, you can view the logs by executing the following command in the terminal:
+
+
+
+```
+root@td3q8uc46:~# kubectl logs halo-0 | grep 'Generated random password:' | tail -1
+2023-05-30T13:32:16.942+08:00 INFO 7 --- [-controller-t-1] r.h.app.security.SuperAdminInitializer : === Generated random password: QTu2J2xgUQ3ngqUo for super administrator: admin ===
+```
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/examples/blog-platform/install-wordpress.md b/docs/archived/4.0/docs/examples/blog-platform/install-wordpress.md
new file mode 100644
index 000000000000..13fbdcd12120
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/blog-platform/install-wordpress.md
@@ -0,0 +1,103 @@
+---
+sidebar_position: 1
+---
+
+# Quick Installation of WordPress
+
+[WordPress](https://github.com/WordPress/WordPress) is a powerful, user-friendly and open source content management system (CMS) that can be used to publish websites, news, blogs and other content online. It provides convenient methods for content editing, publishing and updating, and can also be highly customized through themes and plugins. WordPress has become a very successful and popular CMS system, with over 30 million websites built on WordPress worldwide.
+
+> By deploying **WordPress** with **Sealos**, you can greatly reduce deployment time and start users' content publishing journey at lightning speed.
+
+WordPress requires a MySQL database to be installed and used together. As a best practice for using Sealos, it is recommended to deploy the MySQL database independently using a database application, and quickly install WordPress based on the independent database.
+
+
+## Create MySQL instance
+
+Open the **Database** application in the [Sealos](https://cloud.sealos.io) desktop environment, quickly set up the required database instance and wait for the database deployment to be completed.
+
+
+
+
+
+
+
+## Create a New Database
+
+When installing a MySQL instance through the **Database** application, user-defined databases are not automatically created. When connecting an application to the MySQL database, you need to manually create the corresponding database in the MySQL instance.
+
+You can access the database terminal command line in the following ways:
+
+
+
+
+Alternatively, you can access the database terminal command line using the following methods:
+
+`mysql -u username -p -h yourhost`
+
+
+
+After successfully connecting to the database, you can create a new database using the following command:
+
+`CREATE DATABASE yourdb;`
+
+At this point, you have manually created a user-defined database in the MySQL instance.
+
+## App Launchpad
+
+Click on App Launchpad to enter the installation interface.
+
+
+
+## Basic Configuration
+
+Begin by setting a custom application name and selecting the image to install. Sealos supports both public and private image sources. In this example, the official image source is used, and if no image version is specified, the latest version will be pulled by default.
+
+Next, configure CPU and memory resources for WordPress. WordPress is very lightweight, and a small amount of resources is sufficient to ensure the normal operation of the program.
+
+
+
+## NetWork Configuration
+
+For network configuration, enable external access, and Sealos will automatically assign an exit domain name for users to access from the internet. If the user has already registered their custom domain, they can create a CNAME record pointing to the assigned exit domain name, allowing the custom domain to be used to access the installed application.
+
+Note: In subsequent demonstrations, a randomly generated exit domain name by Sealos will be used.
+
+
+
+
+## Advanced Configuration
+
+In the advanced configuration, you need to configure the environment variables for WordPress based on the actual database situation.
+
+In the advanced configuration, you need to set environment variables and local storage for WordPress.
+
+Common WordPress environment variables are shown below:
+
+```Plain
+WORDPRESS_DB_HOST IP address of the database instance
+WORDPRESS_DB_USER Database user for connection
+WORDPRESS_DB_PASSWORD User password required for connecting to the database
+WORDPRESS_DB_NAME Name of the connected database
+```
+
+Based on the previously installed MySQL example, you can configure the environment variables as follows:
+
+```Plain
+WORDPRESS_DB_HOST Database IP Address
+WORDPRESS_DB_USER Database username
+WORDPRESS_DB_PASSWORD Database password
+```
+
+The storage volume mount point for the official WordPress image is: /var/www/html. Users can allocate storage space for the application according to their needs.
+
+With this, WordPress is also successfully installed. Click on the details, and you can successfully access it through the external network address.
+
+
+
+Click on the link, and the following page shows that the installation was successful:
+
+
+
+**Note: If the deployment fails, first check whether the user-defined database that WordPress is trying to access exists in the MySQL instance.**
+
+
diff --git a/docs/archived/4.0/docs/examples/dial-testing-system/install-uptime-kuma.md b/docs/archived/4.0/docs/examples/dial-testing-system/install-uptime-kuma.md
new file mode 100644
index 000000000000..2fbc0e449e8b
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/dial-testing-system/install-uptime-kuma.md
@@ -0,0 +1,62 @@
+---
+sidebar_position: 1
+---
+
+# Quick Installation of Uptime Kuma
+
+
+
+
+
+[Uptime Kuma](https://github.com/louislam/uptime-kuma) is an open-source and easy-to-use server monitoring tool. It helps you monitor the real-time status, response time, and other key metrics of your server to ensure that it always remains in optimal condition. If you want to quickly install Uptime Kuma, follow these steps:
+
+### Step 1: First, enter Sealos and open the App Launchpad
+
+
+
+### Step 2: Create a New Application
+
+Open the App Launchpad in the [Sealos](https://cloud.sealos.io) desktop environment, click "New Application" to create a new application.
+
+
+
+### Step 3: Set Startup Parameters
+
+Configure the startup parameters as follows:
+
+- Set the container exposed port to 3001
+- Enable public network access to access the application via the provided domain
+
+
+
+
+
+### Step 4: Set up Storage Volumes
+
+In the advanced settings, add a storage volume mount to persist the Uptime Kuma data directory `/app/data`. This ensures that the application data is not lost when the container restarts.
+
+
+
+### Step 5: Deploy the Application
+
+Click 「Deploy Application」 to start the application:
+
+
+
+### Step 6: Access the Application
+
+Once the application is successfully launched, you can access it using its public network address. Enter the application domain in your browser to access the main interface.
+
+
+
+### Step 7: Create an Administrator Account
+
+To ensure the security of Uptime Kuma, you will need to create an administrator account when you first access the application. Fill in the basic information for the administrator account, including the username and password. After completing the form, click the "Create" button to register the account.
+
+
+
+### Step 8: Add Custom Monitoring Items
+
+
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/examples/gitea/install-gitea.md b/docs/archived/4.0/docs/examples/gitea/install-gitea.md
new file mode 100644
index 000000000000..beae8c0a2ee1
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/gitea/install-gitea.md
@@ -0,0 +1,112 @@
+# Quick Installation of Gitea
+
+Git with a cup of tea! Painless self-hosted all-in-one software development service, includes Git hosting, code review, team collaboration, package registry and CI/CD.
+
+## Step 1: Log in to Sealos
+
+- Go to the [Sealos](https://cloud.sealos.io/) official website
+
+
+
+## Step 2: Open the App Launchpad
+
+
+
+## Step 3: Create a new application
+
+- In App Launchpad, click "Create New Application" to create a new application.
+
+
+
+## Step 4: Application deployment
+
+- Basic configuration:
+
+ - Application name (custom): gitea
+ - Image name (default latest version): gitea/gitea:latest-rootless
+ - CPU (recommended): 1 Core
+ - Memory (recommended): 1 G
+
+- Deployment mode:
+
+ - Number of instances (custom): 1
+
+
+
+- Network configuration:
+
+ - Container port: 3000
+ - Accessible to the Public: enabled
+
+
+
+- Advanced configuration:
+
+ - Custom local storage, persist Gitea data (recommended 1 G).
+
+
+
+
+## Step 5: Deploy the application
+
+- Click「Deploy Application」 to start deploying the application.
+
+
+
+## Step 6: Configure the database
+
+- Configure MySQL database for Gitea via Sealos
+- Click Database
+
+
+
+- Create a new database
+
+
+
+- Basic configuration:
+
+ - Cluster type: mysql
+ - Database version: ac-mysql-8.0.30
+ - CPU (recommended): 1 Core
+ - Memory (recommended): 1 G
+
+
+
+
+- After deployment is successful, enter the details page to view the MySQL connection information
+
+
+
+- Click "One-click Connection" to enter the MySQL terminal connection
+
+
+
+- Execute
+
+```sql
+CREATE DATABASE giteadb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';
+```
+
+Create the database needed for Gitea
+
+
+
+## Step 7: Access the application
+
+- Click 「App Launchpad」 to view.When the application's STATUS changes from Pending to Running, it means the application has started successfully.
+
+- When the STATUS is Running, you can directly access the external address.
+
+
+
+- After accessing the external address, enter the configuration page and configure according to the details of the MySQL just created
+
+
+
+- After configuring, click Install, wait for a while and you can enter the login page, deployment successful!
+
+
+
+
+
diff --git a/docs/archived/4.0/docs/examples/images/2022-04-10-10-52-H2ZWUZ.png b/docs/archived/4.0/docs/examples/images/2022-04-10-10-52-H2ZWUZ.png
new file mode 100644
index 000000000000..025a0c4e45a9
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2022-04-10-10-52-H2ZWUZ.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2022-04-10-14-31-vrNHJU.png b/docs/archived/4.0/docs/examples/images/2022-04-10-14-31-vrNHJU.png
new file mode 100644
index 000000000000..1ca1cd0ed53a
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2022-04-10-14-31-vrNHJU.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2022-04-10-14-32-1iudM0.png b/docs/archived/4.0/docs/examples/images/2022-04-10-14-32-1iudM0.png
new file mode 100644
index 000000000000..5ba9b3ad8784
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2022-04-10-14-32-1iudM0.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2022-04-10-14-32-ADfk8T.png b/docs/archived/4.0/docs/examples/images/2022-04-10-14-32-ADfk8T.png
new file mode 100644
index 000000000000..ac8de099a1ae
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2022-04-10-14-32-ADfk8T.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-11-54-EIVahX.jpg b/docs/archived/4.0/docs/examples/images/2023-06-26-11-54-EIVahX.jpg
new file mode 100644
index 000000000000..1b1fe554ea55
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-11-54-EIVahX.jpg differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-11-55-NDkuEg.jpg b/docs/archived/4.0/docs/examples/images/2023-06-26-11-55-NDkuEg.jpg
new file mode 100644
index 000000000000..a773666c4ff2
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-11-55-NDkuEg.jpg differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-11-59-FxJE12.png b/docs/archived/4.0/docs/examples/images/2023-06-26-11-59-FxJE12.png
new file mode 100644
index 000000000000..fdc343a80edc
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-11-59-FxJE12.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-01-DKect7.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-01-DKect7.png
new file mode 100644
index 000000000000..b4d2db4792a7
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-01-DKect7.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-05-CWczxm.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-05-CWczxm.png
new file mode 100644
index 000000000000..d512ad14195a
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-05-CWczxm.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-06-lvv6ms.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-06-lvv6ms.png
new file mode 100644
index 000000000000..28a241ade20c
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-06-lvv6ms.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-07-s8W7iu.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-07-s8W7iu.png
new file mode 100644
index 000000000000..aa8e415fb5b7
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-07-s8W7iu.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-09-RslDGj.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-09-RslDGj.png
new file mode 100644
index 000000000000..0935a645d999
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-09-RslDGj.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-24-QHYKZt.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-24-QHYKZt.png
new file mode 100644
index 000000000000..a446015a5c59
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-24-QHYKZt.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-26-HYOaBJ.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-26-HYOaBJ.png
new file mode 100644
index 000000000000..6996e718eb53
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-26-HYOaBJ.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-28-ccnUOj.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-28-ccnUOj.png
new file mode 100644
index 000000000000..c76f2e0535ae
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-28-ccnUOj.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-29-z5E9gi.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-29-z5E9gi.png
new file mode 100644
index 000000000000..ba4d166331c6
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-29-z5E9gi.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-39-1jsF0t.jpeg b/docs/archived/4.0/docs/examples/images/2023-06-26-12-39-1jsF0t.jpeg
new file mode 100644
index 000000000000..7ca839b52b2f
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-39-1jsF0t.jpeg differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-FwsbfW.png b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-FwsbfW.png
new file mode 100644
index 000000000000..ac7134504f86
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-FwsbfW.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-Vs9ccy.png b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-Vs9ccy.png
new file mode 100644
index 000000000000..59460d087f22
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-Vs9ccy.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-YFHPYc.png b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-YFHPYc.png
new file mode 100644
index 000000000000..0fc1e2d152b9
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-YFHPYc.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-hdHfxP.png b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-hdHfxP.png
new file mode 100644
index 000000000000..00e7aa10391a
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-hdHfxP.png differ
diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-nwrxrv.png b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-nwrxrv.png
new file mode 100644
index 000000000000..08ff96c0c126
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-nwrxrv.png differ
diff --git a/docs/archived/4.0/docs/examples/images/appsmith-1.png b/docs/archived/4.0/docs/examples/images/appsmith-1.png
new file mode 100644
index 000000000000..700c4e0d4d30
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-1.png differ
diff --git a/docs/archived/4.0/docs/examples/images/appsmith-2.png b/docs/archived/4.0/docs/examples/images/appsmith-2.png
new file mode 100644
index 000000000000..bca16d416695
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-2.png differ
diff --git a/docs/archived/4.0/docs/examples/images/appsmith-3.png b/docs/archived/4.0/docs/examples/images/appsmith-3.png
new file mode 100644
index 000000000000..01876eeff7a0
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-3.png differ
diff --git a/docs/archived/4.0/docs/examples/images/appsmith-4.png b/docs/archived/4.0/docs/examples/images/appsmith-4.png
new file mode 100644
index 000000000000..fad0d90a71df
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-4.png differ
diff --git a/docs/archived/4.0/docs/examples/images/appsmith-5.png b/docs/archived/4.0/docs/examples/images/appsmith-5.png
new file mode 100644
index 000000000000..d6567da245ae
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-5.png differ
diff --git a/docs/archived/4.0/docs/examples/images/appsmith-6.png b/docs/archived/4.0/docs/examples/images/appsmith-6.png
new file mode 100644
index 000000000000..78b505e75298
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-6.png differ
diff --git a/docs/archived/4.0/docs/examples/images/appsmith-7.png b/docs/archived/4.0/docs/examples/images/appsmith-7.png
new file mode 100644
index 000000000000..9771645376a2
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-7.png differ
diff --git a/docs/archived/4.0/docs/examples/images/appsmith-8.png b/docs/archived/4.0/docs/examples/images/appsmith-8.png
new file mode 100644
index 000000000000..ca7f7e62e60c
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-8.png differ
diff --git a/docs/archived/4.0/docs/examples/images/fireboom/1-1.png b/docs/archived/4.0/docs/examples/images/fireboom/1-1.png
new file mode 100644
index 000000000000..2e8a8d45325b
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/1-1.png differ
diff --git a/docs/archived/4.0/docs/examples/images/fireboom/2-1.png b/docs/archived/4.0/docs/examples/images/fireboom/2-1.png
new file mode 100644
index 000000000000..5fb4c0ec51f5
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/2-1.png differ
diff --git a/docs/archived/4.0/docs/examples/images/fireboom/2-2.png b/docs/archived/4.0/docs/examples/images/fireboom/2-2.png
new file mode 100644
index 000000000000..714ad3891845
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/2-2.png differ
diff --git a/docs/archived/4.0/docs/examples/images/fireboom/2-3.png b/docs/archived/4.0/docs/examples/images/fireboom/2-3.png
new file mode 100644
index 000000000000..e0d5a1a605d4
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/2-3.png differ
diff --git a/docs/archived/4.0/docs/examples/images/fireboom/3-1.png b/docs/archived/4.0/docs/examples/images/fireboom/3-1.png
new file mode 100644
index 000000000000..10d9ef1b793a
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/3-1.png differ
diff --git a/docs/archived/4.0/docs/examples/images/fireboom/3-2.png b/docs/archived/4.0/docs/examples/images/fireboom/3-2.png
new file mode 100644
index 000000000000..eef62cdfc5f9
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/3-2.png differ
diff --git a/docs/archived/4.0/docs/examples/images/fireboom/4-1.png b/docs/archived/4.0/docs/examples/images/fireboom/4-1.png
new file mode 100644
index 000000000000..f717abd1abf4
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/4-1.png differ
diff --git a/docs/archived/4.0/docs/examples/images/fireboom/5-1.png b/docs/archived/4.0/docs/examples/images/fireboom/5-1.png
new file mode 100644
index 000000000000..477f9ebd26ef
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/5-1.png differ
diff --git a/docs/archived/4.0/docs/examples/images/fireboom/5-2.png b/docs/archived/4.0/docs/examples/images/fireboom/5-2.png
new file mode 100644
index 000000000000..e353d414d31b
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/5-2.png differ
diff --git a/docs/archived/4.0/docs/examples/images/fireboom/5-3.png b/docs/archived/4.0/docs/examples/images/fireboom/5-3.png
new file mode 100644
index 000000000000..d1bddbf12c65
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/5-3.png differ
diff --git a/docs/archived/4.0/docs/examples/images/fireboom/fireboom-architecture.jpeg b/docs/archived/4.0/docs/examples/images/fireboom/fireboom-architecture.jpeg
new file mode 100644
index 000000000000..7f9660c847fa
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/fireboom-architecture.jpeg differ
diff --git a/docs/archived/4.0/docs/examples/images/fireboom/fireboom-intro.jpeg b/docs/archived/4.0/docs/examples/images/fireboom/fireboom-intro.jpeg
new file mode 100644
index 000000000000..848bd2b5d97d
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/fireboom-intro.jpeg differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-0.png b/docs/archived/4.0/docs/examples/images/gitea-0.png
new file mode 100644
index 000000000000..946d921d4ce3
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-0.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-1.png b/docs/archived/4.0/docs/examples/images/gitea-1.png
new file mode 100644
index 000000000000..cae3923a16fb
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-1.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-10.png b/docs/archived/4.0/docs/examples/images/gitea-10.png
new file mode 100644
index 000000000000..0087724d0b14
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-10.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-11.png b/docs/archived/4.0/docs/examples/images/gitea-11.png
new file mode 100644
index 000000000000..b820dc0b7817
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-11.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-12.png b/docs/archived/4.0/docs/examples/images/gitea-12.png
new file mode 100644
index 000000000000..31c2451f5e33
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-12.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-13.png b/docs/archived/4.0/docs/examples/images/gitea-13.png
new file mode 100644
index 000000000000..5557c68da9c4
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-13.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-14.png b/docs/archived/4.0/docs/examples/images/gitea-14.png
new file mode 100644
index 000000000000..566c44f970bd
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-14.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-15.png b/docs/archived/4.0/docs/examples/images/gitea-15.png
new file mode 100644
index 000000000000..90e280b8053a
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-15.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-16.png b/docs/archived/4.0/docs/examples/images/gitea-16.png
new file mode 100644
index 000000000000..901bcb8ac4ae
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-16.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-17.png b/docs/archived/4.0/docs/examples/images/gitea-17.png
new file mode 100644
index 000000000000..d0f6300f2494
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-17.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-2.png b/docs/archived/4.0/docs/examples/images/gitea-2.png
new file mode 100644
index 000000000000..295b148d1798
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-2.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-3.png b/docs/archived/4.0/docs/examples/images/gitea-3.png
new file mode 100644
index 000000000000..2d9a9a6554b5
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-3.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-4.png b/docs/archived/4.0/docs/examples/images/gitea-4.png
new file mode 100644
index 000000000000..9a7d30cba2e6
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-4.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-5.png b/docs/archived/4.0/docs/examples/images/gitea-5.png
new file mode 100644
index 000000000000..9fe9a5f7ac9d
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-5.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-6.png b/docs/archived/4.0/docs/examples/images/gitea-6.png
new file mode 100644
index 000000000000..e7db5c844514
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-6.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-7.png b/docs/archived/4.0/docs/examples/images/gitea-7.png
new file mode 100644
index 000000000000..6ced9251709d
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-7.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-8.png b/docs/archived/4.0/docs/examples/images/gitea-8.png
new file mode 100644
index 000000000000..9395b5fdd4de
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-8.png differ
diff --git a/docs/archived/4.0/docs/examples/images/gitea-9.png b/docs/archived/4.0/docs/examples/images/gitea-9.png
new file mode 100644
index 000000000000..9da2f0456b52
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-9.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-1.png b/docs/archived/4.0/docs/examples/images/halo_img-1.png
new file mode 100644
index 000000000000..ccc68e469f5e
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-1.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-10.png b/docs/archived/4.0/docs/examples/images/halo_img-10.png
new file mode 100644
index 000000000000..e1137ca41541
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-10.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-11.png b/docs/archived/4.0/docs/examples/images/halo_img-11.png
new file mode 100644
index 000000000000..707d5e9c313f
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-11.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-12.png b/docs/archived/4.0/docs/examples/images/halo_img-12.png
new file mode 100644
index 000000000000..a62bad32ef89
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-12.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-13.png b/docs/archived/4.0/docs/examples/images/halo_img-13.png
new file mode 100644
index 000000000000..d270bab0a421
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-13.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-14.png b/docs/archived/4.0/docs/examples/images/halo_img-14.png
new file mode 100644
index 000000000000..2865276b96df
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-14.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-15.png b/docs/archived/4.0/docs/examples/images/halo_img-15.png
new file mode 100644
index 000000000000..c9afff34a93e
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-15.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-16.png b/docs/archived/4.0/docs/examples/images/halo_img-16.png
new file mode 100644
index 000000000000..8d843a1f0e7f
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-16.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-17.png b/docs/archived/4.0/docs/examples/images/halo_img-17.png
new file mode 100644
index 000000000000..6e7d3f8e8585
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-17.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-18.png b/docs/archived/4.0/docs/examples/images/halo_img-18.png
new file mode 100644
index 000000000000..a3d0fcb8dd93
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-18.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-19.png b/docs/archived/4.0/docs/examples/images/halo_img-19.png
new file mode 100644
index 000000000000..5254c15d9678
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-19.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-2.png b/docs/archived/4.0/docs/examples/images/halo_img-2.png
new file mode 100644
index 000000000000..5a5cfc3f2684
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-2.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-20.png b/docs/archived/4.0/docs/examples/images/halo_img-20.png
new file mode 100644
index 000000000000..d20e5101e4b4
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-20.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-21.png b/docs/archived/4.0/docs/examples/images/halo_img-21.png
new file mode 100644
index 000000000000..42dcb2303a18
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-21.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-22.png b/docs/archived/4.0/docs/examples/images/halo_img-22.png
new file mode 100644
index 000000000000..e560a5aec5b7
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-22.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-3.png b/docs/archived/4.0/docs/examples/images/halo_img-3.png
new file mode 100644
index 000000000000..3a23c04cef78
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-3.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-4.png b/docs/archived/4.0/docs/examples/images/halo_img-4.png
new file mode 100644
index 000000000000..13665f4535d1
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-4.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-5.png b/docs/archived/4.0/docs/examples/images/halo_img-5.png
new file mode 100644
index 000000000000..200db6367fde
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-5.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-6.png b/docs/archived/4.0/docs/examples/images/halo_img-6.png
new file mode 100644
index 000000000000..19ce13c9b30b
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-6.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-7.png b/docs/archived/4.0/docs/examples/images/halo_img-7.png
new file mode 100644
index 000000000000..d450ab20f873
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-7.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-8.png b/docs/archived/4.0/docs/examples/images/halo_img-8.png
new file mode 100644
index 000000000000..5d857a1b40e7
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-8.png differ
diff --git a/docs/archived/4.0/docs/examples/images/halo_img-9.png b/docs/archived/4.0/docs/examples/images/halo_img-9.png
new file mode 100644
index 000000000000..4e32d5475cf3
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-9.png differ
diff --git a/docs/archived/4.0/docs/examples/images/pageplug-1.png b/docs/archived/4.0/docs/examples/images/pageplug-1.png
new file mode 100644
index 000000000000..700c4e0d4d30
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-1.png differ
diff --git a/docs/archived/4.0/docs/examples/images/pageplug-10.png b/docs/archived/4.0/docs/examples/images/pageplug-10.png
new file mode 100644
index 000000000000..0aa0f2546ce3
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-10.png differ
diff --git a/docs/archived/4.0/docs/examples/images/pageplug-11.png b/docs/archived/4.0/docs/examples/images/pageplug-11.png
new file mode 100644
index 000000000000..ef702aadb7e3
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-11.png differ
diff --git a/docs/archived/4.0/docs/examples/images/pageplug-2.png b/docs/archived/4.0/docs/examples/images/pageplug-2.png
new file mode 100644
index 000000000000..bca16d416695
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-2.png differ
diff --git a/docs/archived/4.0/docs/examples/images/pageplug-3.png b/docs/archived/4.0/docs/examples/images/pageplug-3.png
new file mode 100644
index 000000000000..00edfb9382b2
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-3.png differ
diff --git a/docs/archived/4.0/docs/examples/images/pageplug-4.png b/docs/archived/4.0/docs/examples/images/pageplug-4.png
new file mode 100644
index 000000000000..c947ad852ca0
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-4.png differ
diff --git a/docs/archived/4.0/docs/examples/images/pageplug-5.png b/docs/archived/4.0/docs/examples/images/pageplug-5.png
new file mode 100644
index 000000000000..289789fd639f
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-5.png differ
diff --git a/docs/archived/4.0/docs/examples/images/pageplug-6.png b/docs/archived/4.0/docs/examples/images/pageplug-6.png
new file mode 100644
index 000000000000..a58fbc16537b
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-6.png differ
diff --git a/docs/archived/4.0/docs/examples/images/pageplug-7.png b/docs/archived/4.0/docs/examples/images/pageplug-7.png
new file mode 100644
index 000000000000..414ff44e067b
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-7.png differ
diff --git a/docs/archived/4.0/docs/examples/images/pageplug-8.png b/docs/archived/4.0/docs/examples/images/pageplug-8.png
new file mode 100644
index 000000000000..dc7bb21bd4eb
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-8.png differ
diff --git a/docs/archived/4.0/docs/examples/images/pageplug-9.png b/docs/archived/4.0/docs/examples/images/pageplug-9.png
new file mode 100644
index 000000000000..0011841a7d14
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-9.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/0.png b/docs/archived/4.0/docs/examples/images/tailchat/0.png
new file mode 100644
index 000000000000..197223de4a8c
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/0.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/1.png b/docs/archived/4.0/docs/examples/images/tailchat/1.png
new file mode 100644
index 000000000000..3eced36954fd
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/1.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/10.png b/docs/archived/4.0/docs/examples/images/tailchat/10.png
new file mode 100644
index 000000000000..599ead17397d
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/10.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/11.png b/docs/archived/4.0/docs/examples/images/tailchat/11.png
new file mode 100644
index 000000000000..2841c01c2057
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/11.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/12.png b/docs/archived/4.0/docs/examples/images/tailchat/12.png
new file mode 100644
index 000000000000..43310c129d89
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/12.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/13.png b/docs/archived/4.0/docs/examples/images/tailchat/13.png
new file mode 100644
index 000000000000..c89cc68375b3
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/13.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/14.png b/docs/archived/4.0/docs/examples/images/tailchat/14.png
new file mode 100644
index 000000000000..3c637c422d93
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/14.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/15.png b/docs/archived/4.0/docs/examples/images/tailchat/15.png
new file mode 100644
index 000000000000..3203e025d9fb
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/15.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/16.png b/docs/archived/4.0/docs/examples/images/tailchat/16.png
new file mode 100644
index 000000000000..5f9b13ef61eb
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/16.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/17.png b/docs/archived/4.0/docs/examples/images/tailchat/17.png
new file mode 100644
index 000000000000..21e6091eb4bd
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/17.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/2.png b/docs/archived/4.0/docs/examples/images/tailchat/2.png
new file mode 100644
index 000000000000..234358eae33c
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/2.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/3.png b/docs/archived/4.0/docs/examples/images/tailchat/3.png
new file mode 100644
index 000000000000..e8341c9130cb
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/3.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/4.png b/docs/archived/4.0/docs/examples/images/tailchat/4.png
new file mode 100644
index 000000000000..8d8db3ca8bb8
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/4.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/5.png b/docs/archived/4.0/docs/examples/images/tailchat/5.png
new file mode 100644
index 000000000000..4efdc860247d
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/5.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/6.png b/docs/archived/4.0/docs/examples/images/tailchat/6.png
new file mode 100644
index 000000000000..f0dc20aee71a
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/6.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/7.png b/docs/archived/4.0/docs/examples/images/tailchat/7.png
new file mode 100644
index 000000000000..563c28b48c19
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/7.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/8.png b/docs/archived/4.0/docs/examples/images/tailchat/8.png
new file mode 100644
index 000000000000..602a961b7035
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/8.png differ
diff --git a/docs/archived/4.0/docs/examples/images/tailchat/9.png b/docs/archived/4.0/docs/examples/images/tailchat/9.png
new file mode 100644
index 000000000000..06ad0d6ea766
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/9.png differ
diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-0.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-0.png
new file mode 100644
index 000000000000..e2940389dae3
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-0.png differ
diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-1.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-1.png
new file mode 100644
index 000000000000..b250072db39a
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-1.png differ
diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-10.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-10.png
new file mode 100644
index 000000000000..6411f1d99ccf
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-10.png differ
diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-2.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-2.png
new file mode 100644
index 000000000000..083044758d59
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-2.png differ
diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-3.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-3.png
new file mode 100644
index 000000000000..566495613ee1
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-3.png differ
diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-4.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-4.png
new file mode 100644
index 000000000000..ec61518674cf
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-4.png differ
diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-5.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-5.png
new file mode 100644
index 000000000000..4e5a2e13b63a
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-5.png differ
diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-6.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-6.png
new file mode 100644
index 000000000000..cdeb8f3db368
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-6.png differ
diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-7.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-7.png
new file mode 100644
index 000000000000..a33d5580c9dd
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-7.png differ
diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-8.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-8.png
new file mode 100644
index 000000000000..850d72fbb991
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-8.png differ
diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-9.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-9.png
new file mode 100644
index 000000000000..2003e96b9002
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-9.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_1.png b/docs/archived/4.0/docs/examples/images/wordpress_1.png
new file mode 100644
index 000000000000..bbfd6001594b
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_1.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_10.png b/docs/archived/4.0/docs/examples/images/wordpress_10.png
new file mode 100644
index 000000000000..d1cd7993028d
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_10.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_11.png b/docs/archived/4.0/docs/examples/images/wordpress_11.png
new file mode 100644
index 000000000000..103bf474654c
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_11.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_12.png b/docs/archived/4.0/docs/examples/images/wordpress_12.png
new file mode 100644
index 000000000000..80c69d185806
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_12.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_13.png b/docs/archived/4.0/docs/examples/images/wordpress_13.png
new file mode 100644
index 000000000000..250114b79ef9
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_13.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_14.png b/docs/archived/4.0/docs/examples/images/wordpress_14.png
new file mode 100644
index 000000000000..d180432b02c4
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_14.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_15.png b/docs/archived/4.0/docs/examples/images/wordpress_15.png
new file mode 100644
index 000000000000..877112038094
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_15.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_16.png b/docs/archived/4.0/docs/examples/images/wordpress_16.png
new file mode 100644
index 000000000000..b490fade878c
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_16.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_17.png b/docs/archived/4.0/docs/examples/images/wordpress_17.png
new file mode 100644
index 000000000000..9d0bc0391604
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_17.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_2.png b/docs/archived/4.0/docs/examples/images/wordpress_2.png
new file mode 100644
index 000000000000..83254bdb6cd1
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_2.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_3.png b/docs/archived/4.0/docs/examples/images/wordpress_3.png
new file mode 100644
index 000000000000..1b0a1f926bb8
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_3.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_4.png b/docs/archived/4.0/docs/examples/images/wordpress_4.png
new file mode 100644
index 000000000000..86ad0a605c7d
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_4.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_5.png b/docs/archived/4.0/docs/examples/images/wordpress_5.png
new file mode 100644
index 000000000000..1a9a1566ae1c
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_5.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_6.png b/docs/archived/4.0/docs/examples/images/wordpress_6.png
new file mode 100644
index 000000000000..52b0dc908d42
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_6.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_7.png b/docs/archived/4.0/docs/examples/images/wordpress_7.png
new file mode 100644
index 000000000000..a18bfa6d1703
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_7.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_8.png b/docs/archived/4.0/docs/examples/images/wordpress_8.png
new file mode 100644
index 000000000000..484684f79ae4
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_8.png differ
diff --git a/docs/archived/4.0/docs/examples/images/wordpress_9.png b/docs/archived/4.0/docs/examples/images/wordpress_9.png
new file mode 100644
index 000000000000..24db5cadb900
Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_9.png differ
diff --git a/docs/archived/4.0/docs/examples/low-code-platform/install-appsmith.md b/docs/archived/4.0/docs/examples/low-code-platform/install-appsmith.md
new file mode 100644
index 000000000000..93333797c2af
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/low-code-platform/install-appsmith.md
@@ -0,0 +1,65 @@
+# Quick installation of Appsmith
+
+[Appsmith](https://github.com/appsmithorg/appsmith) is an open-source platform to build, deploy, and maintain internal apps. You can build anything from simple CRUD apps, admin panels, dashboards to custom business apps and complicated multi-step workflows.
+
+## Step 1: Step 1: open the App Launchpad application in the [Sealos](https://cloud.sealos.io) desktop environment
+
+
+
+## Step 2: create a new application
+
+- In App Launchpad, click "New Application" to create a new application.
+
+
+
+## Step 3: application deployment
+
+- Basic configuration:
+
+ - Application name (Custom): appsmith
+
+ - Image name (default latest version): appsmith/appsmith-ce
+
+ - CPU (recommended): 2 Core
+
+ - Memory (recommended): 4 GB
+
+- deployment mode:
+
+ - number of instances (custom): 1
+
+
+
+- Network configuration:
+
+ - Container exposure port: 80
+
+ - Public network access: enabled
+
+
+
+- Advanced configuration:
+
+ - Customize local storage and persist Appsmith data (15 GB is recommended).
+
+
+
+## Step 4: deploy the application
+
+- Click "deploy Application" to start deploying the application.
+
+
+
+## Step 5: access the application
+
+- Click "details" to view, when the STATUS of the application has changed from Pending to Running, it indicates that the application has been launched successfully.
+
+- When STATUS is Running, you can directly access the public network address.
+
+
+
+- The visit was successful!
+
+
+
+
diff --git a/docs/archived/4.0/docs/examples/low-code-platform/install-pageplug.md b/docs/archived/4.0/docs/examples/low-code-platform/install-pageplug.md
new file mode 100644
index 000000000000..833ae0ea299b
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/low-code-platform/install-pageplug.md
@@ -0,0 +1,72 @@
+# Quick installation of PagePlug
+
+[PagePlug](https://github.com/cloudtogo/pageplug) is a Chinese project of [Appsmith](https://github.com/appsmithorg/appsmith), which optimizes the overall performance and Sinicizes based on Appsmith, and also integrates the characteristic form solution Formily component, chart solution Echarts component, low code Mini Program development, etc.
+Is an open source, declarative, visual, intuitive front-end low-code framework for research and development.
+
+## Step 1: Step 1: open the App Launchpad application in the [Sealos](https://cloud.sealos.io) desktop environment
+
+
+
+## Step 2: create a new application
+
+- In App Launchpad, click "New Application" to create a new application.
+
+
+
+## Step 3: application deployment
+
+- Basic configuration:
+
+ - Application name (Custom): pageplug
+
+ - Image name (default latest version): cloudtogouser/pageplug-ce
+
+ - CPU (recommended): 2 Core
+
+ - Memory (recommended): 4 GB
+
+- Deployment model:
+
+ - Number of instances (custom): 1
+
+
+
+- Network configuration:
+
+ - Container exposure port: 80
+
+ - Public network access: enabled
+
+
+
+- Advanced configuration:
+
+ - Customize local storage and persist PagePlug data.
+
+
+
+## Step 4: deploy the application
+
+- Click "deploy Application" to start deploying the application.
+
+
+
+## Step 5: access the application
+
+- Click "details" to view, when the STATUS of the application has changed from Pending to Running, it indicates that the application has been launched successfully.
+
+
+
+
+
+- When STATUS is Running, you can directly access the public network address.
+
+
+
+- If a 503 exception occurs in the access, wait for a while and try again.
+
+
+
+- The visit was successful!
+
+
diff --git a/docs/archived/4.0/docs/examples/others/install-anki.md b/docs/archived/4.0/docs/examples/others/install-anki.md
new file mode 100644
index 000000000000..a368f5c0bfd7
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/others/install-anki.md
@@ -0,0 +1,144 @@
+# Quick Installation of a Custom Anki Sync Server
+
+Anki is a spaced repetition flashcard program. Essentially, it is a card sorting tool that **actively tests users on custom card content, allows users to self-grade their responses, and then uses an algorithm to reschedule cards based on the judgments to optimize long-term retention**.
+
+The so-called "cards" are technically called flash cards. They are small cards with a question or prompt on one side and the answer on the reverse side. You first read the question/prompt, try to recall the answer mentally, then flip the card to verify against the answer provided.
+
+The core principle of flashcard creation is:**one knowledge point per card**. As such, Anki is well-suited for learning languages, memorizing historical dates, formulas, etc.
+
+Anki's official sync server is hosted overseas and is a personal project with limited bandwidth. Syncing is very slow. To sync learning progress and new cards across multiple clients would be extremely painful.
+
+To address this, we need to deploy a custom sync server and have the clients connect to it instead.
+
+## Deploying an Anki Sync Server
+
+Since the February 2023 release of Anki 2.1.57 for desktop, Anki on desktop, Android, and iOS now support custom sync servers without needing to install plugins. Anki users no longer need to worry about sync issues. The longstanding sync problem has finally been completely solved.
+
+Currently, the only project that supports the latest Anki versions is [anki-sync-server-rs](https://github.com/ankicommunity/anki-sync-server-rs). Other sync server projects are now mostly obsolete. This Rust project tracks progress on Anki's official sync server and also uses sqlite as the backend data store.
+
+Below we will deploy anki-sync-server-rs on Sealos and configure it.
+
+First, enter the URL https://cloud.sealos.io/ in your browser to access the Sealos desktop. Then open "App Launchpad":
+
+
+
+Click "Create Application":
+
+
+
+Enter the application name and image name. The exposed container port is `27701`. Enable external network access:
+
+
+
+Scroll down and expand "Advanced Configuration". Click "Edit Environment Variables":
+
+
+
+Paste the following into the environment variable input box:
+
+```
+bash
+Copy code
+
+ANKISYNCD_USERNAME=
+ANKISYNCD_PASSWORD=
+```
+
+Replace `` with your username and `` with your password.
+
+
+
+Click "Add Storage Volume":
+
+
+
+Set the mount path to `/app` and confirm:
+
+
+
+Finally, click "Deploy Application" in the top right corner.
+
+After deployment, click "Details" to enter the application details screen.
+
+
+
+Here you can see the instance status. Wait until the status shows as "running" before proceeding. If it stays pending for a while, click "Details" to check the failure reason:
+
+
+
+Once deployed successfully, you can monitor metrics like CPU and memory usage. Click the external URL to directly access the sync server's web interface.
+
+
+
+If you see the following screen, the deployment succeeded:
+
+
+
+Viewing logs is also straightforward - click the "three dots" on the instance panel and select "Logs":
+
+
+
+
+
+## Client Configuration
+
+### Desktop
+
+To configure the desktop client (macOS/Windows/Linux):
+
+1. Open "Preferences"
+
+ 
+
+2. Click "Network" and look for the "self-hosted sync server" section. Enter your server's address:
+
+ 
+
+3. Restart Anki and click "Sync":
+
+ 
+
+4. A prompt will appear asking for username and password. Enter what you configured earlier:
+
+ 
+
+5. Click OK and syncing will begin.
+
+### Android
+
+On Android, directly configure via "Settings -> Advanced -> Custom sync server":
+
+
+
+Also enter username and password:
+
+> Settings -> Basic -> AnkiWeb Account
+
+This completes the configuration. All card decks should sync over:
+
+
+
+
+The official app version is quite old. For the latest community version, download the Beta here:
+
+- https://github.com/ankidroid/Anki-Android/releases
+
+The **arm64-v8a** version is recommended.
+
+After installing, the sync server can be configured under "Settings -> Sync -> Custom sync server":
+
+
+
+Also enter the username and password:
+
+> Settings -> Sync -> AnkiWeb Account
+
+### iOS
+
+AnkiMobile also supports syncing with a custom server. At least version Ankimobile 2.0.90(20090.2) seems to work based on [reports in the Anki forums](https://forums.ankiweb.net/t/ankimobile-self-sync-server-failure-the-one-bundled-in-version-2-1-60-qt6/27862).
+
+If you encounter sync issues after configuring, try toggling "Allow Anki to access local network" off and on in iOS settings per Anki's docs:
+
+> If you're using AnkiMobile and are unable to connect to a server on your local network, please go into the iOS settings, locate Anki near the bottom, and toggle "Allow Anki to access local network" off and on again.
+
+The tip above is excerpted from the [Anki tutorial](https://docs.ankiweb.net/sync-server.html#client-setup).
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Go Apps.md b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Go Apps.md
new file mode 100644
index 000000000000..619b7db8abc8
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Go Apps.md
@@ -0,0 +1,143 @@
+# Quick installation of Go Apps
+
+First, make sure you have installed the following tools:
+
+- Docker
+
+## Step 1: Write Your Go Program
+
+```go
+package main
+
+import (
+ "fmt"
+ "net/http"
+)
+
+func main() {
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "Hello, World!")
+ })
+
+ http.ListenAndServe(":8080", nil)
+}
+```
+
+## Step 2: Create Docker Image
+
+- In the same directory as `main.go`, create a file named `Dockerfile` with the following content:
+
+```
+FROM golang:1.17 as builder
+
+WORKDIR /app
+COPY . .
+
+RUN go mod init myapp
+RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
+
+FROM alpine:latest
+RUN apk --no-cache add ca-certificates
+WORKDIR /app
+COPY --from=builder /app/main /app/
+
+EXPOSE 8080
+ENTRYPOINT ["/app/main"]
+```
+
+This `Dockerfile` defines a multi-stage build process. In the first stage, we use the official `golang` image as the base image and then compile the Go application. In the second stage, we use the lightweight `alpine` image, copy the compiled binary file to the `/app` directory, and expose port 8080.
+
+- Run the following command in the directory where the `Dockerfile` is located to build a Docker image for the Go application:
+
+```
+docker build -t your_image_name .
+```
+
+Replace `your_image_name` with your image name and tag.
+
+## Step 3: Push Docker Image
+
+- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming you are already logged in to the Docker repository**, use the following command to push the image:
+
+ - First, tag the Docker image. Before pushing an image, you need to add a tag to it so that Docker knows where to push it. Run the following command to add a tag to the image:
+
+ ```
+ docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ Replace `your-image-name` with your local image name, `your-dockerhub-username` with your Docker Hub username, `your-repo-name` with the repository name you want to create on Docker Hub, and `your-tag` with the tag you set for the image (e.g., latest).
+
+ For example:
+
+ ```
+ docker tag demo damager6666/demo:latest
+ ```
+
+ - Next, push the Docker image. Use the following command to push the image to Docker Hub:
+
+ ```
+ docker push your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ Replace `your-dockerhub-username`, `your-repo-name`, and `your-tag` with the actual values. For example:
+
+ ```
+ docker push damager6666/demo:latest
+ ```
+
+## Step 4: Log in to Sealos
+
+- Go to the [Sealos](https://cloud.sealos.io/) official website
+
+
+
+## Step 5: Open the "App Launchpad" App
+
+
+
+## Step 6: Create a New Application
+
+- In "App Launchpad", click "Create New Application" to create a new application.
+
+
+
+## Step 7: Application Deployment
+
+- Basic Configuration:
+ - Application Name (custom): go-demo
+ - Image Name: damager6666/demo:latest
+ - CPU (recommended): 1 Core
+ - Memory (recommended): 1 G
+- Deployment Mode:
+ - Number of Instances (custom): 1
+
+
+
+- Network Configuration:
+ - Container Exposed Port: 8080
+ - Internet Access: Enable
+
+
+
+## Step 8: Deploy Application
+
+- Click "Deploy Application" to start deploying the application.
+
+
+
+## Step 9: Access Application
+
+- Click "Application Management" to view. When the application's STATUS changes from Pending to Running, it means the application has started successfully.
+- When STATUS is Running, you can directly access the external network address.
+
+
+
+- In the browser, enter
+
+```
+https://tmgkflgdlstl.cloud.sealos.io/hello
+```
+
+
+
+- The page displays "Hello, World!", indicating that your Go application is running on Sealos.
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Java Apps.md b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Java Apps.md
new file mode 100644
index 000000000000..fbec744248df
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Java Apps.md
@@ -0,0 +1,259 @@
+# Quick installation of Java Apps
+
+First, make sure you have installed the following tools:
+
+- Docker
+- Maven
+
+## Step 1: Write Your Java Program
+
+- Here, directly generate a basic Spring Boot project through [Spring Initializr](https://start.spring.io/).
+
+
+
+- Open the project's `pom.xml` file and add the following content to the `` section to include the MySQL JDBC driver:
+
+ ```
+
+ mysql
+ mysql-connector-java
+
+ ```
+
+- Next, add the following content to the `application.properties` file in the `src/main/resources` directory to configure the database connection:
+
+ ```
+ spring.datasource.url=jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
+ spring.datasource.username=root
+ spring.datasource.password=your_password
+ spring.jpa.hibernate.ddl-auto=update
+ ```
+
+## Step 2: Create Entity and Repository Classes
+
+Create a new Java class in the project, such as `Person.java`, to represent the entity in the database. Add the following content:
+
+```java
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+
+@Entity
+public class Person {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+ private String name;
+
+ // Getters and setters
+ // ...
+}
+```
+
+- Then create a repository interface, such as `PersonRepository.java`:
+
+ ```java
+ import org.springframework.data.jpa.repository.JpaRepository;
+ import org.springframework.stereotype.Repository;
+
+ @Repository
+ public interface PersonRepository extends JpaRepository {
+ }
+ ```
+
+## Step 3: Create a Web Controller
+
+- In the `src/main/java` directory of the project, create a simple web controller. For example, create a file named `PersonController.java` and add the following content:
+
+```java
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+public class PersonController {
+ @Autowired
+ private PersonRepository personRepository;
+
+ @GetMapping("/getPersons")
+ public String getAllPersons() {
+ List persons = personRepository.findAll();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < persons.size(); i++) {
+ sb.append("id: " + persons.get(i).getId() + " name: " + persons.get(i).getName() + "/n");
+ }
+ System.out.println(sb.toString());
+ return sb.toString();
+ }
+
+ @PostMapping(value = "/addPerson")
+ public Person addPerson(@RequestBody Person person) {
+ return personRepository.save(person);
+ }
+
+
+}
+```
+
+## Step 4: Build the Application
+
+- Build the project using Maven by running the following command:
+
+ ```
+ mvn clean install
+ ```
+
+
+
+## Step 5: Create a Docker Image
+
+- In the root directory of the Java project, create a file named `Dockerfile`. This file will contain the instructions required to build the Docker image. Here's a simple example Dockerfile:
+
+ ````
+ # Use the official OpenJDK image as the base image
+ FROM openjdk:8-jre-slim
+
+ # Set the working directory
+ WORKDIR /app
+
+ # Copy the built JAR file into the image
+ COPY target/demo-0.0.1-SNAPSHOT.jar /app/demo-0.0.1-SNAPSHOT.jar
+
+ # Expose the application's port
+ EXPOSE 8080
+
+ # Set the start command
+ CMD ["java", "-jar", demo-0.0.1-SNAPSHOT.jar"]
+ ```
+ ````
+
+- In the project root directory, run the following command to build the Docker image:
+
+```
+docker build -t java-demo .
+```
+
+## Step 6: Push the Docker Image
+
+- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming you are already logged in to the Docker repository**, use the following command to push the image:
+
+ - First, tag the Docker image. Before pushing an image, you need to add a tag to it so that Docker knows where to push it. Run the following command to add a tag to the image:
+
+ ```
+ docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ Replace `your-image-name` with your local image name, `your-dockerhub-username` with your Docker Hub username, `your-repo-name` with the repository name you want to create on Docker Hub, and `your-tag` with the tag you set for the image (e.g., `latest`).
+
+ - Then, push the tagged image to the Docker repository:
+
+ ```
+ docker push your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+## Step 7: Log in to Sealos
+
+- Visit the [Sealos](https://cloud.sealos.io/) official website
+
+
+
+## Step 8: Open the "Database" application
+
+
+
+## Step 9: Create a database
+
+- Click on "Create New Database"
+
+
+
+- Basic configuration:
+ - Cluster type: mysql
+ - Cluster name: demo-db
+ - CPU (recommended): 1 Core
+ - Memory (recommended): 1 G
+ - Number of instances: 1
+ - Storage capacity: 1 Gi
+- Click "Deploy Cluster"
+
+## Step 10: Configure the database
+
+
+
+- Click on "One-click Connect to Database" and execute the following statements:
+
+ - Create the `test_db` database
+
+ ```sql
+ create database test_db;
+ ```
+
+ - Create the `test` table
+
+ ```sql
+ CREATE TABLE `name_info` (
+ `id` int(11) NOT NULL,
+ `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ PRIMARY KEY (`id`)
+ );
+ ```
+
+ - Insert data
+
+ ```sql
+ insert into test values (1,'Sealos');
+ ```
+
+## Step 11: Open the "App Launchpad" application
+
+
+
+## Step 12: Create a new application
+
+- In "App Launchpad", click on "Create New Application" to create a new application.
+
+
+
+## Step 13: Deploy the application
+
+- Basic configuration:
+ - Application name (custom): java-demo
+ - Image name: damager6666/java-demo:v2
+ - CPU (recommended): 1 Core
+ - Memory (recommended): 1 G
+- Deployment mode:
+ - Number of instances (custom): 1
+
+
+
+- Network configuration:
+ - Exposed container port: 8080
+ - Public network access: enabled
+
+
+
+## Step 14: Deploy the application
+
+- Click on "Deploy Application" to start deploying the application.
+
+
+
+## Step 15: Access the application
+
+- Click on "Application Management" to view the application. When the application's STATUS changes from Pending to Running, it indicates that the application has started successfully.
+- When the STATUS is Running, you can directly access the public network address.
+
+
+
+- Enter the following in your browser:
+
+```
+https://tmgkflgdlstl.cloud.sealos.io/getPersons
+```
+
+
+
+- The page displays the data inserted into the database earlier, indicating that your Java application is now running on Sealos.
+
diff --git a/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Node.js Apps.md b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Node.js Apps.md
new file mode 100644
index 000000000000..c8ec7fd3323e
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Node.js Apps.md
@@ -0,0 +1,218 @@
+# Quick installation of Node.js Apps
+
+First, make sure you have installed the following tools:
+
+- Docker
+
+## Step 1: Write your Node.js program
+
+- Create a file named `app.js` in your directory and add the following code:
+
+ ```js
+ const express = require('express');
+ const mysql = require('mysql');
+ const app = express();
+
+ const db = mysql.createConnection({
+ host: process.env.DB_HOST,
+ user: process.env.DB_USER,
+ password: process.env.DB_PASSWORD,
+ database: process.env.DB_NAME,
+ });
+
+ db.connect((err) => {
+ if (err) throw err;
+ console.log('Connected to the database.');
+ });
+
+ app.get('/', (req, res) => {
+ db.query('SELECT id, name FROM users', (err, results) => {
+ if (err) throw err;
+ res.send(JSON.stringify(results));
+ });
+ });
+
+ app.listen(8080, () => {
+ console.log('Server is running on port 8080');
+ });
+ ```
+
+- Create a file named `package.json` in the root directory of your project, and include the following content:
+
+ ~~~json
+ {
+ "name": "nodejs-web-app-mysql",
+ "version": "1.0.0",
+ "main": "app.js",
+ "scripts": {
+ "start": "node app.js"
+ },
+ "dependencies": {
+ "express": "^4.17.1",
+ "mysql": "^2.18.1"
+ }
+ }
+ ```
+ ~~~
+
+## Step 2: Create a Docker image
+
+- In the root directory of your project, create a file named `Dockerfile`. This file will contain the instructions needed to build the Docker image. Here is a simple example Dockerfile:
+
+```dockerfile
+FROM node:16
+
+WORKDIR /app
+
+COPY package*.json ./
+
+RUN npm install
+
+COPY . .
+
+EXPOSE 8080
+
+CMD ["npm", "start"]
+```
+
+## Step 3: Build the Docker image
+
+- Run the following command in the directory containing the `Dockerfile` to build the Docker image:
+
+ ```
+ docker build -t nodejs-demo .
+ ```
+
+## Step 4: Push the Docker image
+
+- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming you have logged in to the Docker repository**, use the following command to push the image:
+
+ - First, tag the Docker image. Before pushing the image, you need to add a tag to it so that Docker knows where to push it. Run the following command to add a tag to the image:
+
+ ```
+ docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ Replace `your-image-name` with the name of your local image, `your-dockerhub-username` with your Docker Hub username, `your-repo-name` with the name of the repository you want to create on Docker Hub, and `your-tag` with the tag you set for the image (e.g. latest).
+
+ For example:
+
+ ```
+ docker tag nodejs-demo damager6666/nodejs-demo:v1
+ ```
+
+ - Next, push the Docker image. Use the following command to push the image to Docker Hub:
+
+ ```
+ docker push your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ Replace `your-dockerhub-username`, `your-repo-name`, and `your-tag` with the actual values. For example:
+
+ ```
+ docker push damager6666/nodejs-demo:v1
+ ```
+
+## Step 5: Login to Sealos
+
+- Go to the [Sealos](https://cloud.sealos.io/) website.
+
+
+
+## Step 6: Open the "Database" application
+
+
+
+## Step 7: Create a database
+
+- Click on "Create a database"
+
+
+
+- Basic configuration:
+ - Cluster type: mysql
+ - Cluster name: demo-db
+ - CPU (recommended): 1 Core
+ - Memory (recommended): 1 G
+ - Number of instances: 1
+ - Storage capacity: 1 Gi
+- Click "Deploy Cluster"
+
+## Step 8: Configure the database
+
+
+
+- Click "Connect to the database" to execute the following statements:
+
+ - Create a database named test_db
+
+ ```sql
+ create database test_db;
+ ```
+
+ - Create a table named test
+
+ ```sql
+ CREATE TABLE `name_info` (
+ `id` int(11) NOT NULL,
+ `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ PRIMARY KEY (`id`)
+ );
+ ```
+
+ - Insert some data
+
+ ```sql
+ insert into test values (1,'Sealos');
+ ```
+
+## Step 9: Open the "App Launchpad" application
+
+
+
+## Step 10: Create a new application
+
+- In App Launchpad", click "Create New Application" to create a new application.
+
+
+
+## Step 11: Deploy the application
+
+- Basic configuration:
+ - Application name (customizable): nodejs-demo
+ - Image name : damager6666/nodejs-demo:v1
+ - CPU (recommended): 1 core
+ - Memory (recommended): 1 G
+- Deployment mode:
+ - Number of instances (customizable): 1
+
+
+
+- Network configuration:
+ - Exposed container port: 8080
+ - External network access: enabled
+
+
+
+## Step 12: Deploy the application
+
+- Click "Deploy Application" to start deploying the application.
+
+
+
+## Step 13: Access the application
+
+- Click "App Launchpad" to view the application status. When the STATUS changes from Pending to Running, it means that the application has been successfully launched.
+- When the STATUS is Running, you can directly access the external network address.
+
+
+
+- Enter the following URL in your browser:
+
+```
+https://tmgkflgdlstl.cloud.sealos.io/getPersons
+```
+
+
+
+- The page will display the data inserted into the database, indicating that your Node.js application is running on Sealos.
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Python Apps.md b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Python Apps.md
new file mode 100644
index 000000000000..7e4885dfc28f
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Python Apps.md
@@ -0,0 +1,237 @@
+# Quick installation of Python Apps
+
+First, make sure you have the following tools installed:
+
+- Docker
+
+## Step 1: Write your Python program
+
+- Create a file named `app.py` in your project directory and add the following code:
+
+ ```python
+ from flask import Flask, render_template
+ import mysql.connector
+
+ app = Flask(__name__)
+
+ def get_data_from_database():
+ cnx = mysql.connector.connect(
+ host="java-demo-db-mysql.ns-7otl3mb2.svc",
+ user="root",
+ password="l9h8f24b",
+ database="test_db"
+ )
+
+ cursor = cnx.cursor()
+ cursor.execute("SELECT * FROM test")
+ data = cursor.fetchall()
+ cursor.close()
+ cnx.close()
+
+ return data
+
+ @app.route('/')
+ def index():
+ data = get_data_from_database()
+ return render_template('index.html', data=data)
+
+ if __name__ == '__main__':
+ app.run(host='0.0.0.0', debug=True)
+
+ ```
+
+- Create a file named `requirements.txt` in the directory to list the dependencies required by the application.
+
+```
+Flask
+mysql-connector-python
+```
+
+- Create a folder named `templates` in your project directory and create a file named `index.html` inside it. Add the following content to `index.html`:
+
+ ```html
+
+
+
+
+ Data from MySQL
+
+
+ Data from MySQL
+
+
+ | Column1 |
+ Column2 |
+ Column3 |
+
+ {% for row in data %}
+
+ | {{ row[0] }} |
+ {{ row[1] }} |
+ {{ row[2] }} |
+
+ {% endfor %}
+
+
+
+
+ Replace `Column1`, `Column2`, and `Column3` with the actual column names you want to display.
+ ```
+
+## Step 2: Create a Docker image
+
+- Create a file named `Dockerfile` in the root directory of the project. This file will contain the instructions needed to build the Docker image. Here is a simple example Dockerfile:
+
+ ```dockerfile
+ FROM python:3.8
+
+ WORKDIR /app
+
+ COPY requirements.txt .
+
+ RUN pip install --no-cache-dir -r requirements.txt
+
+ COPY . .
+
+ EXPOSE 5000
+
+ CMD ["python", "app.py"]
+ ```
+
+## Step 3: Build the Docker image
+
+- Run the following command in the directory containing the `Dockerfile` to build the Docker image:
+
+ ```
+ docker build -t python-demo .
+ ```
+
+## Step 4: Push the Docker image
+
+- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming that you have logged in to the Docker repository**, use the following command to push the image:
+
+ - First, tag the Docker image. Before pushing the image, you need to add a tag to it so that Docker knows where to push it. Run the following command to add a tag to the image:
+
+ ```
+ docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ Replace `your-image-name` with your local image name, `your-dockerhub-username` with your Docker Hub username, `your-repo-name` with the name of the repository you want to create on Docker Hub, and `your-tag` with the tag you set for the image (e.g. latest).
+
+ For example:
+
+ ```
+ docker tag python-demo damager6666/python-demo:v1
+ ```
+
+ - Next, push the Docker image. Use the following command to push the image to Docker Hub:
+
+ ```
+ docker push your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ Replace `your-dockerhub-username`, `your-repo-name`, and `your-tag` with the actual values. For example:
+
+ ```
+ docker push damager6666/python-demo:v1
+ ```
+
+## Step 5: Log in to Sealos
+
+- Go to the [Sealos](https://cloud.sealos.io/) website.
+
+
+
+## Step 6: Open the "Database" application
+
+
+
+## Step 7: Create a database
+
+- Click "Create Database"
+
+
+
+- Basic Configuration:
+ - Cluster Type: mysql
+ - Cluster Name: demo-db
+ - CPU (Recommended): 1 Core
+ - Memory (Recommended): 1 G
+ - Number of Instances: 1
+ - Storage Capacity: 1 Gi
+- Click "Deploy Cluster"
+
+## Step 8: Configure the database
+
+
+
+- Click "Connect to the Database" and execute the following SQL statements:
+
+ - Create the test_db database
+
+ ```sql
+ create database test_db;
+ ```
+
+ - Create the test table
+
+ ```sql
+ CREATE TABLE `name_info` (
+ `id` int(11) NOT NULL,
+ `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ PRIMARY KEY (`id`)
+ );
+ ```
+
+ - Insert data
+
+ ```sql
+ insert into test values (1,'Sealos');
+ ```
+
+## Step 9: Open the "App Management" application
+
+
+
+## Step 10: Create a new app
+
+- In "App Management", click "New App" to create a new app.
+
+
+
+## Step 11: App deployment
+
+- Basic Configuration:
+ - Application Name (Customized): python-demo
+ - Image Name: damager6666/python-demo:v1
+ - CPU (Recommended): 1 Core
+ - Memory (Recommended): 1 G
+- Deployment Mode:
+ - Number of Instances (Customized): 1
+
+
+
+- Network Configuration:
+ - Container Exposed Port: 5000
+ - External Access: Enabled
+
+
+
+## Step 12: Deploy the app
+
+
+
+## Step 13: Access the app
+
+- Click "App Management" to check when the STATUS of the app changes from "Pending" to "Running", indicating that the app has been successfully launched.
+- When the STATUS is Running, you can directly access the external network address.
+
+
+
+- Enter the external network address in the browser to access the data that was just stored in MySQL, indicating that the program has been successfully deployed on Sealos.
+
+ ```http
+ https://mkqreaqfgwac.cloud.sealos.io/
+ ```
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Vue Apps.md b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Vue Apps.md
new file mode 100644
index 000000000000..83b0260994c9
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Vue Apps.md
@@ -0,0 +1,137 @@
+# Quick installation of Vue Apps
+
+First, make sure you have installed the following tools:
+
+- Docker
+- Node.js
+- npm
+
+## Step 1: Install Vue CLI
+
+```bash
+npm install -g @vue/cli
+```
+
+## Step 2: Create a new project with Vue CLI
+
+```bash
+vue create my-vue-app
+```
+
+## Step 3: Build the production version of the project with Vue CLI
+
+```bash
+cd my-vue-app
+npm run build
+```
+
+## Step 4: Create a Docker image
+
+- In the `my-vue-app` directory, create a file named `Dockerfile` with the following content:
+
+```dockerfile
+# 使用 nginx 镜像作为基础镜像
+FROM nginx:1.21-alpine
+
+# 复制构建的前端文件到 nginx 容器中
+COPY dist/ /usr/share/nginx/html
+
+# 暴露端口
+EXPOSE 80
+
+# 启动 nginx
+CMD ["nginx", "-g", "daemon off;"]
+```
+
+- Run the following command in the directory where the `Dockerfile` is located to build a Docker image for the Vue application:
+
+```bash
+docker build -t my-vue-app .
+```
+
+## Step 5: Push Docker Image
+
+- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming you are already logged in to the Docker repository**, use the following command to push the image:
+
+ - First, tag the Docker image. Before pushing an image, you need to add a tag to it so that Docker knows where to push it. Run the following command to add a tag to the image:
+
+ ```
+ docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ Replace `your-image-name` with your local image name, `your-dockerhub-username` with your Docker Hub username, `your-repo-name` with the repository name you want to create on Docker Hub, and `your-tag` with the tag you set for the image (e.g., latest).
+
+ For example:
+
+ ```
+ docker tag my-vue-app damager6666/my-vue-app:v1
+ ```
+
+ - Next, push the Docker image. Use the following command to push the image to Docker Hub:
+
+ ```
+ docker push your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ Replace `your-dockerhub-username`, `your-repo-name`, and `your-tag` with the actual values. For example:
+
+ ```
+ docker push damager6666/my-vue-app:v1
+ ```
+
+## Step 6: Log in to Sealos
+
+- Go to the [Sealos](https://cloud.sealos.io/) official website
+
+
+
+## Step 7: Open the "App Launchpad" App
+
+
+
+## Step 8: Create a New Application
+
+- In "App Launchpad", click "Create New Application" to create a new application.
+
+
+
+## Step 9: Application Deployment
+
+- Basic Configuration:
+ - Application Name (custom): my-vue-app
+ - Image Name: damager6666/my-vue-app:v1
+ - CPU (recommended): 1 Core
+ - Memory (recommended): 1 G
+- Deployment Mode:
+ - Number of Instances (custom): 1
+
+
+
+- Network Configuration:
+ - Container Exposed Port: 80
+ - Internet Access: Enable
+
+
+
+## Step 10: Deploy Application
+
+- Click "Deploy Application" to start deploying the application.
+
+
+
+## Step 11: Access Application
+
+- Click "Application Management" to view. When the application's STATUS changes from Pending to Running, it means the application has started successfully.
+- When STATUS is Running, you can directly access the external network address.
+
+
+
+- In the browser, enter
+
+```
+https://mvpztqzczudy.cloud.sealos.io
+```
+
+)
+
+- The page displays "Hello, Vue.js!", indicating that your Vue application is running on Sealos.
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-0.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-0.png
new file mode 100644
index 000000000000..6ef487195a8c
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-0.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-1.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-1.png
new file mode 100644
index 000000000000..b149426e0e64
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-1.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-10.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-10.png
new file mode 100644
index 000000000000..3b10465747db
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-10.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-11.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-11.png
new file mode 100644
index 000000000000..b1cf562ca8c0
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-11.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-12.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-12.png
new file mode 100644
index 000000000000..6eedee90f87c
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-12.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-13.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-13.png
new file mode 100644
index 000000000000..3548b505e15f
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-13.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-2.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-2.png
new file mode 100644
index 000000000000..179f4b85a5fb
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-2.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-3.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-3.png
new file mode 100644
index 000000000000..3aa6b5e891aa
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-3.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-4.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-4.png
new file mode 100644
index 000000000000..bcb67e312203
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-4.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-5.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-5.png
new file mode 100644
index 000000000000..21df65190ff2
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-5.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-6.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-6.png
new file mode 100644
index 000000000000..ae30ba940afc
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-6.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-7.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-7.png
new file mode 100644
index 000000000000..f77e89f54c54
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-7.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-8.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-8.png
new file mode 100644
index 000000000000..19a4f0f3276e
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-8.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-9.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-9.png
new file mode 100644
index 000000000000..b5582f255cd6
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-9.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/nodejs-example-0.png b/docs/archived/4.0/docs/examples/programming-languages/images/nodejs-example-0.png
new file mode 100644
index 000000000000..6e95ed74b246
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/nodejs-example-0.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/nodejs-example-1.png b/docs/archived/4.0/docs/examples/programming-languages/images/nodejs-example-1.png
new file mode 100644
index 000000000000..ad9450487f21
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/nodejs-example-1.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/python-example-0.png b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-0.png
new file mode 100644
index 000000000000..86c16c2d71df
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-0.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/python-example-1.png b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-1.png
new file mode 100644
index 000000000000..b783a48a786c
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-1.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/python-example-2.png b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-2.png
new file mode 100644
index 000000000000..86144296b4aa
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-2.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/python-example-3.png b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-3.png
new file mode 100644
index 000000000000..54ceb5547f34
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-3.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/python-example-4.png b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-4.png
new file mode 100644
index 000000000000..3ffbe07560ba
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-4.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-0.png b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-0.png
new file mode 100644
index 000000000000..648276d14120
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-0.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-1.png b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-1.png
new file mode 100644
index 000000000000..e47a1b27115d
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-1.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-2.png b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-2.png
new file mode 100644
index 000000000000..dab8d56621b8
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-2.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-3.png b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-3.png
new file mode 100644
index 000000000000..9ac654f24398
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-3.png differ
diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-4.png b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-4.png
new file mode 100644
index 000000000000..b00209521090
Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-4.png differ
diff --git a/docs/archived/4.0/docs/examples/social-communication/install-tailchat.md b/docs/archived/4.0/docs/examples/social-communication/install-tailchat.md
new file mode 100644
index 000000000000..d81e411431ad
--- /dev/null
+++ b/docs/archived/4.0/docs/examples/social-communication/install-tailchat.md
@@ -0,0 +1,147 @@
+# Quick installation of Tailchat
+
+[Tailchat](https://tailchat.msgbyte.com/) is an open source IM application that is pluggable and easy to expand. Plugin architecture gives Tailchat unlimited possibilities.
+
+Front-end micro-kernel architecture + back-end micro-service architecture makes Tailchat to control any customized/privatized scenarios
+
+Created for enterprises and private domain users, highly free group management and customized panel display allow private domain owners to better display their works, manage users, and build their own brand and circle.
+
+
+
+## First, enter Sealos and open "Application Management"
+
+
+
+## Create a new application
+
+
+
+### Create dependencies
+
+As an enterprise-level application, `tailchat` has the minimum dependencies of `mongodb`, `redis`, and `minio`. Let's create them one by one.
+
+#### MongoDB
+
+For convenience, we will fix one instance and bind it to local storage. The image used is `mongo:4`. Note that because we did not set a password for the database, do not provide network services to the public network. The container exposes port 27017, which is the default database service port. The content is as follows:
+
+
+
+Click "Deploy Application" to submit the deployment. Wait patiently for a while, and you can see that the application has started up.
+
+
+
+> Note: that the initial allocation of 64m is too small for MongoDB, so I changed it to 128m by modifying the application. Resource allocation can be changed at any time, which is also a convenient feature of Sealos/Kubernetes.
+
+#### Minio
+
+Next, we will create Minio, an open-source object storage service. We can also quickly create it through Sealos's UI. The image used is `minio/minio`. Note that we need to make some adjustments:
+
+- Expose port: 9000
+- Change the run command to: `minio`
+- Change the command parameters to: `server /data`
+- Set environment variables:
+ - MINIO_ROOT_USER: tailchat
+ - MINIO_ROOT_PASSWORD: com.msgbyte.tailchat
+- Local storage: `/data`
+
+The final result is as follows:
+
+
+
+Click the "Deploy" button and you can see that the service has started up normally.
+
+#### Redis
+
+Finally, we need to deploy Redis as a content cache and message forwarding. The image used is `redis:alpine`, and the exposed port is `6379`. The final result is as follows:
+
+
+
+### Create Tailchat itself
+
+At this point, all the dependencies required by Tailchat have been deployed, as shown below:
+
+
+
+Now we can deploy the Tailchat itself. The Tailchat itself will be relatively complex, but because Sealos is purely UI-based, it will not be too complicated.
+
+- Use image: `moonrailgun/tailchat`
+- Expose port: `11000` (remember to open external access)
+- Configure environment variables as follows:
+ ```
+ SERVICEDIR=services,plugins
+ TRANSPORTER=redis://redis:6379
+ REDIS_URL=redis://redis:6379
+ MONGO_URL=mongodb://mongo/tailchat
+ MINIO_URL=minio:9000
+ MINIO_USER=tailchat
+ MINIO_PASS=com.msgbyte.tailchat
+ ```
+
+The final effect is as follows:
+
+
+
+After waiting patiently for a while, you can see that the Tailchat service has started up.
+
+
+
+## Preview service
+
+First, we can check the availability of the Tailchat service by adding `/health` to the external address provided by the service, such as `https://.cloud.sealos.io/health`. When it starts up, the Tailchat service will return content like this:
+
+
+
+This JSON string contains the image version used, node name, system usage, and microservice loading status. Here we can see that my common services, such as `user`/`chat.message`, and some services with plugin prefixes such as `plugin.registry`, have all started up normally, indicating that our server is running normally. Now we can directly access our external address and see that after a short loading time, the page opens normally and automatically jumps to the login page.
+
+
+
+Register an account casually, and you can see that we can enter the main interface of Tailchat normally, as shown in the following figure:
+
+
+
+At this point, our service has successfully landed in Sealos.
+
+## Scaling service
+
+Of course, as a distributed architecture system, Tailchat naturally supports horizontal scaling. In Sealos, scaling is also very simple. Just modify the number of instances through the change operation:
+
+
+
+
+
+
+
+At this point, when we access `https://.cloud.sealos.io/health`, we can see that we can access different nodes.
+
+
+
+## Add Tailchat entry to desktop
+
+Open Terminal, enter `vim app.yml` to create and edit a configuration file
+
+Enter the following content, note that the url should be replaced with the url deployed by yourself
+
+```yml
+apiVersion: app.sealos.io/v1
+kind: App
+metadata:
+ name: tailchat-app-entry
+spec:
+ name: Tailchat
+ icon:
+ type: iframe
+ data:
+ url:
+ desc:
+ icon: https://tailchat.msgbyte.com/img/logo.svg
+ menuData:
+ displayType: normal
+```
+
+Press `esc` to exit edit mode, press `:wq` to save and exit vim
+
+Type `kubectl apply -f app.yml` to start the configuration.
+
+After refreshing the page, we can see that our entry appears on the desktop of `sealos`
+
+
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/add-domain.md b/docs/archived/4.0/docs/guides/applaunchpad/add-domain.md
new file mode 100644
index 000000000000..dbdbb52ee6f7
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/applaunchpad/add-domain.md
@@ -0,0 +1,29 @@
+---
+sidebar_position: 2
+---
+
+# Add a domain
+
+Assigning a custom domain to your project guarantees that visitors to your application will have a tailored experience that aligns with your brand.
+
+## When Deploying
+
+Just enable "Public Access" when you're deploying, and [Sealos](https://cloud.sealos.io) will sorts you out with a domain.
+
+
+
+Now, on your domain provider's end, link the 'CNAME' to the one Sealos provided.
+
+Once it's active, jump back to Sealos, click on "Custom Domain" to the side:
+
+
+
+Enter your custom domain in the pop-up box and click confirm.
+
+
+
+To wrap up, click the "Deploy" button. Once your app's live, click on the external address to access the app via the custom domain.
+
+## Post Deployment
+
+For the apps you've deployed, just click "Update" top-right on the app details page. Then, follow the earlier steps to integrate your custom domain.
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/applaunchpad.md b/docs/archived/4.0/docs/guides/applaunchpad/applaunchpad.md
new file mode 100644
index 000000000000..4f12047ba05b
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/applaunchpad/applaunchpad.md
@@ -0,0 +1,55 @@
+---
+sidebar_position: 1
+---
+
+# App Launchpad
+
+**App Launchpad** is a feature within Sealos that serves as a single-image deployment tool. Its main goal is to streamline and expedite the process of deploying applications, allowing you to launch your application in as little as 5 minutes.
+
+The tool currently boasts a range of functionalities:
+
+- Capability to deploy applications using private images.
+- Flexibility to tailor CPU and memory resources according to the specific needs of the application.
+- Support for deploying multiple replicas.
+- Horizontal Pod Autoscaling (HPA) for dynamic scaling.
+- Provision of external URLs for easy access from the public network.
+- Option to assign custom domain to applications, enhancing both brand visibility and the user experience.
+- Utilization of ConfigMap for configuration file management.
+- Persistent storage solutions for application data, ensuring both its security and continuity.
+- Real-time monitoring features for applications and Pods to facilitate prompt issue detection and resolution.
+- Comprehensive logging of application activities, aiding in troubleshooting and performance optimization.
+- Analysis of system events (Events) to extract critical insights for enhancing application performance.
+- A convenient one-click feature to access the container terminal, simplifying management and debugging tasks.
+- Ability to expose several ports of an application to the external network.
+
+## [Quick Start](/quick-start/use-app-launchpad.md)
+
+For quick and easy installation of commonly utilized applications.
+
+## [Update Application](/guides/applaunchpad/update-app.md)
+
+Guidance on modifying application configurations after initial deployment.
+
+## [Add a domain](/guides/applaunchpad/add-domain.md)
+
+Instructions for integrating a custom domain with your application.
+
+## [Exposing Multiple Ports](/guides/applaunchpad/expose-multi-ports.md)
+
+Details on how to make multiple ports of an application accessible externally.
+
+## [Environment](/guides/applaunchpad/environment.md)
+
+Directions for configuring applications through the use of environment variables.
+
+## [ConfigMap](/guides/applaunchpad/configmap.md)
+
+Guidelines for setting up application configurations via configuration files.
+
+## [Autoscaling](/guides/applaunchpad/autoscale.md)
+
+Strategy for autoscaling the number of application instances in response to varying workloads.
+
+## [Persistent Volume](/guides/applaunchpad/persistent-volume.md)
+
+Utilizing persistent storage for the long-term preservation of data.
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/autoscale.md b/docs/archived/4.0/docs/guides/applaunchpad/autoscale.md
new file mode 100644
index 000000000000..48a5e5ebfa4f
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/applaunchpad/autoscale.md
@@ -0,0 +1,24 @@
+---
+sidebar_position: 6
+---
+
+# Autoscaling
+
+In [Sealos](https://cloud.sealos.io), the "[App Launchpad](/guides/applaunchpad/applaunchpad.md)" feature enables the automatic adjustment of application instance numbers to effectively respond to varying load conditions. This functionality is known as "**Autoscaling**," or more technically, the Horizontal Pod Autoscaler (HPA).
+
+Autoscaling operates by dynamically altering the count of application instances based on specific metrics like CPU and memory usage. This ensures that the applications run efficiently and resources are optimally utilized.
+
+> Key Point: In Autoscaling, "usage" typically refers to the average use across all instances of an application. For example, if an app runs on two instances, its average CPU usage is calculated as the mean of the usage of these two instances.
+
+The workings of Autoscaling are as follows:
+
+1. **Monitoring**: It continuously monitors crucial performance indicators like CPU and memory usage.
+2. **Decision Making**: Based on predefined thresholds (e.g., maintaining CPU usage below 50%), it calculates the required adjustments in the instance count.
+3. **Adjustment**: Following this, Autoscaling automatically instructs the Sealos controller to modify the number of instances, ensuring the usage stays within the desired range.
+
+For instance, if we set up an application with specific Autoscaling rules such as a maximum CPU usage of 50% and the ability for instance numbers to vary between 1 and 5, Autoscaling will:
+
+- Increase the number of instances when the average CPU usage exceeds 50%, up to a maximum of 5.
+- Decrease the number of instances when the average CPU usage drops below 50%, but always maintain at least one instance in operation.
+
+
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/configmap.md b/docs/archived/4.0/docs/guides/applaunchpad/configmap.md
new file mode 100644
index 000000000000..64f1f6488c18
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/applaunchpad/configmap.md
@@ -0,0 +1,17 @@
+---
+sidebar_position: 5
+---
+
+# ConfgMap
+
+In [Sealos](https://cloud.sealos.io), the implementation of configuration files plays a vital role, particularly when the application deals with numerous or complex configurations. Differing from environment variables, configuration files are a more versatile and dependable means for managing settings. Environment variables are more apt for simple, small-scale configuration tasks.
+
+The primary strength of configuration files is their capability to hold and control elaborate configuration data, including aspects like configuration files, command-line arguments, and environment variables. These pieces of data can be incorporated into the container upon the launch of the application container, facilitating adjustments to the application's functionalities without the necessity of recompiling the image.
+
+Take, for example, the Nginx container. The utilization of configuration files in this context can be described as follows:
+
+- **Filename**: This pertains to a file within the Nginx container, for which references can be drawn from the instructions provided by the image supplier.
+- **File Value**: This is the content corresponding to the file. In cases where the content is elaborate, it's recommended to complete editing it offline and then paste it into the specified location.
+- **Key Points**: The approach involves mounting an individual file, not an entire directory. It is imperative to precisely identify the file to be mounted, rather than just a directory path.
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/environment.md b/docs/archived/4.0/docs/guides/applaunchpad/environment.md
new file mode 100644
index 000000000000..72f493ecf436
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/applaunchpad/environment.md
@@ -0,0 +1,28 @@
+---
+sidebar_position: 4
+---
+
+# Environment
+
+In [Sealos](https://cloud.sealos.io), environment variables are pivotal in managing the configuration data for container applications. These variables enable the provision of essential configuration information to applications without necessitating changes to the application's code or image, thus bolstering their maintainability and scalability.
+
+The process of defining environment variables in the "[App Launchpad](/guides/applaunchpad/applaunchpad.md)" interface is streamlined through a bulk input method. Users can define multiple variables by entering them line by line. Each variable comprises a key and a value, separated by either an equal sign (=) or a colon (:). The interface is designed to automatically eliminate any invalid characters from the key, ensuring the accuracy and validity of the environment variables.
+
+
+
+**Environment Variable Formats That Are Correctly Interpreted:**
+
+```shell
+host=127.0.0.1
+port:3000
+name: sealos
+- username=123
+- password:123
+# Comments like this line are ignored, as they don't include an equal sign (=) or a colon (:), which are the key markers.
+```
+
+**Environment Variable Formats That Cannot Be Interpreted:**
+
+```shell
+host=127.0.0.1 # This line is interpreted because it contains an equal sign (=) or a colon (:). The comment here is also considered part of the variable due to the preceding equal sign (=).
+```
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/expose-multi-ports.md b/docs/archived/4.0/docs/guides/applaunchpad/expose-multi-ports.md
new file mode 100644
index 000000000000..ae41b094222b
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/applaunchpad/expose-multi-ports.md
@@ -0,0 +1,20 @@
+---
+sidebar_position: 3
+---
+
+# Exposing Multiple Ports
+
+In complex application environments, it's common for services to expose multiple ports simultaneously to cater to diverse needs. These requirements can emerge from various scenarios:
+
++ **Multi-protocol support**: For example, an application might support both HTTP and HTTPS, necessitating the exposure of both ports 80 and 443.
++ **Multi-functional application**: An application might have a web service and an admin service, both of which listen on different ports.
++ **Compatibility considerations**: To remain compatible with older versions or other services, you might need to expose ports for both the new and old interfaces.
++ **Combined Database + App**: For instance, if you have an application and a database within the same Pod, you might need to expose ports for both the application and the database.
++ **Prometheus Monitoring and App Service**: If your application has a business port and another port for Prometheus monitoring via `/metrics`, you might need to expose both.
++ **Coexistence of GRPC and RESTful services**: If your application offers both GRPC and RESTful services, you might need to expose separate ports for each type of service.
+
+When deploying applications using "[App Launchpad](/guides/applaunchpad/applaunchpad.md)" on [Sealos](https://cloud.sealos.io), you can easily choose to expose multiple ports. During the deployment process, users simply click on the "Network" option and then select "Add Port" to configure multiple ports.
+
+
+
+Furthermore, the Sealos platform offers external access to these ports. Once exposed to the public network, each port will be assigned a unique sub-domain, facilitating easier remote access and management.
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain.png
new file mode 100644
index 000000000000..013dfd23003e
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain2.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain2.png
new file mode 100644
index 000000000000..840e42c1e952
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain2.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain3.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain3.png
new file mode 100644
index 000000000000..7ff6bfd47d46
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain3.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad.jpg b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad.jpg
new file mode 100644
index 000000000000..1a64b79192e3
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad.jpg differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad10.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad10.png
new file mode 100644
index 000000000000..2a88dbdda281
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad10.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad11.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad11.png
new file mode 100644
index 000000000000..acc586c337db
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad11.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad12.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad12.png
new file mode 100644
index 000000000000..d8490adeccc8
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad12.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad3.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad3.png
new file mode 100644
index 000000000000..e64eadecb89a
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad3.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad4.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad4.png
new file mode 100644
index 000000000000..01c66a4b184d
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad4.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad6.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad6.png
new file mode 100644
index 000000000000..6019b61ca10f
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad6.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad7.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad7.png
new file mode 100644
index 000000000000..57114556318f
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad7.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad8.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad8.png
new file mode 100644
index 000000000000..b72c79adb57d
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad8.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad9.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad9.png
new file mode 100644
index 000000000000..8a111d8424a8
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad9.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-list.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-list.png
new file mode 100644
index 000000000000..a9461aae6b7e
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-list.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/applaunchpad12.png b/docs/archived/4.0/docs/guides/applaunchpad/images/applaunchpad12.png
new file mode 100644
index 000000000000..b92744e027a7
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/applaunchpad12.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/applaunchpad13.png b/docs/archived/4.0/docs/guides/applaunchpad/images/applaunchpad13.png
new file mode 100644
index 000000000000..5e0307ba334a
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/applaunchpad13.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/autoscale.png b/docs/archived/4.0/docs/guides/applaunchpad/images/autoscale.png
new file mode 100644
index 000000000000..0c59a5f3bfb0
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/autoscale.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/multi-ports1.png b/docs/archived/4.0/docs/guides/applaunchpad/images/multi-ports1.png
new file mode 100644
index 000000000000..74e5bd569bf1
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/multi-ports1.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/persistent-volume.png b/docs/archived/4.0/docs/guides/applaunchpad/images/persistent-volume.png
new file mode 100644
index 000000000000..af83504c71ca
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/persistent-volume.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert1.png b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert1.png
new file mode 100644
index 000000000000..a4ba01524001
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert1.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert2.png b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert2.png
new file mode 100644
index 000000000000..b7ea59eed18a
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert2.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert3.png b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert3.png
new file mode 100644
index 000000000000..0fddf4574890
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert3.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert4.png b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert4.png
new file mode 100644
index 000000000000..aa4fc627bc11
Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert4.png differ
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/persistent-volume.md b/docs/archived/4.0/docs/guides/applaunchpad/persistent-volume.md
new file mode 100644
index 000000000000..6b0b80f0b764
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/applaunchpad/persistent-volume.md
@@ -0,0 +1,13 @@
+---
+sidebar_position: 7
+---
+
+# Persistent Volume
+
+[Sealos](https://cloud.sealos.io) offers a flexible environment where containers can be effortlessly created and destroyed. This flexibility is advantageous for application deployment and management, but it also raises the issue of maintaining data persistence. In scenarios where data is stored within an application container, its destruction leads to the loss of all stored data.
+
+To counter this problem, the use of persistent storage is essential. Persistent storage ensures that data is stored externally, thereby preserving it even through container restarts or redeployments. This is particularly vital for applications requiring data retention, like databases, file storage systems, or any services involving user data.
+
+For instance, in deploying Nextcloud, all data associated with its container is located in the `/var/www/html` directory. To maintain data continuity, it's necessary to use external storage solutions for persisting data in this directory.
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/guides/applaunchpad/set-cert.md b/docs/archived/4.0/docs/guides/applaunchpad/set-cert.md
new file mode 100644
index 000000000000..8875d6483766
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/applaunchpad/set-cert.md
@@ -0,0 +1,54 @@
+---
+sidebar_position: 8
+---
+
+# Set a Custom Domain Certificate
+
+Successfully set up a custom domain in "App Launchpad" but cannot access the domain, and it shows the certificate is not
+secure. This is because cert-manager did not successfully issue the certificate. To resolve this issue, we can manually
+set up the certificate.
+
+First, make sure you have successfully set up CNAME in your cloud provider and have downloaded the certificate
+corresponding to your domain.
+
+Open "App Launchpad", set a custom domain.
+
+
+
+Open the "Terminal", and execute the following commands in sequence.
+
+```bash
+# Create tls.crt using the certificate file information (replace xxxx with the actual certificate file information).
+cat > tls.crt < tls.key <established with the database simultaneously. |
+| max_wal_size | Sets the maximum size of WAL (Write-Ahead Logging) files. |
+| min_wal_size | Sets the minimum size of WAL files. |
+| max_worker_processes | Sets the maximum number of background processes that PostgreSQL can start. |
+| shared_buffers | The size of memory used for data caching. |
+
+**Common MySQL Parameters**:
+
+| Parameter Name | Description |
+|-------------------------|-----------------------------------------------------------|
+| innodb_buffer_pool_size | Sets the size of the InnoDB buffer pool. |
+| max_connections | The maximum number of concurrent connections allowed. |
+| query_cache_size | The size of the query cache. |
+| thread_cache_size | The size of the thread cache. |
+| max_allowed_packet | The maximum packet size. |
+| innodb_log_file_size | The size of the InnoDB log file. |
+
+**Common MongoDB Parameters**:
+
+| Parameter Name | Description |
+|-------------------------|-----------------------------------------------------------|
+| storage.dbPath | The path for storing data files. |
+| storage.journal.enabled | Sets the maximum size of WAL (Write-Ahead Logging) files. |
+| min_wal_size | Enables logging. |
+| net.port | The server port. |
+| net.bindIp | The bound IP address. |
+
+**Common Redis Parameters**:
+
+| Parameter Name | Description |
+|-------------------------|-----------------------------------------------------------|
+| maxclients | The maximum number of client connections. |
+| maxmemory | The maximum amount of memory usage. |
+| maxmemory-policy | The memory eviction policy. |
+| appendonly | Switch for AOF persistence. |
+| appendfsync | The frequency of AOF file flushing. |
+
+4. **Apply pg-config.yaml**:
+
+```bash
+$ kubectl apply -f pg-config.yaml
+```
+
+
+5. **Check whether pg-config.yaml is successfully applied**:
+```bash
+# If the status of OpsRequest is Succeed and the status of pod is Running, the application is successfully configured
+$ kubectl get OpsRequest
+$ kubectl get pod
+```
+
+
+6. **Access the database to check whether the configuration takes effect**:
+```bash
+$ show max_connections;
+```
+
+
+
diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_1.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_1.png
new file mode 100644
index 000000000000..d8c8c62be3a6
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_1.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_2.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_2.png
new file mode 100644
index 000000000000..4bd234485bf3
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_2.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_3.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_3.png
new file mode 100644
index 000000000000..1f6d2ff3383b
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_3.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_4.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_4.png
new file mode 100644
index 000000000000..02f9a61edddf
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_4.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_5.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_5.png
new file mode 100644
index 000000000000..412e0bbe7fc7
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_5.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_6.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_6.png
new file mode 100644
index 000000000000..5f49dd84f12e
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_6.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_7.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_7.png
new file mode 100644
index 000000000000..88f930a876f0
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_7.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/dbprovider.md b/docs/archived/4.0/docs/guides/dbprovider/dbprovider.md
new file mode 100644
index 000000000000..8092c4359491
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/dbprovider/dbprovider.md
@@ -0,0 +1,208 @@
+---
+sidebar_position: 2
+---
+
+# Database
+
+** Database ** is the core component of an application, Sealos provides a database cluster deployment management tool, can help you quickly deploy database clusters. It supports MySQL, PostgreSQL, MongoDB, Redis, Kafka, and more. Currently the ** database ** supports the following features.
+
++ Elastic expansion
++ Support multi-version and multi-type databases
++ Database connection: one-click connection, external network access
++ Database connection: manual backup, automatic backup
++ Database monitoring: resource, status, and performance monitoring
++ Database migration: public network migration and file migration
++ Database high availability: Supports multiple database instances
++ Visual database management: Create, delete, update, pause, restart the database
+
+
+## Quick start
+
+Take deploying a PostgreSQL database as an example to experience the convenience of a database.
+
+Deployment and access in just 6 steps:
+
+1. Enter the Database application from the [Sealos](https://cloud.sealos.io) desktop:
+
+
+
+2. Click on the creation of a new cluster.:
+
+
+
+3. Select the corresponding database and parameters:
+
+
+
+4. Click on deployment:
+
+
+
+5. Enter DB to view details:
+
+
+
+6. Database details:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Database connection
+
+### One-click connection
+
+1. Enter ** database **, click Connect:
+
+
+
+2. Manipulate the database at the terminal:
+
+
+
+
+### Extranet access
+
+1. Enter ** database **, click Connect:
+
+
+
+2. Confirm open ** database **:
+
+
+
+3. Copy the database connection information:
+
+
+
+4. Connect to the database in the Database Connection tool:
+
+
+
+
+
+
+## Database backup
+
+### Manual backup
+1. Enter the database backup page, click Backup:
+
+
+
+2. Enter the backup information to start the backup:
+
+
+
+3. Check Backup status:
+
+
+
+
+
+
+### Automatic backup
+1. Enter the database backup page, click Backup:
+
+
+
+2. Enable automatic backup and enter backup information:
+
+
+
+
+## Database migration
+
+This section uses the MySQL database as an example to describe how to migrate a database.
+
+### Public network migration
+
+Public network migration involves two databases: the source database and the target database. The source database is the data source for migration, and the target database is the data destination for migration. The following introduction demonstrates the local database as the source database.
+
+1. Enter the target database and connect to the target database:
+
+
+
+2. Create the corresponding database in the terminal interface (skip this step if the corresponding database already exists):
+
+
+```bash
+# Example of creating a database sql statement:
+$ create database testmysql;
+```
+
+3. Enter the public network migration page of the target database, click the Migrate button:
+
+
+
+4. View migration configuration information:
+
+
+
+Copy the migration configuration information as follows:
+```bash
+# Example of an sql statement for setting configuration information:
+$ set global binlog_format=ROW;
+$ set binlog_row_image ='FULL';
+```
+
+5. Set the configuration information in the source database (MySQL and Postgres require manual configuration information, Mongo does not require configuration):
+```bash
+# xample of an sql statement for setting configuration information:
+$ set global binlog_format=ROW;
+$ set binlog_row_image ='FULL';
+```
+
+
+6. Enter the target database, fill in the migration information of the source database, which tables to migrate, and fill in which database in the target database to migrate:
+
+
+
+If you need to continuously migrate data from the source database, enable continuous migration in the advanced configuration as follows:
+
+
+
+7. View migration task information:
+
+
+
+8. Enter the target database, connect to the target database, and check whether the migration data is complete:
+
+
+
+### File migration
+
+1. Enter the target database and connect to the target database:
+
+
+
+2. Create the corresponding database in the terminal interface (skip this step if the corresponding database already exists):
+```bash
+# Example of creating a database sql statement:
+$ create database testmysql;
+```
+
+
+3. Upload the migration file, enter the database name, and start the migration:
+
+
+
+4. File migrating, Wait for the migration result:
+
+
+
+
+
+5. Connect to the database to check whether the migrated data is complete:
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_1.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_1.png
new file mode 100644
index 000000000000..1755f5622f18
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_1.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_2.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_2.png
new file mode 100644
index 000000000000..44917f8d0a63
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_2.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_3.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_3.png
new file mode 100644
index 000000000000..0ddf8b9aa425
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_3.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_4.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_4.png
new file mode 100644
index 000000000000..ef9cbf5e9829
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_4.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_5.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_5.png
new file mode 100644
index 000000000000..297b3965f89e
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_5.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_6.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_6.png
new file mode 100644
index 000000000000..aff13dcc9b0d
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_6.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_1.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_1.png
new file mode 100644
index 000000000000..7d7152c94a1d
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_1.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_2.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_2.png
new file mode 100644
index 000000000000..46f35589f728
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_2.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_3.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_3.png
new file mode 100644
index 000000000000..3b1c76fe6024
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_3.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_4.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_4.png
new file mode 100644
index 000000000000..eb861ab4b7f9
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_4.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_5.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_5.png
new file mode 100644
index 000000000000..408532d632ba
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_5.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_6.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_6.png
new file mode 100644
index 000000000000..92f075af1fb0
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_6.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_7.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_7.png
new file mode 100644
index 000000000000..c45553504155
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_7.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_1.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_1.png
new file mode 100644
index 000000000000..fc27c6626da6
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_1.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_10.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_10.png
new file mode 100644
index 000000000000..fc27c6626da6
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_10.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_11.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_11.png
new file mode 100644
index 000000000000..9ddbee463462
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_11.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_12.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_12.png
new file mode 100644
index 000000000000..3f814eee38e6
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_12.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_13.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_13.png
new file mode 100644
index 000000000000..137e4ea3d8d5
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_13.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_14.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_14.png
new file mode 100644
index 000000000000..b7a2267dd5eb
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_14.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_15.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_15.png
new file mode 100644
index 000000000000..3f814eee38e6
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_15.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_2.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_2.png
new file mode 100644
index 000000000000..9b2b39d5df8b
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_2.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_3.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_3.png
new file mode 100644
index 000000000000..c7f7411bd67f
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_3.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_4.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_4.png
new file mode 100644
index 000000000000..214e9f5477c3
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_4.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_5.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_5.png
new file mode 100644
index 000000000000..f86eeb99def4
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_5.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_6.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_6.png
new file mode 100644
index 000000000000..8c0949d5da92
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_6.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_7.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_7.png
new file mode 100644
index 000000000000..0f2814956c68
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_7.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_8.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_8.png
new file mode 100644
index 000000000000..d15a9d1ba453
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_8.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_9.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_9.png
new file mode 100644
index 000000000000..3f814eee38e6
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_9.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_1.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_1.png
new file mode 100644
index 000000000000..379a09ef5664
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_1.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_10.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_10.png
new file mode 100644
index 000000000000..544b065a2be5
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_10.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_11.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_11.png
new file mode 100644
index 000000000000..2ef27b4ecb4f
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_11.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_12.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_12.png
new file mode 100644
index 000000000000..c1519b2d62fb
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_12.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_2.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_2.png
new file mode 100644
index 000000000000..f83990d4edb8
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_2.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_3.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_3.png
new file mode 100644
index 000000000000..971e368049b5
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_3.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_4.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_4.png
new file mode 100644
index 000000000000..058b39327035
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_4.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_5.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_5.png
new file mode 100644
index 000000000000..333ffbed2bb5
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_5.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_6.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_6.png
new file mode 100644
index 000000000000..42acc0ad91f7
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_6.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_7.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_7.png
new file mode 100644
index 000000000000..63f79d1ac789
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_7.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_8.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_8.png
new file mode 100644
index 000000000000..b3ab09bf8c2e
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_8.png differ
diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_9.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_9.png
new file mode 100644
index 000000000000..40b2a7308f15
Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_9.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/1.png b/docs/archived/4.0/docs/guides/objectstorage/images/1.png
new file mode 100644
index 000000000000..703fe96200f9
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/1.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/10.png b/docs/archived/4.0/docs/guides/objectstorage/images/10.png
new file mode 100644
index 000000000000..e636144a8eeb
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/10.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/11.png b/docs/archived/4.0/docs/guides/objectstorage/images/11.png
new file mode 100644
index 000000000000..771f415a78f2
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/11.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/12.png b/docs/archived/4.0/docs/guides/objectstorage/images/12.png
new file mode 100644
index 000000000000..76c1487cf46e
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/12.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/13.png b/docs/archived/4.0/docs/guides/objectstorage/images/13.png
new file mode 100644
index 000000000000..5abeca82fcee
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/13.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/14.png b/docs/archived/4.0/docs/guides/objectstorage/images/14.png
new file mode 100644
index 000000000000..0c4f82ee2374
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/14.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/15.png b/docs/archived/4.0/docs/guides/objectstorage/images/15.png
new file mode 100644
index 000000000000..7884c768e0d3
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/15.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/16.png b/docs/archived/4.0/docs/guides/objectstorage/images/16.png
new file mode 100644
index 000000000000..3f76223396a7
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/16.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/17.png b/docs/archived/4.0/docs/guides/objectstorage/images/17.png
new file mode 100644
index 000000000000..21c2d258ae63
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/17.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/2.png b/docs/archived/4.0/docs/guides/objectstorage/images/2.png
new file mode 100644
index 000000000000..25d649f88c7c
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/2.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/3.png b/docs/archived/4.0/docs/guides/objectstorage/images/3.png
new file mode 100644
index 000000000000..176138f21cc5
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/3.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/4.png b/docs/archived/4.0/docs/guides/objectstorage/images/4.png
new file mode 100644
index 000000000000..acbf8f0358b1
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/4.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/5.png b/docs/archived/4.0/docs/guides/objectstorage/images/5.png
new file mode 100644
index 000000000000..52bb97ac5538
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/5.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/6.png b/docs/archived/4.0/docs/guides/objectstorage/images/6.png
new file mode 100644
index 000000000000..d9f63a24dd9a
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/6.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/7.png b/docs/archived/4.0/docs/guides/objectstorage/images/7.png
new file mode 100644
index 000000000000..b5a7d5ea362d
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/7.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/8.png b/docs/archived/4.0/docs/guides/objectstorage/images/8.png
new file mode 100644
index 000000000000..e0bb62a7b74b
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/8.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/9.png b/docs/archived/4.0/docs/guides/objectstorage/images/9.png
new file mode 100644
index 000000000000..f2284d0f79b9
Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/9.png differ
diff --git a/docs/archived/4.0/docs/guides/objectstorage/objectstorage.md b/docs/archived/4.0/docs/guides/objectstorage/objectstorage.md
new file mode 100644
index 000000000000..e6e0e365d43d
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/objectstorage/objectstorage.md
@@ -0,0 +1,189 @@
+---
+sidebar_position: 0
+---
+
+# Object Storage
+
+**Object Storage** is Sealos' built-in object storage service, which is primarily used to store and manage unstructured
+data.
+
+Currently, **Object Storage** has the following features:
+
+- Upload files to bucket
+- Download files from bucket
+- Expose the access permission of the bucket
+- Use SDK to access bucket
+- Monitors bucket resource metrics
+- Static host
+
+## Quick start
+
+### Upload files to bucket
+
+Go to Object Storage
+
+
+Create a bucket
+
+
+Set bucket name to test and permission to private
+
+
+Bucket is created successfully
+
+
+Upload file
+
+
+File uploaded successfully
+
+
+### Expose the access permission of the bucket
+
+Click the Edit button
+
+
+Set Bucket Permission to publicRead and click the Application button
+
+
+Copy file link
+
+
+Paste to browser address bar to access files
+
+
+### View the access key configuration
+
+An Object Storage user consists of a unique access key (username) and corresponding secret key (password). Internal is
+the internal access address of Object Storage, and External is the external access address of Object Storage.
+
+
+### Use SDK to access bucket
+
+The SDK requires three parameters to access bucket: AccessKey, SecretKey, and Endpoint (Internal or External). If the
+Region parameter is required, us-east-1 is used by default.
+
+#### Go Client SDK
+
+Detailed documentation reference: https://min.io/docs/minio/linux/developers/go/API.html
+
+Example: Use the Go Client SDK to upload the style.css file to the sv3dd7u4-test bucket, and set the endpoint to the
+external address. If the service is deployed in the K8s cluster, you can change the endpoint to the internal address.
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+)
+import "github.com/minio/minio-go/v7"
+import "github.com/minio/minio-go/v7/pkg/credentials"
+
+func main() {
+ endpoint := "objectstorageapi.xxx.xxx.xxx"
+ accessKey := "xxxxxxxx"
+ secretKey := "xxxxxxxxxxxxxxxx"
+ // init minio client
+ minioClient, err := minio.New(endpoint, &minio.Options{
+ Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
+ })
+ if err != nil {
+ log.Fatalln(err)
+ }
+ // get local file
+ file, err := os.Open("./style.css")
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ defer file.Close()
+
+ fileStat, err := file.Stat()
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ // put object
+ uploadInfo, err := minioClient.PutObject(context.Background(), "sv3dd7u4-test", "style.css", file, fileStat.Size(), minio.PutObjectOptions{ContentType: "text/css"})
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Println("Successfully uploaded bytes: ", uploadInfo)
+}
+```
+
+File uploaded successfully
+
+
+#### Java Client SDK
+
+Detailed documentation reference: https://min.io/docs/minio/linux/developers/java/API.html
+
+Example: Use the Java Client SDK to upload the style1.css file to the sv3dd7u4-test bucket, and set the endpoint to the
+external address. If the service is deployed in the K8s cluster, you can change the endpoint to the internal address.
+
+```xml
+
+
+ io.minio
+ minio
+ 8.5.9
+
+```
+
+```javascript
+package org.example;
+
+import io.minio.MinioClient;
+import io.minio.UploadObjectArgs;
+
+public class FileUploader {
+ public static void main(String[] args) throws Exception {
+
+ MinioClient minioClient =
+ MinioClient.builder()
+ .endpoint("https://objectstorageapi.xxx.xxx.xxx")
+ .credentials("xxxxxxxx", "xxxxxxxxxxxxxxxx")
+ .build();
+
+
+ minioClient.uploadObject(
+ UploadObjectArgs.builder()
+ .bucket("sv3dd7u4-test")
+ .object("style1.css")
+ .filename("src/main/java/org/example/style1.css")
+ .build());
+
+ System.out.println("Successfully uploaded bytes.");
+ }
+}
+```
+
+File uploaded successfully
+
+
+#### Omit other language SDK
+
+Detailed documentation reference: https://min.io/docs/minio/linux/developers/minio-drivers.html
+
+### Static host
+
+Create a permission for publicRead/publicReadwrite bucket, click Enable Hosting.
+
+
+Click on the address to jump to access.
+
+
+Click Custom Domain.
+
+
+In the Update area of App Launchpad, you can customize the domain.
+
+
+
+
+
diff --git a/docs/archived/4.0/docs/guides/templates/images/templates.png b/docs/archived/4.0/docs/guides/templates/images/templates.png
new file mode 100644
index 000000000000..ccfc2aace37c
Binary files /dev/null and b/docs/archived/4.0/docs/guides/templates/images/templates.png differ
diff --git a/docs/archived/4.0/docs/guides/templates/submit-template.md b/docs/archived/4.0/docs/guides/templates/submit-template.md
new file mode 100644
index 000000000000..5fd6ebcd1d70
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/templates/submit-template.md
@@ -0,0 +1,9 @@
+---
+sidebar_position: 2
+---
+
+# Submit template
+
+Every template in the Sealos template marketplace is directly and continuously updated from the [Sealos Template Repository](https://github.com/labring-actions/templates). For those interested in contributing new templates, the process involves submitting a Pull Request (PR) to this repository.
+
+To craft a new template, reference is available in the form of the [template.yaml](https://github.com/labring-actions/templates/blob/main/template.yaml) file. The system is equipped with a variety of common environment variables and functions that are accessible during the template development process. These integrated features enable the use of syntax akin to `GitHub Actions`. For instance, environment variables such as `${{ SEALOS_NAMESPACE }}` can be utilized to configure specific parameters in the template. Comprehensive details about these built-in environment variables are available in the [Template Guidelines](https://github.com/labring-actions/templates/blob/main/example.md).
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/guides/templates/templates.md b/docs/archived/4.0/docs/guides/templates/templates.md
new file mode 100644
index 000000000000..ae2fec02015d
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/templates/templates.md
@@ -0,0 +1,17 @@
+---
+sidebar_position: 0
+---
+
+# Templates
+
+import Highlight from '@site/src/components/Highlight'
+
+
+
+Sealos's [templates](https://template.cloud.sealos.io/) offers an array of pre-designed templates, ideal for quickly setting up and launching websites and a range of applications. This marketplace hosts a variety of template types, including blogs, AI applications, low-code solutions, cloud storage, IM applications, and middleware. These templates aim to make the development process more efficient, allowing developers to rapidly initiate and deploy projects without the need to build a website from scratch or concern themselves with the intricacies of application dependencies.
+
+Contrasting with Vercel's template marketplace, Sealos provides a different range of applications and functionalities. Vercel primarily targets front-end project deployment, offering templates that enhance website interfaces and user interactions. However, Vercel does not offer persistent data storage, resulting in data being temporary and lost upon application restart.
+
+In contrast, Sealos's application templates encompass both front-end and back-end projects, along with a variety of other applications. Significantly, Sealos includes support for persistent storage, an essential aspect for applications that require extensive data storage or need to maintain data continuity. This is particularly critical for applications like e-commerce websites, large-scale social media platforms, and enterprise-level applications, where data persistence is a key requirement.
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/guides/terminal/images/database-terminal-1.png b/docs/archived/4.0/docs/guides/terminal/images/database-terminal-1.png
new file mode 100644
index 000000000000..f1fcc577c7f2
Binary files /dev/null and b/docs/archived/4.0/docs/guides/terminal/images/database-terminal-1.png differ
diff --git a/docs/archived/4.0/docs/guides/terminal/images/database-terminal.png b/docs/archived/4.0/docs/guides/terminal/images/database-terminal.png
new file mode 100644
index 000000000000..01dba426f6b0
Binary files /dev/null and b/docs/archived/4.0/docs/guides/terminal/images/database-terminal.png differ
diff --git a/docs/archived/4.0/docs/guides/terminal/images/nginx-terminal-1.png b/docs/archived/4.0/docs/guides/terminal/images/nginx-terminal-1.png
new file mode 100644
index 000000000000..ecb9443c9621
Binary files /dev/null and b/docs/archived/4.0/docs/guides/terminal/images/nginx-terminal-1.png differ
diff --git a/docs/archived/4.0/docs/guides/terminal/images/nginx-terminal.png b/docs/archived/4.0/docs/guides/terminal/images/nginx-terminal.png
new file mode 100644
index 000000000000..c8fa6b4ae9f9
Binary files /dev/null and b/docs/archived/4.0/docs/guides/terminal/images/nginx-terminal.png differ
diff --git a/docs/archived/4.0/docs/guides/terminal/images/terminal-pod.png b/docs/archived/4.0/docs/guides/terminal/images/terminal-pod.png
new file mode 100644
index 000000000000..d9aada4d4da3
Binary files /dev/null and b/docs/archived/4.0/docs/guides/terminal/images/terminal-pod.png differ
diff --git a/docs/archived/4.0/docs/guides/terminal/images/terminal.webp b/docs/archived/4.0/docs/guides/terminal/images/terminal.webp
new file mode 100644
index 000000000000..d1daf72539d7
Binary files /dev/null and b/docs/archived/4.0/docs/guides/terminal/images/terminal.webp differ
diff --git a/docs/archived/4.0/docs/guides/terminal/terminal.md b/docs/archived/4.0/docs/guides/terminal/terminal.md
new file mode 100644
index 000000000000..3962b3c16bf6
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/terminal/terminal.md
@@ -0,0 +1,86 @@
+---
+sidebar_position: 4
+---
+
+# Terminal
+
+Integrated within the [Sealos](https://cloud.sealos.io) Cloud Operating System is the Terminal, an application of profound potency. Mirroring the terminal in standalone operating systems, it facilitates users to interact directly with the operating system, executing various operations via command entry.
+
+
+
+The value of the Terminal rests in its efficiency, flexibility, and powerful capabilities. For a plethora of tasks, employing the Terminal proves swifter and more concise than utilizing a Graphical User Interface (GUI). It is an indispensable instrument for developers, permitting users to gain a profound understanding and control of the Sealos Cloud Operating System.
+
+**Every user's Terminal is in fact operating within a Pod of Kubernetes**, which can be observed in the Terminal using the following command:
+
+
+
+## Accessing the Terminal of an Application Container
+
+One may directly access the Terminal of any application container via the Terminal App. Suppose you have deployed an application such as Nginx via application management, one can navigate directly to the Nginx application's detail page, click on the three dots on the right side of the details, and then click on 'Terminal' to access the Terminal of the Nginx application.
+
+
+
+
+
+## 终端一键直连数据库
+
+It is also possible to establish a direct connection to the database created in the[Database App](../dbprovider/dbprovider.md) with a single click via the Terminal.
+
+Navigate to the database details page and click on '一键连接' on the left:
+
+
+
+This will take you directly to the Terminal App and connect to the database:
+
+
+
+## Direct Manipulation of Kubernetes Resources via Terminal
+
+:::danger
+
+The following operations necessitate foundational knowledge of Kubernetes and container-related concepts, avoid casual execution without this understanding.
+
+:::
+
+Veteran Kubernetes users can utilize the Terminal to manipulate Kubernetes resources directly, affording many advantages for such seasoned users:
+
++ Enables batch operations and automation
++ Offers high flexibility
++ Allows in-depth troubleshooting
+
+Below are examples of common operations with Kubernetes resources within the Terminal:
+
++ View the list of user's Pods
+
+```bash
+kubectl get pods
+```
+
++ Create and manage resource
+
+```bash
+# Create resources using YAML or JSON files
+$ kubectl create -f
+
+# Create or update resources using YAML or JSON files
+$ kubectl apply -f
+
+# Delete specified resource type and name
+$ kubectl delete
+
+# View the status of a specified Deployment's rolling update
+$ kubectl rollout status deployment/
+```
+
++ Troubleshooting
+
+```bash
+# Obtain detailed information about a specific resource, including events and status
+$ kubectl describe
+
+# Obtain logs of a specific container in a specific Pod
+$ kubectl logs -c
+
+# Open a terminal in a specific Pod for debugging purposes
+$ kubectl exec -it
+```
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/guides/workspace/images/accept-invite.gif b/docs/archived/4.0/docs/guides/workspace/images/accept-invite.gif
new file mode 100644
index 000000000000..404c7bde776a
Binary files /dev/null and b/docs/archived/4.0/docs/guides/workspace/images/accept-invite.gif differ
diff --git a/docs/archived/4.0/docs/guides/workspace/images/create-workspace.gif b/docs/archived/4.0/docs/guides/workspace/images/create-workspace.gif
new file mode 100644
index 000000000000..8be2cb426d82
Binary files /dev/null and b/docs/archived/4.0/docs/guides/workspace/images/create-workspace.gif differ
diff --git a/docs/archived/4.0/docs/guides/workspace/images/invite-user.gif b/docs/archived/4.0/docs/guides/workspace/images/invite-user.gif
new file mode 100644
index 000000000000..0de5fbabc405
Binary files /dev/null and b/docs/archived/4.0/docs/guides/workspace/images/invite-user.gif differ
diff --git a/docs/archived/4.0/docs/guides/workspace/images/switch-workspace.gif b/docs/archived/4.0/docs/guides/workspace/images/switch-workspace.gif
new file mode 100644
index 000000000000..1091cfc7ad9c
Binary files /dev/null and b/docs/archived/4.0/docs/guides/workspace/images/switch-workspace.gif differ
diff --git a/docs/archived/4.0/docs/guides/workspace/worksapce.md b/docs/archived/4.0/docs/guides/workspace/worksapce.md
new file mode 100644
index 000000000000..68b46db27da9
--- /dev/null
+++ b/docs/archived/4.0/docs/guides/workspace/worksapce.md
@@ -0,0 +1,55 @@
+---
+sidebar_position: 5
+---
+
+# Team Collaboration and Workspace
+
+## Term Definitions
+
+### Workspace
+
+Workspace is a core feature of the Sealos Cloud Operating System, implemented based on Kubernetes namespaces. It serves
+as a multi-tenant resource isolation mechanism, allowing the partitioning of a Kubernetes cluster into multiple
+workspaces. Each workspace has its own resource quotas and permissions, enabling the allocation of different users to
+distinct workspaces for resource isolation and permission control.
+
+Personal workspace is a special form of workspace. Each user has a personal workspace with a name identical to their
+username. The resource quotas and permissions of personal workspaces are the same as regular workspaces. However,
+personal workspaces cannot be deleted, and other users cannot be added to them.
+
+### Roles and Permissions
+
+Users within a workspace can have different roles, each with its own set of permissions. Currently, Sealos Cloud
+Operating System includes the following roles: Owner, Manager, and Developer.
+
++ Owner: Possesses all permissions within the workspace, including deleting the workspace, viewing/creating/modifying
+ all resources within the workspace, and inviting users to join the workspace as administrators/developers.
++ Manager: Possesses managerial permissions within the workspace, such as viewing/creating/modifying all resources
+ within the workspace and inviting users to join as developers.
++ Developer: Possesses development permissions within the workspace, including viewing the status of resources within
+ the workspace.
+
+## Quick Start
+
+### Create Workspace
+
+
+
+### Switch Workspace
+
+
+
+### Invite Users to Join Workspace
+
+Invite users to join the workspace using their user ID. The invited users can choose to accept or decline the
+invitation. If accepted, the invited users become members of the workspace with resource permissions.
+
+
+
+### Accept Invitation
+
+When a user is invited to join a workspace, they can view the invitation message in the management panel. By clicking on
+the invitation message, they can see the details and accept the invitation. After acceptance, the user becomes a member
+of the workspace with resource permissions.
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/images/sealos-desktop.webp b/docs/archived/4.0/docs/images/sealos-desktop.webp
new file mode 100644
index 000000000000..8a1e98ad2d87
Binary files /dev/null and b/docs/archived/4.0/docs/images/sealos-desktop.webp differ
diff --git a/docs/archived/4.0/docs/msa/privacy-policy.md b/docs/archived/4.0/docs/msa/privacy-policy.md
new file mode 100644
index 000000000000..fdf4525f71cf
--- /dev/null
+++ b/docs/archived/4.0/docs/msa/privacy-policy.md
@@ -0,0 +1,59 @@
+---
+sidebar_position: 2
+---
+
+# Sealos Cloud Privacy Policy
+
+We prioritize your privacy immensely. This policy details the collection, use, disclosure, and protection of your personal information when you utilize our services. Please thoroughly understand its contents.
+
+**Information Collection**
+
+1. We may gather your personal details like name, phone number, email, and address during registration or service usage.
+2. We also collect data produced during service use, such as operational logs, IP addresses, and device types.
+3. For enhanced user experience, technologies like cookies may be employed to store information related to your service interaction.
+
+**Information Usage**
+
+1. The handling of your personal data adheres to legal regulations and user agreements.
+2. We utilize the information for improving service quality, innovating new products, and other similar purposes.
+3. Collected data might be used for delivering service-related notifications and advertisements.
+
+**Disclosure of Information**
+
+1. Your personal data will not be shared with third parties, except under these circumstances:
+
+ 1. With your prior agreement;
+ 2. When legally or regulatorily required;
+ 3. To uphold our or other users' legal rights.
+
+2. We may share your information with affiliated entities or partners, ensuring data security through strict confidentiality.
+
+**Protection of Information**
+
+1. We apply various security measures, like encryption and access control, to prevent unauthorized data access, use, or disclosure.
+2. Regular safety assessments are conducted on stored personal data to guarantee its security.
+3. Should a data leak or similar security incident occur, we'll immediately implement emergency measures and inform you as required by law.
+
+**User Rights**
+
+1. You have complete authority to access, correct, or delete your personal data at any time.
+2. Opting out of data collection can limit your access to certain service features.
+3. You may request a halt in processing your personal data, which might affect service continuation.
+
+**Privacy Policy Updates**
+
+1. We may periodically update this policy, and any changes will be published on our service page. Continuing service use implies acceptance of any updated terms.
+2. Regularly reviewing this policy is encouraged to stay informed about your data protection.
+
+**Minor Protection**
+
+Minors' data protection is crucial to us. If you're a minor, please engage with our services under guardian supervision, ensuring proper data management.
+
+**Cross-border Data Transfer**
+
+Considering our servers' diverse locations, your data may be transferred and processed internationally. We assure that cross-border data remains securely protected.
+
+**Contact Us**
+
+1. For any questions, suggestions, or complaints about this policy, please reach us via email fanux@sealos.io
+2. We are committed to quickly resolving any issues you raise.
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/msa/terms-of-service.md b/docs/archived/4.0/docs/msa/terms-of-service.md
new file mode 100644
index 000000000000..718b60d41c64
--- /dev/null
+++ b/docs/archived/4.0/docs/msa/terms-of-service.md
@@ -0,0 +1,68 @@
+---
+sidebar_position: 1
+---
+
+# Sealos Cloud Terms of Service
+
+These Sealos Cloud Terms of Service (this “Agreement”) are entered into by and between you and Zhuhai Huanjie Cloud Computing Co., Ltd. (referred to as “we/us” or "the Company") regarding the use of our cloud services (hereafter referred to as "the Service"). We urge you to read and understand every clause, particularly those concerning limitations of our liability, restrictions on your rights, and the terms about dispute resolution and legal jurisdiction. If any aspect of this agreement is not acceptable to you, refrain from registering for or using the Service.
+
+**Article 1: Scope of Services**
+
+1. We offer a range of internet-based IT services, including but not limited to storage, computation, and network transmission.
+2. Technical support and customer assistance will be provided to enhance your experience with the Service.
+3. We reserve the right to modify, suspend, or discontinue any or all aspects of the Service without any obligation to you.
+
+**Article 2: Account Registration and Management**
+
+1. Prior to using the Service, registering an account is mandatory. You must ensure that the information provided at registration is accurate and updated regularly.
+2. The security of your account credentials is your responsibility. Any misuse of your account should be reported to us immediately.
+3. We hold the right to inspect your account and, if irregularities are detected, may halt or terminate the services provided to you.
+
+**Article 3: Usage Guidelines**
+
+1. The Service must not be used for illegal activities or in ways that infringe upon the rights of others, including intellectual property rights and confidentiality breaches.
+2. The Service must not be used for malicious registration of accounts, whether for profit, speculation, or other purposes.
+3. Dissemination of illegal, harmful, or malevolent content via the Service is strictly prohibited.
+4. You are liable for adhering to all relevant laws and the terms of this agreement, including the responsibility for the content shared and outcomes arising from your use of the Service.
+
+**Article 4: Fees and Payment**
+
+1. You agree to the fee structure associated with the Service, as outlined in our published rates.
+2. We may revise the fee structure based on market conditions, notifying you in advance. Continued use of the Service implies agreement to the revised fees.
+3. Non-payment of fees may lead to the suspension or termination of the Service, and we retain the right to seek compensation.
+
+**Article 5: Disclaimer and Limitation of Liability**
+
+1. The Service is provided based on existing technology and conditions, and we do not guarantee uninterrupted, completely secure, or error-free service.
+2. We are not liable for Service disruptions or unavailability due to force majeure.
+3. We bear no responsibility for loss or damage of data caused by your actions.
+
+**Article 6: Privacy Policy**
+
+Your agreement to and compliance with our privacy policy, an integral part of this agreement, is required. Please refer to the detailed "Privacy Policy" section below.
+
+**Article 7: Intellectual Property Rights**
+
+1. The intellectual property of the Service and all related materials, including software and documentation, is owned by us. Unauthorized reproduction, distribution, or reverse engineering is prohibited.
+2. While you retain intellectual property rights over your data and content generated within the Service, you grant us permission for necessary operations such as storage and backup to facilitate the Service.
+
+**Article 8: Agreement Termination**
+
+1. Violation of any terms of this agreement permits us to cease providing the Service immediately and to pursue legal remedies.
+2. You can terminate this agreement by requesting the cancellation of your account.
+
+**Article 9: Dispute Resolution and Governing Law**
+
+1. This agreement is governed by the laws of the People's Republic of China. Disputes arising from this agreement should be resolved through negotiation; failing that, legal action may be pursued in a competent court.
+2. The agreement includes provisions for resolving disputes and the applicable legal framework.
+
+**Article 10: Network Security**
+
+1. You bear the consequences for any services provided through the platform that violate national laws.
+2. You are responsible for overseeing the services you offer and bear consequences for any illegal activities conducted by your users.
+3. Should we discover your involvement in illegal activities, legal action will be taken and evidence provided to the authorities.
+
+**Article 11: Additional Provisions**
+
+1. Should parts of this agreement become invalid due to legal conflicts, it does not affect the validity of the remaining terms.
+2. The Company retains the ultimate interpretive authority over this agreement and the privacy policy. For inquiries, please contact us.
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/quick-start/images/SCR-20230529-sokx.png b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-sokx.png
new file mode 100644
index 000000000000..834cb08f40d3
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-sokx.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/SCR-20230529-tlpi.png b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-tlpi.png
new file mode 100644
index 000000000000..b904addd338a
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-tlpi.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/SCR-20230529-tpsc.jpg b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-tpsc.jpg
new file mode 100644
index 000000000000..8e477e550a4d
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-tpsc.jpg differ
diff --git a/docs/archived/4.0/docs/quick-start/images/SCR-20230529-ualx.jpg b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-ualx.jpg
new file mode 100644
index 000000000000..49ab8b95ae32
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-ualx.jpg differ
diff --git a/docs/archived/4.0/docs/quick-start/images/SCR-20230529-ubqj.jpg b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-ubqj.jpg
new file mode 100644
index 000000000000..5f1da8a47b4b
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-ubqj.jpg differ
diff --git a/docs/archived/4.0/docs/quick-start/images/app-deployment-2.png b/docs/archived/4.0/docs/quick-start/images/app-deployment-2.png
new file mode 100644
index 000000000000..7154f775b5d2
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-deployment-2.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/app-deployment-3.png b/docs/archived/4.0/docs/quick-start/images/app-deployment-3.png
new file mode 100644
index 000000000000..2c57fd60b2e2
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-deployment-3.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/app-deployment.png b/docs/archived/4.0/docs/quick-start/images/app-deployment.png
new file mode 100644
index 000000000000..3d3819f431d0
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-deployment.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/app-launchpad-1.jpg b/docs/archived/4.0/docs/quick-start/images/app-launchpad-1.jpg
new file mode 100644
index 000000000000..1b6652464f9a
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-launchpad-1.jpg differ
diff --git a/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain.png b/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain.png
new file mode 100644
index 000000000000..013dfd23003e
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain2.png b/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain2.png
new file mode 100644
index 000000000000..840e42c1e952
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain2.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain3.png b/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain3.png
new file mode 100644
index 000000000000..7ff6bfd47d46
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain3.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/app-launchpad.jpg b/docs/archived/4.0/docs/quick-start/images/app-launchpad.jpg
new file mode 100644
index 000000000000..1a64b79192e3
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-launchpad.jpg differ
diff --git a/docs/archived/4.0/docs/quick-start/images/app-list-2.png b/docs/archived/4.0/docs/quick-start/images/app-list-2.png
new file mode 100644
index 000000000000..355cd126eec3
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-list-2.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/app-list.png b/docs/archived/4.0/docs/quick-start/images/app-list.png
new file mode 100644
index 000000000000..a9461aae6b7e
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-list.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/app-nginx.png b/docs/archived/4.0/docs/quick-start/images/app-nginx.png
new file mode 100644
index 000000000000..79a1815c53e4
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-nginx.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/app-url.png b/docs/archived/4.0/docs/quick-start/images/app-url.png
new file mode 100644
index 000000000000..39af8e02421d
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-url.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/database-configure.png b/docs/archived/4.0/docs/quick-start/images/database-configure.png
new file mode 100644
index 000000000000..df6a0d91db4d
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/database-configure.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/database-connect.png b/docs/archived/4.0/docs/quick-start/images/database-connect.png
new file mode 100644
index 000000000000..5155d12912b3
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/database-connect.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/database-creating.png b/docs/archived/4.0/docs/quick-start/images/database-creating.png
new file mode 100644
index 000000000000..6589431fcd38
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/database-creating.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/database-launch.jpg b/docs/archived/4.0/docs/quick-start/images/database-launch.jpg
new file mode 100644
index 000000000000..83800e085969
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/database-launch.jpg differ
diff --git a/docs/archived/4.0/docs/quick-start/images/database-more.png b/docs/archived/4.0/docs/quick-start/images/database-more.png
new file mode 100644
index 000000000000..206a8a9b453f
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/database-more.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/database-select.png b/docs/archived/4.0/docs/quick-start/images/database-select.png
new file mode 100644
index 000000000000..fe8ba2991ff1
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/database-select.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/deploy-template.png b/docs/archived/4.0/docs/quick-start/images/deploy-template.png
new file mode 100644
index 000000000000..4c3aa7f6f9fd
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/deploy-template.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/multi-ports1.png b/docs/archived/4.0/docs/quick-start/images/multi-ports1.png
new file mode 100644
index 000000000000..74e5bd569bf1
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/multi-ports1.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/nginx.png b/docs/archived/4.0/docs/quick-start/images/nginx.png
new file mode 100644
index 000000000000..14adf1fcc771
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/nginx.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/template-detail.png b/docs/archived/4.0/docs/quick-start/images/template-detail.png
new file mode 100644
index 000000000000..ddd16e9fc873
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/template-detail.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/templates-2.png b/docs/archived/4.0/docs/quick-start/images/templates-2.png
new file mode 100644
index 000000000000..94c601d43930
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/templates-2.png differ
diff --git a/docs/archived/4.0/docs/quick-start/images/templates.jpg b/docs/archived/4.0/docs/quick-start/images/templates.jpg
new file mode 100644
index 000000000000..f52b412f94e5
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/templates.jpg differ
diff --git a/docs/archived/4.0/docs/quick-start/images/terminal.png b/docs/archived/4.0/docs/quick-start/images/terminal.png
new file mode 100644
index 000000000000..34dc2ed40a28
Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/terminal.png differ
diff --git a/docs/archived/4.0/docs/quick-start/next-steps.md b/docs/archived/4.0/docs/quick-start/next-steps.md
new file mode 100644
index 000000000000..13028e9351cd
--- /dev/null
+++ b/docs/archived/4.0/docs/quick-start/next-steps.md
@@ -0,0 +1,7 @@
+---
+sidebar_position: 5
+---
+
+# Next Steps
+
+Congratulations on getting started with Sealos! Now, you can continue learning more about Sealos's many features:
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/quick-start/use-app-launchpad.md b/docs/archived/4.0/docs/quick-start/use-app-launchpad.md
new file mode 100644
index 000000000000..e5d19ee02ad2
--- /dev/null
+++ b/docs/archived/4.0/docs/quick-start/use-app-launchpad.md
@@ -0,0 +1,31 @@
+---
+sidebar_position: 2
+---
+
+# Use App Launchpad
+
+Sealos desktop is similar to that of PC operating systems, such as macOS or Windows. Each icon on the desktop represents an app, similar to the shortcuts in Windows. Like PC operating systems, Sealos also comes with built-in apps. To quickly install applications on this cloud operating system, you need to use the system's built-in **App Launchpad**.
+
+> For more information about application management, please refer to [Introduction to App Launchpad](/guides/applaunchpad/applaunchpad.md)
+
+This guide will show you how to quickly install some common applications on the Sealos cloud operating system.
+
+## Deploy nginx
+
+First, open App Launchpad in the [Sealos](https://cloud.sealos.io) desktop.
+
+
+
+Click on "New Application".
+
+Enter the application name and image name in sequence, enable external network access, and then click "Deploy Application" to proceed.
+
+
+
+Once the deployment is finished, click "Details" to enter the application's detailed view.
+
+you can view the operational metrics of the Nginx application, including CPU and memory usage. Click on the external link to open the Nginx interface directly via the external domain name.
+
+
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/quick-start/use-database.md b/docs/archived/4.0/docs/quick-start/use-database.md
new file mode 100644
index 000000000000..f7b5bbec80bd
--- /dev/null
+++ b/docs/archived/4.0/docs/quick-start/use-database.md
@@ -0,0 +1,33 @@
+---
+sidebar_position: 4
+---
+
+# Use Database
+
+Databases are fundamental tools for data management, designed for efficient storage and access. Sealos offers a user-friendly database interface, eliminating the complexities of command-line operations. This assists in managing an array of databases, such as relational databases, NoSQL, vector databases, and streaming databases. With just a few clicks in the "[Database](/guides/dbprovider/dbprovider.md)" app, you can seamlessly create a variety of databases, including MySQL, PostgreSQL, MongoDB, Redis, and many more.
+
+This guide will show you how to install MySQL use Databse app.
+
+## Install MySQL
+
+1. Open Database app in the [Sealos](https://cloud.sealos.io) desktop.
+
+
+
+2. Click on 'Create Database' and finalize the basic configuration.
+
+
+
+3. Click on 'Deploy' then click 'Confirm' to wait for the successful creation of the MySQL.
+
+
+
+4. Click on 'connect', which will directly launch the "Terminal" application, leading you to the database command line.
+
+
+
+## Install PostgreSQL and MongoDB
+
+The installation process for PostgreSQL and MongoDB mirrors that of MySQL. Simply choose the respective database type on the basic configuration page.
+
+
diff --git a/docs/archived/4.0/docs/quick-start/use-template.md b/docs/archived/4.0/docs/quick-start/use-template.md
new file mode 100644
index 000000000000..a86926811112
--- /dev/null
+++ b/docs/archived/4.0/docs/quick-start/use-template.md
@@ -0,0 +1,34 @@
+---
+sidebar_position: 1
+---
+
+# Use Template
+
+This guide will show you how to use templates to fast-track applications deployment. and maximize Sealos's features.
+
+## 1. Find a template
+
+Start by accessing the "**Templates**" in the Sealos desktop.
+
+
+
+select the template you’d like to deploy
+
+
+
+Not sure which one to use? How about [exploring FastGPT](/examples/ai-applications/install-fastgpt-on-desktop.md)
+
+## 2. Deploy the template to Sealos
+
+Once you've selected a template, configure the necessary parameters, then click **Deploy Application** on the template page to start deployment.
+
+
+
+Once deployment concludes, click "Confirm" to navigate to the application's details.
+
+
+
+Wait for the application's status to switch to running. Subsequently, click on the external link to launch the application's Web interface directly through the external domain name.
+
+
+
diff --git a/docs/archived/4.0/docs/quick-start/use-terminal.md b/docs/archived/4.0/docs/quick-start/use-terminal.md
new file mode 100644
index 000000000000..3596109beb6d
--- /dev/null
+++ b/docs/archived/4.0/docs/quick-start/use-terminal.md
@@ -0,0 +1,161 @@
+---
+sidebar_position: 3
+---
+
+# Use Terminal
+
+Users familiar with Kubernetes and container fundamentals may opt to deploy applications using the **[Terminal](/guides/terminal/terminal.md)** app. This guide will show you how to use the terminal for deployment, using Nginx as an example.
+
+Start by launching the Terminal app within the [Sealos](https://cloud.sealos.io) desktop.
+
+
+
+Upon opening the Terminal app, a terminal window is automatically allocated, enabling you to execute various command-line operations.
+
+
+
+① Begin by creating a deployment manifest for Nginx Deployment:
+
+```yaml
+# deployment.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nginx
+ labels:
+ app: nginx
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: nginx
+ template:
+ metadata:
+ labels:
+ app: nginx
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:1.14.2
+ ports:
+ - containerPort: 80
+ resources:
+ requests:
+ cpu: 0.2
+ memory: 32Mi
+ limits:
+ cpu: 0.2
+ memory: 32Mi
+```
+
+② Subsequently, create a deployment manifest for Nginx Service:
+
+```yaml
+# service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ name: nginx
+ labels:
+ app: nginx
+spec:
+ type: ClusterIP
+ ports:
+ - port: 80
+ targetPort: 80
+
+ selector:
+ app: nginx
+```
+
+③ Deploy both the Deployment and Service by executing the following:
+
+```bash
+kubectl apply -f deployment.yaml
+kubectl apply -f service.yaml
+```
+
+Verify Nginx's accessibility:
+
+```bash
+$ kubectl get pod -l app=nginx
+NAME READY STATUS RESTARTS AGE
+nginx-7546c75fb4-572x9 1/1 Running 0 5m37s
+
+$ kubectl get svc -l app=nginx
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+nginx ClusterIP 10.64.204.173 80/TCP 5m36s
+
+$ curl 10.64.204.173
+
+
+
+Welcome to nginx!
+
+
+
+Welcome to nginx!
+If you see this page, the nginx web server is successfully installed and
+working. Further configuration is required.
+
+For online documentation and support please refer to
+nginx.org.
+Commercial support is available at
+nginx.com.
+
+Thank you for using nginx.
+
+
+```
+
+④ To expose Nginx to the internet and access it via a public domain, create an Ingress resource. Start by creating a deployment manifest:
+
+```yaml
+# ingress.yaml
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ annotations:
+ kubernetes.io/ingress.class: nginx
+ nginx.ingress.kubernetes.io/backend-protocol: HTTP
+ nginx.ingress.kubernetes.io/client-body-buffer-size: 64k
+ nginx.ingress.kubernetes.io/proxy-body-size: 32m
+ nginx.ingress.kubernetes.io/proxy-buffer-size: 64k
+ nginx.ingress.kubernetes.io/server-snippet: |
+ client_header_buffer_size 64k;
+ large_client_header_buffers 4 128k;
+ nginx.ingress.kubernetes.io/ssl-redirect: "false"
+ name: nginx
+spec:
+ rules:
+ - host: nginx.cloud.sealos.io
+ http:
+ paths:
+ - backend:
+ service:
+ name: nginx
+ port:
+ number: 80
+ path: /
+ pathType: Prefix
+ tls:
+ - hosts:
+ - nginx.cloud.sealos.io
+ secretName: wildcard-cloud-sealos-io-cert
+```
+
+⑤ Deploy the Ingress resource:
+
+```bash
+$ kubectl apply -f ingress.yaml
+```
+
+⑥ Confirm the Nginx's accessibility through the public domain:
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/QA.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/QA.md
new file mode 100644
index 000000000000..7e213a92ead8
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/QA.md
@@ -0,0 +1,185 @@
+---
+sidebar_position: 1
+---
+
+# Frequently Asked Questions
+
+When using Sealos, you may encounter some common questions and issues. Here are answers and solutions to some of the common problems.
+
+## Image Building Issues
+
+### Q1: How to set up a proxy service during the build phase?
+
+During the execution of the build command, you can configure a proxy service by setting the HTTP_PROXY environment variable.
+
+```shell
+HTTP_PROXY=socket5://127.0.0.1:7890 sealos build xxxxx
+```
+
+### Q2: How to enable debug logs for buildah?
+
+To view debug logs for buildah, you can set the `BUILDAH_LOG_LEVEL` environment variable.
+
+```shell
+BUILDAH_LOG_LEVEL=debug sealos images
+```
+
+### Q3: How to execute Sealos build within a Pod?
+
+If you want to execute Sealos build within a Pod, follow these steps:
+
+1. Build the image within the Pod. You can create a Deployment with the following YAML configuration:
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ labels:
+ app: sealoscli
+ name: sealoscli
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: sealoscli
+ strategy: {}
+ template:
+ metadata:
+ labels:
+ app: sealoscli
+ spec:
+ containers:
+ - image: # Replace with your sealos image
+ name: sealoscli
+ stdin: true
+ stdinOnce: true
+ securityContext:
+ privileged: true
+```
+
+2. Create a Dockerfile. Here's an example that you can modify as per your needs:
+
+```dockerfile
+FROM bitnami/minideb:buster
+
+ARG TARGETOS
+ARG TARGETARCH
+
+LABEL from=bitnami/minideb:buster platform=rootcloud team=oam tag=buster name=base
+
+RUN sed -i "s@http://deb.debian.org@http://mirrors.aliyun.com@g" /etc/apt/sources.list && sed -i "s@http://security.debian.org@http://mirrors.aliyun.com/debian-security@g" /etc/apt/sources.list
+RUN install_packages curl iputils-ping net-tools telnet procps vim wget jq
+
+ENV LANG=C.UTF-8
+ENV LANGUAGE=C.UTF-8
+ENV LC_ALL=C.UTF-8
+ENV TZ=Asia/Shanghai
+```
+
+3. Execute the build command within the Pod.
+
+```shell
+sealos build --arch arm64 --build-arg TARGETOS=linux --build-arg TARGETARCH=arm64 -t test -f Dockerfile .
+```
+
+### Q4: How to Build Cluster Images Using Other Build Tools?
+
+If you want to use other container tools like Docker or Podman to build cluster images, you can utilize [sreg](https://github.com/labring/sreg) to cache the images.
+
+Follow these steps:
+
+1. Install sreg:
+ ```shell
+ wget https://github.com/labring/sreg/releases/download/v0.1.1/sreg_0.1.1_linux_amd64.tar.gz
+ tar -xzf sreg_0.1.1_linux_amd64.tar.gz sreg
+ mv sreg /usr/bin/
+ ```
+2. Cache the images:
+ ```shell
+ sreg save --registry-dir=registry .
+ ```
+3. Build the cluster image:
+ ```shell
+ docker build -t xxxx -f Sealfile .
+ ```
+
+### Q5: Encounter the error "lgetxattr /var/lib/containers/storage/overlay/0c2afe770ec7870ad4639f18a1b50b3a84718f95c8907f3d54e14dbf0a01d50d/merged/dev/ptmx: no such device" during Sealos build. How to fix it?
+
+This issue might be related to the version of `fuse-overlayfs`. We recommend downloading the latest version from [here](https://github.com/containers/fuse-overlayfs/releases) and replacing `/bin/fuse-overlayfs`.
+
+## Runtime Selection Issues
+
+### Q1: How to select the Kubernetes runtime?
+
+Sealos determines the runtime based on the image you choose. If you select the `kubernetes-docker` image, Sealos will use Docker as the runtime. If you choose the `kubernetes-crio` image, Sealos will use CRI-O as the runtime.
+
+## Version Compatibility Issues
+
+### Q1: Error "Applied to cluster error: failed to
+
+init exec auth.sh failed exit status 127"?
+
+This error is often caused by a mismatch between the version of Sealos and the version of the image being used. Make sure that the image version and the Sealos version are compatible. For example, if you are using a Kubernetes version like `v1.xx.x`, you may need to upgrade Sealos, especially if you are using an older version of Sealos while the Sealos cluster image is using the latest version. Another solution is to choose the corresponding version of the Sealos image. For example, if your Sealos version is 4.1.3, then the cluster image should be something like `kubernetes:v1.24.0-4.1.3`. Ensuring that the image version and Sealos version are compatible can help avoid such issues.
+
+### Q2: Error when adding additional domains or modifying the service CIDR in the cluster during the addition of a master node
+
+To address this issue, the Sealos team made the necessary fixes in version 4.2.0. You can refer to the specific fix and discussion in this pull request: [https://github.com/labring/sealos/pull/2943](https://github.com/labring/sealos/pull/2943).
+
+Therefore, if you encounter this problem, we recommend upgrading to Sealos version 4.2.0. The updated version should handle these changes correctly and not produce errors when adding a master node.
+
+## File and Directory Location Issues
+
+### Q1: How to modify the default storage location for `/root/.sealos`?
+
+If you need to change the default storage location, you can set the `SEALOS_RUNTIME_ROOT` environment variable and then run the Sealos command. It is recommended to set this environment variable globally so that it can be conveniently used in other commands or scenarios.
+
+```shell
+export SEALOS_RUNTIME_ROOT=/data/.sealos
+sealos run labring/kubernetes:v1.24.0
+```
+
+### Q2: How to modify the default storage location for `/var/lib/sealos`?
+
+If you need to change the default storage location, you can set the `SEALOS_DATA_ROOT` environment variable and then run the Sealos command. Similarly, it is recommended to set this environment variable globally.
+
+```shell
+export SEALOS_DATA_ROOT=/data/sealos
+sealos run labring/kubernetes:v1.24.0
+```
+
+### Q3: How to modify the storage paths for Sealos image data and status?
+
+> When using the Sealos cluster, you may need to change the default storage paths for image data and status data. By default, these data are stored at the locations defined in the `/etc/containers/storage.conf` file.
+
+1. **View the current storage configuration**
+ First, you can use the following command to view the current image storage configuration:
+ ```
+ sealos images --debug
+ ```
+ This command will print the file that contains the current storage configuration, for example:
+ ```
+ 2023-06-07T16:27:02 debug using file /etc/containers/storage.conf as container storage config
+ REPOSITORY TAG IMAGE ID CREATED SIZE
+ ```
+2. **Modify the storage path for image data**
+ If you want to change the storage path for image data, you can edit the `/etc/containers/storage.conf` file. In this file, find and modify the `graphroot` field to set it to the new path. For example:
+ ```
+ vim /etc/containers/storage.conf
+ ```
+ In the editor, modify the value of the `graphroot` field to the desired new path.
+3. **Modify the storage
+
+path for status data**
+Similar to the design of Buildah, Sealos also provides the ability to set the storage path for status data. In the same configuration file `/etc/containers/storage.conf`, find and modify the `runroot` field to the new path.
+
+By following these steps, you can save the image data and status data of the Sealos cluster to the new paths you set. Each time you run a Sealos command, it will use the new paths you set in `graphroot` and `runroot` to store the image data and status data, respectively.
+
+### Q4: How to disable file md5 check during SSH file transfer?
+
+When the network environment is good, disabling the md5 check can greatly improve transfer speed. If you don't want to check the md5 of files during SSH file transfer, you can add the `-o "HashKnownHosts no"` option to the SSH command.
+
+```shell
+scp -o "HashKnownHosts no" local_file remote_user@remote_ip:/path/to/destination
+```
+
+This option tells SSH not to hash the hostnames in the known_hosts file, which avoids the md5 check during file transfer.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/build-image-using-registry-sync.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/build-image-using-registry-sync.md
new file mode 100644
index 000000000000..6a65a58e9d05
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/build-image-using-registry-sync.md
@@ -0,0 +1,87 @@
+---
+sidebar_position: 2
+---
+
+# Guide to Image Building Improvement
+
+## Deep Understanding of Sealos Image Building
+
+To understand the work behind Sealos image building, we will first reveal what operations it actually performs at the underlying level. Here is an intuitive architectural diagram:
+
+
+
+Sealos covers the following core steps in the image building process:
+
+- **Cache images**: Parse the working directory during the build execution (here we call it the "context" directory), save the cache image to the registry structure, and store it in the ./registry directory.
+- **Build images**: Build images in the context directory and generate new images. (Please note, you need to copy the ./registry directory when building images.)
+
+## Enhancing Image Building Efficiency
+
+In the current project, we have borrowed the source code of `github.com/distribution/distribution`. During the process of caching images, we directly call the registry's sdk and start the registry-proxy function. With the caching ability of the image repository, we cache the image and store it in the context/registry directory.
+
+The key to this process is to call the method of the distribution repository to save the image:
+
+- Start the registry-proxy function.
+- Save image digest and related index data (by calling the saveManifestAndGetDigest method).
+- Save image file data (by calling the saveBlobs method).
+
+This method does have some significant advantages:
+
+- Lightweight: Images can be saved without relying on other components.
+- Free control: You can freely control the save logic without relying on third-party components.
+
+However, we have also noticed some potential problems:
+
+- For beginners, the code is difficult to understand and the logic here is not easy to grasp.
+- Unable to cache using the token authentication method.
+- Need to rely on some temporary storage space, which requires space.
+
+Considering these issues, we decided to try a new mode: start a lightweight registry locally and use the sdk of `skopeo copy` for code reuse. This change directly solves all the previous problems.
+
+
+
+**Therefore, the new construction method ✨Image Repository Sync✨ gracefully debuts 🎉🎉**
+
+The [#3154](https://github.com/labring/sealos/pull/3154) PR in the official repository has completed the implementation of this feature. Currently, Sealos supports these two ways of image construction. Next, I will introduce how to start the new feature (if the new feature performs stably, we may abandon the old construction method).
+
+## How to Start the New Feature
+
+> Sealos v4.3.0 and later versions support this function by default.
+
+Starting the new feature is very simple, just add an environment variable before you build the image. This feature supports both build and merge commands.
+
+```shell
+SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t test .
+```
+
+Here is the expected output after executing the above command:
+
+```tex
+SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t test .
+Getting image source signatures
+Copying blob fee740108510 done
+Copying config f92f3ea6e4 done
+Writing manifest to image destination
+Storing signatures
+Getting image source signatures
+Copying blob 08409d417260 done
+Copying config 44dd6f2230 done
+Writing manifest to image destination
+Storing signatures
+2023-06-01T13:16:07 info saving images busybox, alpine
+STEP 1/2: FROM scratch
+STEP 2/2: COPY registry ./registry
+COMMIT test
+Getting image source signatures
+Copying blob 13ab73c881c8 done
+Copying config 4e22d16b36 done
+Writing manifest to image destination
+Storing signatures
+--> 4e22d16
+
+b366
+Successfully tagged localhost/test:latest
+4e22d16b366e9fec25641522a74cbd73a7db67dc0516b8f8e00200c4d0551592
+```
+
+I hope the above content can help you better understand and use Sealos's new image building method.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/dual-stack-cluster.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/dual-stack-cluster.md
new file mode 100644
index 000000000000..7cbfd71f7665
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/dual-stack-cluster.md
@@ -0,0 +1,789 @@
+---
+sidebar_position: 5
+---
+
+# install Dual-stack cluster with Calico
+
+1. Prerequisites:
+ - Sealos version >=4.3.0
+ - The hosts can communicate using both IPv6 and IPv4 addresses.
+ - Calico adopts the VXLAN mode,the kernel version must be >= 3.12 。 refer to [official docs](https://github.com/cyclinder/kubespray/blob/042c960c6617f8a360a8281464ff63f99ee2471c/docs/calico.md)
+2. run`sealos gen` to generate a Clusterfile, for example:
+
+```shell
+$ sealos gen labring/kubernetes:v1.26.1 labring/helm:v3.10.3 labring/calico:v3.25.0 --masters 192.168.0.10 --nodes 192.168.0.11 --passwd "xxx" >Clusterfile
+```
+
+Notice: labring/helm should be set before labring/calico.
+
+The generated Clusterfile is as follows:
+
+
+Clusterfile
+
+```yaml
+apiVersion: apps.sealos.io/v1beta1
+kind: Cluster
+metadata:
+ creationTimestamp: null
+ name: default
+spec:
+ hosts:
+ - ips:
+ - 192.168.0.10:22
+ roles:
+ - master
+ - amd64
+ - ips:
+ - 192.168.0.11:22
+ roles:
+ - node
+ - amd64
+ image:
+ - labring/kubernetes:v1.26.1
+ - labring/helm:v3.10.3
+ - labring/calico:v3.25.0
+ ssh:
+ passwd: xxx
+ pk: /root/.ssh/id_rsa
+ port: 22
+status: {}
+
+---
+BootstrapTokens: null
+CertificateKey: ""
+LocalAPIEndpoint:
+ AdvertiseAddress: 192.168.0.10
+ BindPort: 6443
+NodeRegistration:
+ CRISocket: /run/containerd/containerd.sock
+ IgnorePreflightErrors: null
+ KubeletExtraArgs: null
+ Name: ""
+ Taints: null
+Patches: null
+SkipPhases: null
+apiVersion: kubeadm.k8s.io/v1beta3
+kind: InitConfiguration
+
+---
+APIServer:
+ CertSANs:
+ - 127.0.0.1
+ - apiserver.cluster.local
+ - 10.103.97.2
+ - 192.168.0.10
+ ExtraArgs:
+ audit-log-format: json
+ audit-log-maxage: "7"
+ audit-log-maxbackup: "10"
+ audit-log-maxsize: "100"
+ audit-log-path: /var/log/kubernetes/audit.log
+ audit-policy-file: /etc/kubernetes/audit-policy.yml
+ enable-aggregator-routing: "true"
+ feature-gates: ""
+ ExtraVolumes:
+ - HostPath: /etc/kubernetes
+ MountPath: /etc/kubernetes
+ Name: audit
+ PathType: DirectoryOrCreate
+ ReadOnly: false
+ - HostPath: /var/log/kubernetes
+ MountPath: /var/log/kubernetes
+ Name: audit-log
+ PathType: DirectoryOrCreate
+ ReadOnly: false
+ - HostPath: /etc/localtime
+ MountPath: /etc/localtime
+ Name: localtime
+ PathType: File
+ ReadOnly: true
+ - HostPath: /etc/kubernetes
+ MountPath: /etc/kubernetes
+ Name: audit
+ PathType: DirectoryOrCreate
+ ReadOnly: false
+ - HostPath: /var/log/kubernetes
+ MountPath: /var/log/kubernetes
+ Name: audit-log
+ PathType: DirectoryOrCreate
+ ReadOnly: false
+ - HostPath: /etc/localtime
+ MountPath: /etc/localtime
+ Name: localtime
+ PathType: File
+ ReadOnly: true
+ TimeoutForControlPlane: null
+CIImageRepository: ""
+CIKubernetesVersion: ""
+CertificatesDir: ""
+ClusterName: ""
+ComponentConfigs: null
+ControlPlaneEndpoint: apiserver.cluster.local:6443
+ControllerManager:
+ ExtraArgs:
+ bind-address: 0.0.0.0
+ cluster-signing-duration: 876000h
+ feature-gates: ""
+ ExtraVolumes:
+ - HostPath: /etc/localtime
+ MountPath: /etc/localtime
+ Name: localtime
+ PathType: File
+ ReadOnly: true
+ - HostPath: /etc/localtime
+ MountPath: /etc/localtime
+ Name: localtime
+ PathType: File
+ ReadOnly: true
+DNS:
+ ImageRepository: ""
+ ImageTag: ""
+ Type: ""
+Etcd:
+ External: null
+ Local:
+ DataDir: ""
+ ExtraArgs:
+ listen-metrics-urls: http://0.0.0.0:2381
+ ImageRepository: ""
+ ImageTag: ""
+ PeerCertSANs: null
+ ServerCertSANs: null
+FeatureGates: null
+ImageRepository: ""
+KubernetesVersion: v1.26.1
+Networking:
+ DNSDomain: ""
+ PodSubnet: 100.64.0.0/10
+ ServiceSubnet: 10.96.0.0/22
+Scheduler:
+ ExtraArgs:
+ bind-address: 0.0.0.0
+ feature-gates: ""
+ ExtraVolumes:
+ - HostPath: /etc/localtime
+ MountPath: /etc/localtime
+ Name: localtime
+ PathType: File
+ ReadOnly: true
+ - HostPath: /etc/localtime
+ MountPath: /etc/localtime
+ Name: localtime
+ PathType: File
+ ReadOnly: true
+apiVersion: kubeadm.k8s.io/v1beta3
+kind: ClusterConfiguration
+
+---
+CACertPath: /etc/kubernetes/pki/ca.crt
+ControlPlane:
+ CertificateKey: ""
+ LocalAPIEndpoint:
+ AdvertiseAddress: ""
+ BindPort: 6443
+Discovery:
+ BootstrapToken: null
+ File: null
+ TLSBootstrapToken: ""
+ Timeout: 5m0s
+NodeRegistration:
+ CRISocket: /run/containerd/containerd.sock
+ IgnorePreflightErrors: null
+ KubeletExtraArgs: null
+ Name: ""
+ Taints: null
+Patches: null
+SkipPhases: null
+apiVersion: kubeadm.k8s.io/v1beta3
+kind: JoinConfiguration
+
+---
+apiVersion: kubeproxy.config.k8s.io/v1alpha1
+bindAddress: 0.0.0.0
+bindAddressHardFail: false
+clientConnection:
+ acceptContentTypes: ""
+ burst: 10
+ contentType: application/vnd.kubernetes.protobuf
+ kubeconfig: ""
+ qps: 5
+clusterCIDR: ""
+configSyncPeriod: 15m0s
+conntrack:
+ maxPerCore: 32768
+ min: 131072
+ tcpCloseWaitTimeout: 1h0m0s
+ tcpEstablishedTimeout: 24h0m0s
+detectLocal:
+ bridgeInterface: ""
+ interfaceNamePrefix: ""
+detectLocalMode: ""
+enableProfiling: false
+healthzBindAddress: 0.0.0.0:10256
+hostnameOverride: ""
+iptables:
+ masqueradeAll: false
+ masqueradeBit: 14
+ minSyncPeriod: 1s
+ syncPeriod: 30s
+ipvs:
+ excludeCIDRs:
+ - 10.103.97.2/32
+ minSyncPeriod: 0s
+ scheduler: ""
+ strictARP: false
+ syncPeriod: 30s
+ tcpFinTimeout: 0s
+ tcpTimeout: 0s
+ udpTimeout: 0s
+kind: KubeProxyConfiguration
+metricsBindAddress: 0.0.0.0:10249
+mode: ipvs
+nodePortAddresses: null
+oomScoreAdj: -999
+portRange: ""
+showHiddenMetricsForVersion: ""
+udpIdleTimeout: 250ms
+winkernel:
+ enableDSR: false
+ forwardHealthCheckVip: false
+ networkName: ""
+ rootHnsEndpointName: ""
+ sourceVip: ""
+
+---
+address: 0.0.0.0
+apiVersion: kubelet.config.k8s.io/v1beta1
+authentication:
+ anonymous:
+ enabled: false
+ webhook:
+ cacheTTL: 2m0s
+ enabled: true
+ x509:
+ clientCAFile: /etc/kubernetes/pki/ca.crt
+authorization:
+ mode: Webhook
+ webhook:
+ cacheAuthorizedTTL: 5m0s
+ cacheUnauthorizedTTL: 30s
+cgroupDriver: cgroupfs
+cgroupsPerQOS: true
+clusterDomain: cluster.local
+configMapAndSecretChangeDetectionStrategy: Watch
+containerLogMaxFiles: 5
+containerLogMaxSize: 10Mi
+contentType: application/vnd.kubernetes.protobuf
+cpuCFSQuota: true
+cpuCFSQuotaPeriod: 100ms
+cpuManagerPolicy: none
+cpuManagerReconcilePeriod: 10s
+enableControllerAttachDetach: true
+enableDebugFlagsHandler: true
+enableDebuggingHandlers: true
+enableProfilingHandler: true
+enableServer: true
+enableSystemLogHandler: true
+enforceNodeAllocatable:
+- pods
+- pods
+eventBurst: 10
+eventRecordQPS: 5
+evictionHard:
+ imagefs.available: 15%
+ memory.available: 100Mi
+ nodefs.available: 10%
+ nodefs.inodesFree: 5%
+evictionPressureTransitionPeriod: 5m0s
+failSwapOn: true
+fileCheckFrequency: 20s
+hairpinMode: promiscuous-bridge
+healthzBindAddress: 0.0.0.0
+healthzPort: 10248
+httpCheckFrequency: 20s
+imageGCHighThresholdPercent: 85
+imageGCLowThresholdPercent: 80
+imageMinimumGCAge: 2m0s
+iptablesDropBit: 15
+iptablesMasqueradeBit: 14
+kind: KubeletConfiguration
+kubeAPIBurst: 10
+kubeAPIQPS: 5
+localStorageCapacityIsolation: true
+logging:
+ flushFrequency: 5000000000
+ format: text
+ options:
+ json:
+ infoBufferSize: "0"
+ verbosity: 0
+makeIPTablesUtilChains: true
+maxOpenFiles: 1000000
+maxPods: 110
+memoryManagerPolicy: None
+memorySwap: {}
+memoryThrottlingFactor: 0.8
+nodeLeaseDurationSeconds: 40
+nodeStatusMaxImages: 50
+nodeStatusReportFrequency: 10s
+nodeStatusUpdateFrequency: 10s
+oomScoreAdj: -999
+podPidsLimit: -1
+port: 10250
+registerNode: true
+registryBurst: 10
+registryPullQPS: 5
+rotateCertificates: true
+runtimeRequestTimeout: 2m0s
+seccompDefault: false
+serializeImagePulls: true
+shutdownGracePeriod: 0s
+shutdownGracePeriodCriticalPods: 0s
+staticPodPath: /etc/kubernetes/manifests
+streamingConnectionIdleTimeout: 4h0m0s
+syncFrequency: 1m0s
+topologyManagerPolicy: none
+topologyManagerScope: container
+volumePluginDir: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/
+volumeStatsAggPeriod: 1m0s
+```
+
+
+
+3. After generating the Clusterfile, modify the cluster configuration.to add IPv6 pod and svc CIDR 。Here's use the fd85:ee78:d8a6:8607::1:0000/112、fd85:ee78:d8a6:8607::1000/116 as a example。The main modifications are as follows:
+
+
+Clusterfile
+
+```yaml
+apiVersion: kubeadm.k8s.io/v1beta3
+kind: ClusterConfiguration
+Networking:
+ DNSDomain: ""
+ PodSubnet: 100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112 #add pod IPv6 subnet
+ ServiceSubnet: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 #add svc IPv6 subnet
+APIServer:
+ CertSANs:
+ - 127.0.0.1
+ - apiserver.cluster.local
+ - 10.103.97.2
+ - 192.168.0.10
+ - 2001:db8::f816:3eff:fe8c:910a #The IPv6 address of the control node,If you need to access the APIserver using this IP,add it.
+ ExtraArgs:
+ service-cluster-ip-range: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 #add svc IPv6 subnet
+ControllerManager:
+ ExtraArgs:
+ node-cidr-mask-size-ipv6: 120 #Default to 64
+ node-cidr-mask-size-ipv4: 24 #Default to 24
+---
+apiVersion: kubeproxy.config.k8s.io/v1alpha1
+kind: KubeProxyConfiguration
+clusterCIDR: "100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112" #add pod IPv6 subnet
+---
+# add configure dual stack for calico
+apiVersion: apps.sealos.io/v1beta1
+kind: Config
+metadata:
+ name: calico
+spec:
+ path: charts/calico/values.yaml
+ strategy: merge
+ data: |
+ installation:
+ enabled: true
+ kubernetesProvider: ""
+ calicoNetwork:
+ bgp: Disabled
+ ipPools:
+ - blockSize: 22
+ cidr: 100.64.0.0/10
+ encapsulation: VXLAN
+ natOutgoing: Enabled
+ nodeSelector: all()
+ - blockSize: 122
+ cidr: fd85:ee78:d8a6:8607::1:0000/112 #add pod IPv6 subnet
+ encapsulation: VXLAN
+ natOutgoing: Enabled
+ nodeSelector: all()
+ nodeAddressAutodetectionV4:
+ interface: "eth.*|en.*|em.*"
+ nodeAddressAutodetectionV6:
+ interface: "eth.*|en.*|em.*"
+```
+
+
+
+The final Clusterfile would look like this:
+
+
+Clusterfile
+
+```yaml
+apiVersion: apps.sealos.io/v1beta1
+kind: Cluster
+metadata:
+ creationTimestamp: null
+ name: default
+spec:
+ hosts:
+ - ips:
+ - 192.168.0.10:22
+ roles:
+ - master
+ - amd64
+ - ips:
+ - 192.168.0.11:22
+ roles:
+ - node
+ - amd64
+ image:
+ - labring/kubernetes:v1.26.1
+ - labring/helm:v3.10.3
+ - labring/calico:v3.25.0
+ ssh:
+ passwd: xxx
+ pk: /root/.ssh/id_rsa
+ port: 22
+status: {}
+
+---
+BootstrapTokens: null
+CertificateKey: ""
+LocalAPIEndpoint:
+ AdvertiseAddress: 192.168.0.10
+ BindPort: 6443
+NodeRegistration:
+ CRISocket: /run/containerd/containerd.sock
+ IgnorePreflightErrors: null
+ KubeletExtraArgs: null
+ Name: ""
+ Taints: null
+Patches: null
+SkipPhases: null
+apiVersion: kubeadm.k8s.io/v1beta3
+kind: InitConfiguration
+
+---
+APIServer:
+ CertSANs:
+ - 127.0.0.1
+ - apiserver.cluster.local
+ - 10.103.97.2
+ - 192.168.0.10
+ - 2001:db8::f816:3eff:fe8c:910a
+ ExtraArgs:
+ service-cluster-ip-range: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116
+ audit-log-format: json
+ audit-log-maxage: "7"
+ audit-log-maxbackup: "10"
+ audit-log-maxsize: "100"
+ audit-log-path: /var/log/kubernetes/audit.log
+ audit-policy-file: /etc/kubernetes/audit-policy.yml
+ enable-aggregator-routing: "true"
+ feature-gates: ""
+ ExtraVolumes:
+ - HostPath: /etc/kubernetes
+ MountPath: /etc/kubernetes
+ Name: audit
+ PathType: DirectoryOrCreate
+ ReadOnly: false
+ - HostPath: /var/log/kubernetes
+ MountPath: /var/log/kubernetes
+ Name: audit-log
+ PathType: DirectoryOrCreate
+ ReadOnly: false
+ - HostPath: /etc/localtime
+ MountPath: /etc/localtime
+ Name: localtime
+ PathType: File
+ ReadOnly: true
+ - HostPath: /etc/kubernetes
+ MountPath: /etc/kubernetes
+ Name: audit
+ PathType: DirectoryOrCreate
+ ReadOnly: false
+ - HostPath: /var/log/kubernetes
+ MountPath: /var/log/kubernetes
+ Name: audit-log
+ PathType: DirectoryOrCreate
+ ReadOnly: false
+ - HostPath: /etc/localtime
+ MountPath: /etc/localtime
+ Name: localtime
+ PathType: File
+ ReadOnly: true
+ TimeoutForControlPlane: null
+CIImageRepository: ""
+CIKubernetesVersion: ""
+CertificatesDir: ""
+ClusterName: ""
+ComponentConfigs: null
+ControlPlaneEndpoint: apiserver.cluster.local:6443
+ControllerManager:
+ ExtraArgs:
+ node-cidr-mask-size-ipv6: 120
+ node-cidr-mask-size-ipv4: 24
+ bind-address: 0.0.0.0
+ cluster-signing-duration: 876000h
+ feature-gates: ""
+ ExtraVolumes:
+ - HostPath: /etc/localtime
+ MountPath: /etc/localtime
+ Name: localtime
+ PathType: File
+ ReadOnly: true
+ - HostPath: /etc/localtime
+ MountPath: /etc/localtime
+ Name: localtime
+ PathType: File
+ ReadOnly: true
+DNS:
+ ImageRepository: ""
+ ImageTag: ""
+ Type: ""
+Etcd:
+ External: null
+ Local:
+ DataDir: ""
+ ExtraArgs:
+ listen-metrics-urls: http://0.0.0.0:2381
+ ImageRepository: ""
+ ImageTag: ""
+ PeerCertSANs: null
+ ServerCertSANs: null
+FeatureGates: null
+ImageRepository: ""
+KubernetesVersion: v1.26.1
+Networking:
+ DNSDomain: ""
+ PodSubnet: 100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112
+ ServiceSubnet: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116
+Scheduler:
+ ExtraArgs:
+ bind-address: 0.0.0.0
+ feature-gates: ""
+ ExtraVolumes:
+ - HostPath: /etc/localtime
+ MountPath: /etc/localtime
+ Name: localtime
+ PathType: File
+ ReadOnly: true
+ - HostPath: /etc/localtime
+ MountPath: /etc/localtime
+ Name: localtime
+ PathType: File
+ ReadOnly: true
+apiVersion: kubeadm.k8s.io/v1beta3
+kind: ClusterConfiguration
+
+---
+CACertPath: /etc/kubernetes/pki/ca.crt
+ControlPlane:
+ CertificateKey: ""
+ LocalAPIEndpoint:
+ AdvertiseAddress: ""
+ BindPort: 6443
+Discovery:
+ BootstrapToken: null
+ File: null
+ TLSBootstrapToken: ""
+ Timeout: 5m0s
+NodeRegistration:
+ CRISocket: /run/containerd/containerd.sock
+ IgnorePreflightErrors: null
+ KubeletExtraArgs: null
+ Name: ""
+ Taints: null
+Patches: null
+SkipPhases: null
+apiVersion: kubeadm.k8s.io/v1beta3
+kind: JoinConfiguration
+
+---
+apiVersion: kubeproxy.config.k8s.io/v1alpha1
+bindAddress: 0.0.0.0
+bindAddressHardFail: false
+clientConnection:
+ acceptContentTypes: ""
+ burst: 10
+ contentType: application/vnd.kubernetes.protobuf
+ kubeconfig: ""
+ qps: 5
+clusterCIDR: ""
+configSyncPeriod: 15m0s
+conntrack:
+ maxPerCore: 32768
+ min: 131072
+ tcpCloseWaitTimeout: 1h0m0s
+ tcpEstablishedTimeout: 24h0m0s
+detectLocal:
+ bridgeInterface: ""
+ interfaceNamePrefix: ""
+detectLocalMode: ""
+enableProfiling: false
+healthzBindAddress: 0.0.0.0:10256
+hostnameOverride: ""
+iptables:
+ masqueradeAll: false
+ masqueradeBit: 14
+ minSyncPeriod: 1s
+ syncPeriod: 30s
+ipvs:
+ excludeCIDRs:
+ - 10.103.97.2/32
+ minSyncPeriod: 0s
+ scheduler: ""
+ strictARP: false
+ syncPeriod: 30s
+ tcpFinTimeout: 0s
+ tcpTimeout: 0s
+ udpTimeout: 0s
+kind: KubeProxyConfiguration
+clusterCIDR: "100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112"
+metricsBindAddress: 0.0.0.0:10249
+mode: ipvs
+nodePortAddresses: null
+oomScoreAdj: -999
+portRange: ""
+showHiddenMetricsForVersion: ""
+udpIdleTimeout: 250ms
+winkernel:
+ enableDSR: false
+ forwardHealthCheckVip: false
+ networkName: ""
+ rootHnsEndpointName: ""
+ sourceVip: ""
+
+---
+address: 0.0.0.0
+apiVersion: kubelet.config.k8s.io/v1beta1
+authentication:
+ anonymous:
+ enabled: false
+ webhook:
+ cacheTTL: 2m0s
+ enabled: true
+ x509:
+ clientCAFile: /etc/kubernetes/pki/ca.crt
+authorization:
+ mode: Webhook
+ webhook:
+ cacheAuthorizedTTL: 5m0s
+ cacheUnauthorizedTTL: 30s
+cgroupDriver: cgroupfs
+cgroupsPerQOS: true
+clusterDomain: cluster.local
+configMapAndSecretChangeDetectionStrategy: Watch
+containerLogMaxFiles: 5
+containerLogMaxSize: 10Mi
+contentType: application/vnd.kubernetes.protobuf
+cpuCFSQuota: true
+cpuCFSQuotaPeriod: 100ms
+cpuManagerPolicy: none
+cpuManagerReconcilePeriod: 10s
+enableControllerAttachDetach: true
+enableDebugFlagsHandler: true
+enableDebuggingHandlers: true
+enableProfilingHandler: true
+enableServer: true
+enableSystemLogHandler: true
+enforceNodeAllocatable:
+- pods
+- pods
+eventBurst: 10
+eventRecordQPS: 5
+evictionHard:
+ imagefs.available: 15%
+ memory.available: 100Mi
+ nodefs.available: 10%
+ nodefs.inodesFree: 5%
+evictionPressureTransitionPeriod: 5m0s
+failSwapOn: true
+fileCheckFrequency: 20s
+hairpinMode: promiscuous-bridge
+healthzBindAddress: 0.0.0.0
+healthzPort: 10248
+httpCheckFrequency: 20s
+imageGCHighThresholdPercent: 85
+imageGCLowThresholdPercent: 80
+imageMinimumGCAge: 2m0s
+iptablesDropBit: 15
+iptablesMasqueradeBit: 14
+kind: KubeletConfiguration
+kubeAPIBurst: 10
+kubeAPIQPS: 5
+localStorageCapacityIsolation: true
+logging:
+ flushFrequency: 5000000000
+ format: text
+ options:
+ json:
+ infoBufferSize: "0"
+ verbosity: 0
+makeIPTablesUtilChains: true
+maxOpenFiles: 1000000
+maxPods: 110
+memoryManagerPolicy: None
+memorySwap: {}
+memoryThrottlingFactor: 0.8
+nodeLeaseDurationSeconds: 40
+nodeStatusMaxImages: 50
+nodeStatusReportFrequency: 10s
+nodeStatusUpdateFrequency: 10s
+oomScoreAdj: -999
+podPidsLimit: -1
+port: 10250
+registerNode: true
+registryBurst: 10
+registryPullQPS: 5
+rotateCertificates: true
+runtimeRequestTimeout: 2m0s
+seccompDefault: false
+serializeImagePulls: true
+shutdownGracePeriod: 0s
+shutdownGracePeriodCriticalPods: 0s
+staticPodPath: /etc/kubernetes/manifests
+streamingConnectionIdleTimeout: 4h0m0s
+syncFrequency: 1m0s
+topologyManagerPolicy: none
+topologyManagerScope: container
+volumePluginDir: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/
+volumeStatsAggPeriod: 1m0s
+---
+apiVersion: apps.sealos.io/v1beta1
+kind: Config
+metadata:
+ name: calico
+spec:
+ path: charts/calico/values.yaml
+ strategy: merge
+ data: |
+ installation:
+ enabled: true
+ kubernetesProvider: ""
+ calicoNetwork:
+ bgp: Disabled
+ ipPools:
+ - blockSize: 22
+ cidr: 100.64.0.0/10
+ encapsulation: VXLAN
+ natOutgoing: Enabled
+ nodeSelector: all()
+ - blockSize: 122
+ cidr: fd85:ee78:d8a6:8607::1:0000/112
+ encapsulation: VXLAN
+ natOutgoing: Enabled
+ nodeSelector: all()
+ nodeAddressAutodetectionV4:
+ interface: "eth.*|en.*|em.*"
+ nodeAddressAutodetectionV6:
+ interface: "eth.*|en.*|em.*"
+```
+
+
+
+4. run`sealos apply -f Clusterfile` to install the cluster。
+
+5. More refer to [Calico official docs](https://docs.tigera.io/calico/latest/networking/ipam/ipv6) and [k8s offical docs](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/dual-stack-support/)
+
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/image-build-standardized.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/image-build-standardized.md
new file mode 100644
index 000000000000..a844bc9ee068
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/image-build-standardized.md
@@ -0,0 +1,104 @@
+---
+sidebar_position: 1
+---
+
+# Image Building and Standard Directory Configuration
+
+Before embarking on Sealos image building tasks, we suggest first constructing a directory structure that conforms to standards. This makes the building process more standardized, easier to manage, and reduces the likelihood of errors. This article will guide you in detail on how to create such a directory structure and explain the purpose of each directory.
+
+## Directory Structure Example
+
+A complete, standardized directory structure example is as follows:
+
+```shell
+.
+├── charts
+│ └── nginx
+│ ├── Chart.lock
+│ ├── charts
+│ ├── Chart.yaml
+│ ├── README.md
+│ ├── templates
+│ ├── values.schema.json
+│ └── values.yaml
+├── images
+│ └── shim
+│ └── nginxImages
+├── init.sh
+├── Kubefile
+├── manifests
+│ └── nginx
+│ ├── deployment.yaml
+│ ├── ingress.yaml
+│ └── service.yaml
+├── opt
+│ └── helm
+└── registry
+```
+
+## Directory Descriptions
+
+Each directory plays a specific role during the build process, and their detailed descriptions are as follows:
+
+- `Kubefile` (required): This file is similar to Dockerfile and is the core file for image building. It defines various steps in the build process, such as the selection of the base image, setting of environment variables, file copying, etc.
+- `manifests`: This directory is used to store Kubernetes yaml files, which describe the configuration information of your applications, such as Pod, Service, Deployment configurations.
+- `charts`: This directory is used to store Helm chart files. Helm chart is a package management tool for Kubernetes that simplifies the deployment and management of Kubernetes applications.
+- `images/shim`: This directory is used to store images that cannot be automatically extracted from yaml files or Helm charts. During the build process, sealos will automatically pull these images.
+- `opt`: Binary files are stored here.
+- `registry`: This directory is used to store images pulled locally during the build process. During the build process, this directory will be automatically generated, and there is no need to manually create it.
+- `init.sh`: This script is automatically run by GitHub Action during the build process. You can write some automated tasks in this script, such as initializing the environment, preprocessing data, etc. (Following the rules of [cluster-image](https://github.com/labring-actions/cluster-image))
+
+## Kubefile Parameters
+
+The `Kubefile` file is at the core of image building and supports various parameters. Below is a detailed analysis of these parameters:
+
+```shell
+FROM labring/kubernetes:v1.24.0
+ENV version v1.1.0
+COPY manifests ./manifests
+COPY registry ./registry
+ENTRYPOINT ["kubectl apply -f manifests/tigera-operator.yaml"]
+CMD ["kubectl apply -f manifests/custom-resources.yaml"]
+```
+
+Descriptions of each parameter:
+
+- `FROM`: This directive is used to set the base image for building. All build steps are based on this image.
+- `LABEL`: `LABEL` defines some internal configurations of the sealos cluster image.
+ - `check`: Some check scripts operation before the cluster image runs.
+ - `clean`: Cleanup scripts for cluster reset or node deletion.
+ - `clean-registry`: The script to clean the image repository when the cluster is reset.
+ - `image`: The lvscare image address of the cluster (Sealos's IPVS image).
+ - `init`: Cluster initialization script.
+
+
+- `init-registry`: The script to start the container image repository when initializing the cluster.
+ - `sealos.io.type`: Cluster image type, currently mainly rootfs, application, and patch.
+ - Rootfs is the basic image for running the cluster, such as Kubernetes, Kubernetes-docker, which includes images, binaries, etc. required by the cluster (**required for each node**).
+ - Application is the application image, such as calico, helm, istio, etc. application service images. (**only stored on the master0 node**)
+ - Patch is needed to adjust after the rootfs image. It is another way to modify the rootfs image (**another method is the Config method**), it will overwrite the first image of the default cluster running.
+ - `sealos.io.version`: The version number of the image, currently the opened version is v1beta1.
+ - `version`: The version number of the cluster, currently it's the version number of Kubernetes.
+ - `vip`: It's the VIP address for modifying the IPVS virtual IP.
+- `ENV`: The `ENV` directive sets the environment variable `` to the value ``. (There will be some default environment variables in rootfs, which can modify some default parameters in rootfs, such as the username and password of the image repository, the storage directory of docker, containerd, etc.)
+
+ For specific cluster images, you need to inspect it specifically, check the corresponding environment variables with `sealos inspect` image, different versions of the image have slight differences.
+ - SEALOS_SYS_CRI_ENDPOINT: The criSocket of the current cluster image (different types of cluster images may be different).
+ - criData: Data directory of cri.
+ - defaultVIP: Default VIP address.
+ - disableApparmor: Whether to disable apparmor (containerd has this issue).
+ - registryConfig: Configuration directory of the container image registry.
+ - registryData: Data directory of the container image registry (The configuration itself has no practical meaning. because the directory is mounted and it's actually stored in `/var/lib/sealos`).
+ - registryDomain: Default domain of the container image registry.
+ - registryPassword: Default Password of the container image registry.
+ - registryPort: Default port number of the container image registry.
+ - registryUsername: Default username of the container image registry.
+ - sandboxImage: Default sandbox_image for cri to start. (No need to write repo, just need to write image name, eg: pasue:3.7).
+- `COPY`: The `COPY` directive copies new files or directories from `` and adds them to the file system path `` on the container. (**Note that the registry directory needs to be copied, otherwise the cluster has no container images**)
+- `ENTRYPOINT`: This directive is used to set the startup command for the image. When the image starts, this command will be executed.
+- `CMD`: This directive is also used to set the startup command for the image. However, the difference between it and the ENTRYPOINT directive is that if users provide a startup command when running the image (`sealos run --cmd`), the command in the CMD directive will be overridden.
+
+During the build process, Sealos will also automatically set some built-in environment variables, including (environment variables with the prefix 'SEALOS_SYS' cannot be modified):
+
+- SEALOS_SYS_KUBE_VERSION: The version number of Kubernetes, for example v1.26.0
+- SEALOS_SYS_SEALOS_VERSION: The version number of Sealos, for example 4.1.3.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/build.png b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/build.png
new file mode 100644
index 000000000000..d1ae7d6aeadd
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/build.png differ
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/registry-build.png b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/registry-build.png
new file mode 100644
index 000000000000..485b531d28f8
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/registry-build.png differ
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/sealos-run.png b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/sealos-run.png
new file mode 100644
index 000000000000..70ef65ffa13f
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/sealos-run.png differ
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/sealos-run.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/sealos-run.md
new file mode 100644
index 000000000000..362863abc4b9
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/sealos-run.md
@@ -0,0 +1,136 @@
+---
+sidebar_position: 2
+---
+
+# How Sealos Run Works
+
+Below is a detailed explanation of how the `sealos run` command works. To aid understanding, we break it down into nine main steps.
+
+## 1. Execution of Command
+
+Firstly, users launch Sealos by executing the following command:
+
+```bash
+sealos run kubernetes:v1.23.8
+```
+
+## 2. Execution of Pipeline
+
+After the command is executed, Sealos starts to execute a series of related operational steps, which we call the "Pipeline". This process includes the following substeps:
+
+### 2.1 Node Check
+
+Sealos will check all nodes to ensure they are available. For example, checking for duplicate hostnames, and whether the time of all nodes has been synchronized.
+
+### 2.2 Pulling Images
+
+Next, Sealos pulls the Kubernetes images from the remote repository and loads them into local storage.
+
+### 2.3 Configuration Rendering
+
+Then, Sealos renders the configuration of the pulled image's rootfs (file system) to prepare for the subsequent node distribution.
+
+### 2.4 Distribution of Image Files
+
+Once the configuration rendering is complete, Sealos distributes the rendered rootfs files to each node.
+
+### 2.5 Distribution of Registry Directory
+
+In addition, Sealos also distributes the registry directory in the image to the corresponding registry role nodes. Sealos supports two modes:
+
+- **scp mode**: Directly copy the directory to each node via scp, which consumes a lot of network bandwidth.
+- **Image synchronization mode**: Using the skopeo sdk's image synchronization mechanism, incremental image synchronization can be achieved to save network bandwidth. This feature can be enabled with the environment variable `SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true`.
+
+### 2.6 Execution of Bootstrap
+
+Bootstrap is a crucial step, including the following operations:
+
+#### 2.6.1 Addition of Host Resolution
+
+Sealos adds host resolution for the registry on each node.
+
+#### 2.6.2 Execution of Registry-Init Script
+
+After adding host resolution, Sealos executes the registry-init script.
+
+#### 2.6.3 Execution of Init Script
+
+Finally, Sealos executes the init script to start the Kubernetes service.
+
+## 3. Execution of InitMaster0
+
+After the Pipeline execution, Sealos enters the InitMaster0 phase. In this phase, Sealos performs the following operations:
+
+### 3.1 Generation of Kubeadm Configuration
+
+Firstly, Sealos generates the initialization configuration file for Kubeadm.
+
+### 3.2 Generation and Distribution of Certificate Files
+
+Then, Sealos generates all certificate files for the cluster and distributes them to each node.
+
+### 3.3 Initialization of Master0
+
+Finally, Sealos executes `kubeadm init` to initialize the Master0 node.
+
+## 4. Execution of JoinMaster
+
+After the initialization of the Master0 node, Sealos enters the JoinMaster phase. In this phase, Sealos performs the following operations:
+
+### 4.1 Synchronization of Kubeconfig
+
+Firstly, Sealos synchronizes the Kubeconfig file to each node.
+
+### 4.2 Synchronization of Certificate Files
+
+Next, Sealos synchronizes certificate files to each node.
+
+### 4.3 Generation of Join Configuration
+
+Then, Sealos generates the JoinMaster configuration file.
+
+### 4.4 Generation of Join Token
+
+Next, Sealos generates the Join token.
+
+### 4.5 Execution of Join Master Process
+
+Finally, Sealos executes the `kubeadm join` process to add other Master nodes to the cluster.
+
+## 5. Execution of JoinNode
+
+After all Master nodes have joined the cluster, Sealos enters
+
+the JoinNode phase and performs the following operations:
+
+### 5.1 Generation of Join Token
+
+Firstly, Sealos generates the Join token again.
+
+### 5.2 Generation of IPVS Rules
+
+Then, Sealos generates IPVS rules.
+
+### 5.3 Generation of Join Configuration
+
+Next, Sealos generates the JoinNode configuration again.
+
+### 5.4 Execution of Join Node Process
+
+Finally, Sealos executes the Join Node process to add Worker nodes to the cluster.
+
+## 6. Synchronization of IPVS
+
+After all nodes have joined the cluster, Sealos synchronizes the IPVS rules of the Master to all nodes. This is mainly done through the lvscare static pod, which synchronizes the static pod configuration based on the number of Master nodes.
+
+## 7. Execution of Guest Phase
+
+After the IPVS rules are synchronized, Sealos executes the Guest phase. This is the stage to execute the cluster image command.
+
+## 8. Writing to the Clusterfile
+
+Finally, Sealos writes the final execution result to the Clusterfile.
+
+This is how the `sealos run` command works. Through this article, we hope that you have a deeper understanding of the `sealos run` command. Below is the panorama we mentioned to help you better understand the entire process.
+
+
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/template-function.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/template-function.md
new file mode 100644
index 000000000000..f99e389def22
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/template-function.md
@@ -0,0 +1,56 @@
+---
+sidebar_position: 9
+---
+
+# Template Inclusion and Function Enhancement
+
+In the image building process of Sealos, files with the "tmpl" extension under the "etc", "scripts", and "manifests" directories are specially treated.
+
+1. "etc" directory: This directory is usually used to store configuration files. During the build process, Sealos will render the files with the "tmpl" extension in this directory. The files, once rendered, are copied to the corresponding directory of the cluster image.
+
+2. "scripts" directory: This directory is usually used to store execution scripts. Sealos will render the files with the "tmpl" extension in this directory. The generated scripts will be executed during the cluster image build process.
+
+3. "manifests" directory: This directory is usually used to store Kubernetes resource manifest files. Sealos will render the files with the "tmpl" extension in this directory. The generated manifest files will be applied to the Kubernetes cluster during the cluster image build process.
+
+In summary, files with the "tmpl" extension in these three directories are treated as template files during the image build process of Sealos. This approach provides more flexibility, allowing us to dynamically generate configurations, scripts, or Kubernetes resource manifests during the build process.
+
+When building an image, we support `template` to allow maintainers to fully control the generated configuration files (module rendering). For example:
+
+```yaml
+service:
+ name: {{ .serviceName }}
+ port:
+ {{- if typeIs "string" .servicePort }}
+ name: {{ .servicePort }}
+ {{- else if or (typeIs "int" .servicePort) (typeIs "float64" .servicePort) }}
+ number: {{ .servicePort | int }}
+ {{- end }}
+```
+
+Please refer to Golang's [text/template](https://pkg.go.dev/text/template) for a basic introduction and more details.
+
+## Template Function Enhancement
+
+In addition, we support `templateFunc` to enhance template functions. For example:
+
+```yaml
+shim: /var/run/image-cri-shim.sock
+cri: /run/containerd/containerd.sock
+address: http://{{ .registryDomain }}:{{ .registryPort }}
+force: true
+debug: false
+image: /var/lib/image-cri-shim
+{{ if and (ne .SEALOS_SYS_KUBE_VERSION "") (semverCompare "^1.26.0" .SEALOS_SYS_KUBE_VERSION) }}version: v1{{ else }}version: v1alpha2{{ end }}
+timeout: 15m
+auth: {{ .registryUsername }}:{{ .registryPassword }}
+```
+
+Here we use `semverCompare` to check whether the user is running on k8s version v1.26.0 or above. If so, generate `version: v1`, otherwise generate `version: v1alpha2`. With this support, we can easily manage multiple versions of Kubernetes support with a single cluster image file.
+
+### Some Most Commonly Used Template Functions
+
+* [semverCompare](http://masterminds.github.io/sprig/semver.html) compares semantic versions, not string comparisons.
+* [default](http://masterminds.github.io/sprig/defaults.html) default can provide a default value when a value is empty or undefined.
+* [toYaml](https://github.com/labring/sealos/blob/main/lifecycle/pkg/template/funcmap.go#L66) displays the current value (object, map, array) as a yaml formatted string.
+
+For a complete list of supported functions, [click here](http://masterminds.github.io/sprig/).
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/images/sealos.webp b/docs/archived/4.0/docs/self-hosting/lifecycle-management/images/sealos.webp
new file mode 100644
index 000000000000..0cc05e478e9e
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/images/sealos.webp differ
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/lifecycle-management.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/lifecycle-management.md
new file mode 100644
index 000000000000..b62233403420
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/lifecycle-management.md
@@ -0,0 +1,93 @@
+---
+sidebar_position: 0
+---
+
+# Kubernetes Lifecycle Management
+
+Sealos provides a powerful set of tools that allow users to easily manage the entire lifecycle of a cluster.
+
+## Features
+
+With Sealos, you can install a bare Kubernetes cluster without any components. Additionally, Sealos can assemble various upper-layer distributed applications on top of Kubernetes using cluster image capabilities, such as databases, message queues, and more.
+
+Sealos not only allows you to install a single-node Kubernetes development environment but also enables you to build production-grade highly available clusters with thousands of nodes.
+
+Sealos offers features like cluster scaling, backup and recovery, and cluster release. It provides an excellent Kubernetes runtime experience even in offline environments.
+
+## Key Features
+
+- ARM support. Offline packages v1.20 and above support integration with both containerd and Docker.
+- Provides 99-year certificates and supports cluster backup and upgrade.
+- Does not rely on Ansible, HAProxy, or Keepalived. It is a standalone binary tool with zero dependencies.
+- Provides offline installation. Different versions of Kubernetes only require different cluster images.
+- High availability is achieved through localLB based on IPVS, which consumes fewer resources and provides stability and reliability, similar to kube-proxy implementation.
+- Automatically recognizes image names using image-cri-shim, making offline delivery more convenient.
+- Almost compatible with all x86_64 architectures that support systemd.
+- Easy addition/deletion of cluster nodes.
+- Trusted by tens of thousands of users in production environments, stable and reliable.
+- Supports cluster images, allowing you to customize and combine the cluster components you need, such as OpenEBS storage + database + MinIO object storage.
+- Uses the SDK of Buildah to standardize the image format, fully compatible with OCI standards.
+
+## Running a Kubernetes Cluster with Sealos
+
+Running a Kubernetes cluster with Sealos is straightforward. Just follow these steps:
+
+```bash
+$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v4.3.0/scripts/install.sh \
+ | sh -s v4.3.0 labring/sealos
+# Create a cluster
+$ sealos run labring/kubernetes:v1.25.0-4.2.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \
+ --masters 192.168.64.2,192.168.64.22,192.168.64.20 \
+ --nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd]
+```
+
+[](https://asciinema.org/a/519263?speed=3)
+
+## Running Distributed Applications on the Cluster
+
+With the `sealos run` command, you can run various distributed applications on the cluster, such as databases, message queues, AI capabilities, and even enterprise-level SaaS software. For example:
+
+```shell
+# MySQL cluster
+$ sealos run labring/mysql-operator:8.0.23-14.1
+
+# Clickhouse cluster
+$ sealos run labring/clickhouse:0.18.4
+
+# Redis cluster
+$ sealos run labring/redis-operator:3.1.4
+```
+
+## Customizing the Cluster
+
+For cluster images not available in the Sealos ecosystem, users can easily build and customize their own cluster images. For example:
+
+[Building an Ingress Cluster Image](/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md)
+
+You can also customize your own Kubernetes cluster:
+
+Sealfile:
+
+```shell
+FROM kubernetes:v1.25.0
+COPY flannel-chart .
+COPY mysql-chart .
+CMD ["helm install flannel flannel-chart", "helm install mysql mysql-chart"]
+```
+
+```shell
+sealos build -t my-kubernetes:v1.25.0 .
+sealos run my-kubernetes:v1.25.0 ...
+```
+
+## Frequently Asked Questions
+
+**Is Sealos a Kubernetes installation tool?**
+
+Installation and deployment are basic functions of Sealos, similar to the boot module in a single-node operating system. Sealos' boot module effectively manages the lifecycle of Kubernetes in any scenario.
+
+**What are the differences between Sealos, Rancher, and KubeSphere?**
+
+Sealos is designed with the philosophy of "simplifying complexity, freely assembling, and simplicity as the ultimate goal." Sealos leverages the capabilities of Kubernetes to provide users with exactly what they need in a simple way. Users may not necessarily need Kubernetes; what they need is specific functionality.
+
+Sealos is highly flexible and does not impose additional burdens on users. Its form depends on user requirements and the applications being installed. The core of Sealos is distributed applications, and all applications are treated equally.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md
new file mode 100644
index 000000000000..ef9ba1db8c37
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md
@@ -0,0 +1,88 @@
+---
+sidebar_position: 4
+---
+
+# Building Cluster Images Based on Binary Files
+
+This document primarily details how to use the `sealos` tool to package a single binary file (like `helm` or `kustomize`) into a cluster image and install them by deploying the cluster image on the master node. Using `helm` as an example, we will thoroughly discuss how to package a binary file into a cluster image.
+
+## Create a Build Workspace
+
+Firstly, create a base directory to serve as a build workspace:
+
+```shell
+$ mkdir ~/cluster-images
+```
+
+In the workspace, create an `opt` directory for storing the binary files:
+
+```shell
+$ cd cluster-images
+$ mkdir opt/
+```
+
+## Prepare the Binary File
+
+Next, we prepare the `helm` binary file. Here, we download from [github release](https://github.com/helm/helm/releases):
+
+```shell
+wget https://get.helm.sh/helm-v3.10.1-linux-amd64.tar.gz
+tar -zxvf helm-v3.10.1-linux-amd64.tar.gz
+chmod a+x linux-amd64/helm
+mv linux-amd64/helm opt/
+```
+
+## Create the `Sealfile` Required for Building the Image
+
+Create a file named `Sealfile`, with the following content:
+
+```shell
+FROM scratch
+COPY opt ./opt
+CMD ["cp opt/helm /usr/bin/"]
+```
+
+The current directory structure is as follows:
+
+```
+.
+├── Sealfile
+└── opt
+ └── helm
+```
+
+## Build the Cluster Image
+
+Now, everything is ready, and you can begin building the cluster image:
+
+```shell
+sealos build -t labring/helm:v3.10.1 .
+```
+
+**Note:** Firstly, you need to install the `sealos` command on the local host.
+
+You can view the build log to understand the building process.
+
+```shell
+root@ubuntu:~/cluster-images# sealos build -t labring/helm:v3.10.1 .
+...
+```
+
+View the built image, and now all the dependent binary files have been built into the cluster image:
+
+```shell
+root@ubuntu:~/cluster-images# sealos images
+labring/helm v3.10.1 19ed4a24f0fe 3 minutes ago 45.1 MB
+```
+
+## Push the Image
+
+You can push the image to any Docker image repository, the following command pushes the image to DockerHub:
+
+```shell
+sealos push labring/helm:v3.10.1
+```
+
+**Note:** Please use the `sealos` command to operate the cluster image; Docker commands are not supported.
+
+If you are using a private image repository, you can use the `sealos login` command to log into your image repository, then push or pull the image.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md
new file mode 100644
index 000000000000..379ae451afaf
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md
@@ -0,0 +1,106 @@
+---
+sidebar_position: 5
+---
+
+# Building Cluster Images Based on go-template
+
+During the process of building cluster images, we can use the `--env` option to pass some variables through the sealos command line. These environment variables can be used by the `CMD` command of the Kubefile or the yaml file template.
+
+## Using Environment Variables in Kubefile
+
+This example defines a `SERVICE_TYPE` variable that allows the user to customize the service exposure type when installing the application and pass parameters to the helm command in CMD.
+
+Kubefile example:
+
+```shell
+FROM scratch
+ENV SERVICE_TYPE "NodePort"
+COPY charts charts
+COPY registry registry
+CMD ["helm upgrade --install nginx charts/nginx --namespace=nginx --create-namespace --set service.type=$(SERVICE_TYPE)"]
+```
+
+Run the cluster application and set a custom `SERVICE_TYPE=LoadBalancer`, if not set, it will default to NodePort.
+
+```shell
+sealos run labring/nginx:v1.23.1 --env SERVICE_TYPE=LoadBalancer
+```
+
+## Using Environment Variables in Yaml Files
+
+Prepare a simple nginx service yaml file, this file must be a `*.tmpl` extension to be rendered when running `sealos run --env` command.
+
+```shell
+$ cat manifests/service.yaml.tmpl
+apiVersion: v1
+kind: Service
+metadata:
+ name: nginx
+ labels:
+ name: nginx
+spec:
+ type: {{ .serviceType }}
+ ports:
+ - port: 80
+ nodePort: {{ .http_NodePort }}
+ name: http
+ - port: 443
+ nodePort: {{ .https_NodePort }}
+ name: https
+ selector:
+ name: nginx
+```
+
+Here is a Kubefile example where you can set the default environment variables.
+
+```shell
+FROM scratch
+ENV serviceType NodePort
+ENV http_NodePort 30080
+ENV https_NodePort 30443
+
+COPY manifests manifests
+COPY registry registry
+CMD ["kubectl apply -f manifests/service.yaml"]
+```
+
+When you build the image, nothing will happen, it only renders when running the application. If `--env` is not set, it will use the default ENV in Kubefile.
+
+```shell
+sealos run labring/nginx:1.23.1 --env serviceType=LoadBalancer --env http_NodePort=30080 --env https_NodePort=30443
+```
+
+You will find that sealos renders a new yaml file `service.yaml` based on `service.yaml.tmpl` on the local path of the master node.
+
+**Note** The new version of the application's rootfs is placed in the `/var/lib/sealos/data/default/applications` directory, each application has its independent directory.
+
+```shell
+root@node1:~# ls /var/lib/sealos/data/default/rootfs/manifests |grep service
+service.yaml
+service.yaml.tmpl
+```
+
+Check the yaml content:
+
+```shell
+root@node1:~# cat /var/lib/sealos/data/default/rootfs/manifests/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ name: nginx
+ labels:
+ name: nginx
+spec:
+ type: NodePort
+ ports:
+ - port: 80
+ nodePort: 30080
+ name: http
+ - port: 443
+ nodePort: 30443
+ name: https
+ selector:
+ name: nginx
+```
+
+**Note:** All types of files support this feature (the file name suffix is .tmpl and the build directory is in etc, scripts, and manifests), you can try it yourself.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md
new file mode 100644
index 000000000000..fe440580aa7f
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md
@@ -0,0 +1,199 @@
+---
+sidebar_position: 3
+---
+
+# Building Cluster Images Based on Helm Charts
+
+Let's use the simplest nginx application as an example to introduce how to build a cluster image based on nginx using Helm Charts.
+
+## 1. Preparation
+
+Create a base directory for the build work.
+
+```shell
+$ mkdir ~/cloud-images
+```
+
+Create a `charts` directory to store the Kubernetes nginx Helm Charts files.
+
+```shell
+$ cd cloud-images
+$ mkdir charts
+```
+
+## 2. Prepare Helm Charts
+
+Prepare the nginx Helm Charts. Here we use [the official nginx Helm Charts by bitnami](https://bitnami.com/stack/nginx). Let's pull the Helm Chart files locally and unzip them to the `charts` directory.
+
+```shell
+helm repo add bitnami https://charts.bitnami.com/bitnami
+helm search repo bitnami/nginx
+helm pull bitnami/nginx --version=13.2.13 -d charts/ --untar
+```
+
+**Note:** First, you should install the Helm command tool to your local host.
+
+Now, the structure of the charts directory is as follows:
+
+```
+charts/
+└── nginx
+ ├── Chart.lock
+ ├── charts
+ ├── Chart.yaml
+ ├── README.md
+ ├── templates
+ ├── values.schema.json
+ └── values.yaml
+```
+
+## 3. Create Kubefile
+
+Create a file named `Kubefile` for image construction:
+
+```shell
+$ cat Kubefile
+FROM scratch
+COPY charts charts
+COPY registry registry
+CMD ["helm install nginx charts/nginx --namespace=nginx --create-namespace"]
+```
+
+It is recommended to use `helm upgrade --install` instead of `helm install` so that you can rerun the same command when updating the application in the future.
+
+You can add other options as needed, such as exposing the service through NodePort.
+
+```shell
+FROM scratch
+COPY charts charts
+COPY registry registry
+CMD ["helm upgrade --install nginx charts/nginx --namespace=nginx --create-namespace --set service.type=NodePort"]
+```
+
+## 4. Build the Cluster Image
+
+Now everything is ready, and you can start building the cluster image.
+
+```shell
+sealos build -t labring/nginx:v1.23.2 .
+```
+
+**Note:** You should first install the sealos command to your local host.
+
+You can view the build log.
+
+```shell
+root@ubuntu:~/cloud-images# sealos build -t labring/nginx:v1.23.2 .
+...
+```
+
+sealos will automatically extract the images from the charts directory, pull them locally, and store them in the registry directory.
+
+The current directory structure is as follows:
+
+```shell
+.
+├── charts
+│ └── nginx
+│ ├── Chart.lock
+│ ├── charts
+│ ├── Chart.yaml
+│ ├── README.md
+│ ├── templates
+│ ├── values.schema.json
+│ └── values.yaml
+├── Kubefile
+└── registry
+ └── docker
+ └── registry
+```
+
+Check the built image locally. Now all dependent deployment manifests and image caches are built into the cluster image.
+
+```shell
+root@ubuntu:~/cloud-images# sealos images
+labring/nginx v1.23.2 521c85942ee4 4 minutes ago 56.8 MB
+```
+
+You can push the image to any Docker image repository. The following command pushes it to Docker Hub.
+
+```shell
+sealos push labring/nginx:v1.23.2
+```
+
+**Note:** Please use the sealos command to operate the cluster
+
+image, Docker commands are not supported.
+
+If you use a private image repository, just use the `sealos login` command to log in to the registry before pulling or pushing the image.
+
+```shell
+sealos login docker.io -u xxx -p xxx
+
+sealos login registry.cn-hangzhou.aliyuncs.com -u xxx -p xxx
+```
+
+## 5. Install the Cluster Image
+
+Then, you can run the cluster image in your cluster.
+
+```shell
+sealos run labring/nginx:v1.23.2
+```
+
+The helm binary command will be installed on the master node of your Kubernetes cluster.
+
+```shell
+root@ubuntu:~# helm -n nginx ls
+```
+
+## 6. Explanation
+
+By default, when building images, sealos only parses the default values.yml file. However, you can also provide a custom values.yaml file for sealos.
+
+**The custom values file must be placed in the same directory as your Chart, and must be named in the form of `.values.yaml`, for example `loki-stack.values.yaml`.**
+
+```shell
+.
+├── charts
+│ ├── loki-stack
+│ │ ├── charts
+│ │ ├── Chart.yaml
+│ │ ├── README.md
+│ │ ├── requirements.lock
+│ │ ├── requirements.yaml
+│ │ ├── templates
+│ │ └── values.yaml
+│ └── loki-stack.values.yaml
+├── init.sh
+├── Kubefile
+```
+
+The content of `loki-stack.values.yaml` file is as follows:
+
+```shell
+$ cat charts/loki-stack.values.yaml
+promtail:
+ enabled: false
+fluent-bit:
+ enabled: true
+grafana:
+ enabled: true
+```
+
+Different values files may output different image lists, allowing sealos to automatically parse the images during the `sealos build` process.
+
+```shell
+$ helm template charts/loki-stack/ -f charts/loki-stack/values.yaml|grep image:
+ image: "grafana/promtail:2.0.0"
+ image: "grafana/loki:2.0.0"
+ image: "bats/bats:v1.1.0"
+
+$ helm template charts/loki-stack/ -f charts/loki-stack.values.yaml|grep image:
+ image: "grafana/fluent-bit-plugin-loki:1.6.0-amd64"
+ image: "kiwigrid/k8s-sidecar:0.1.209"
+ image: "grafana/grafana:6.7.0"
+ image: "grafana/loki:2.0.0"
+ image: "bats/bats:v1.1.0"
+ image: bats/bats:v1.1.0
+```
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-image-list.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-image-list.md
new file mode 100644
index 000000000000..fa99e7aa89b7
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-image-list.md
@@ -0,0 +1,77 @@
+---
+sidebar_position: 1
+---
+
+# Building Cluster Images Based on Image Manifests
+
+This guide will walk you through the process of building cluster images using image manifests or using existing tarballs stored in Docker.
+
+## Building from Image Manifests
+
+```
+.
+├── Kubefile
+├── cni
+│ ├── custom-resources.yaml
+│ └── tigera-operator.yaml
+├── images
+│ └── shim
+│ └── CalicoImageList
+└── registry
+ └── docker
+ └── registry
+```
+
+```dockerfile
+FROM labring/kubernetes:v1.24.0
+COPY cni ./cni
+COPY images ./images
+COPY registry ./registry
+CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"]
+```
+
+Explanation:
+
+The images listed in CalicoImageList will be pulled locally and then applied to the cluster using the `kubectl apply -f` command.
+
+The image manifest currently supports the following:
+- Remote images like docker.io/calico/cni:v3.20.0
+- Local OCI container images like containers-storage:docker.io/labring/coredns:v0.0.1
+- Local Docker container images like docker-daemon:docker.io/library/nginx:latest
+
+## Building from Image Tarballs
+
+```
+.
+├── Kubefile
+├── cni
+│ ├── custom-resources.yaml
+│ └── tigera-operator.yaml
+├── images
+│ └── skopeo
+│ ├── calico.tar
+│ └── tar.txt
+└── registry
+ └── docker
+ └── registry
+```
+
+```dockerfile
+FROM scratch
+COPY cni ./cni
+COPY images ./images
+COPY registry ./registry
+CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"]
+```
+
+Explanation:
+
+The configurations in tar.txt will be pulled locally and redirected to the image list. Then, they will be applied to the cluster using the `kubectl apply -f` command. The configuration file format is as follows:
+
+```
+docker-archive:calico.tar@calico/cni:v3.20.0
+```
+
+The image manifest currently supports the following:
+- Docker archive images, supporting a single image, like docker-archive
+- OCI archive images, supporting a single image, like oci-archive
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md
new file mode 100644
index 000000000000..561e92e377b9
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md
@@ -0,0 +1,101 @@
+---
+sidebar_position: 2
+---
+
+# Building Cluster Images Based on Deployment Manifest
+
+This document will detail how to build cluster images based on Deployment Manifests. We will use a simple nginx application as an example.
+
+## I. Preparations
+
+1. First, create a base directory as the build workspace.
+
+```shell
+$ mkdir ~/cloud-images
+```
+
+2. Create a directory named `manifests` to store the kubernetes nginx deployment yaml file.
+
+```shell
+$ cd cloud-images
+$ mkdir manifests
+```
+
+## II. Prepare the Manifest File
+
+At this stage, we will prepare a simple nginx kubernetes yaml file.
+
+```shell
+$ cat manifests/deployment.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nginx-deployment
+spec:
+ selector:
+ matchLabels:
+ app: nginx
+ replicas: 2
+ template:
+ metadata:
+ labels:
+ app: nginx
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:1.23.1
+ ports:
+ - containerPort: 80
+```
+
+## III. Create Kubefile
+
+At this stage, we need to create a Kubefile that will be used to build the image.
+
+```shell
+FROM scratch
+COPY manifests manifests
+COPY registry registry
+CMD ["kubectl apply -f manifests/deployment.yaml"]
+```
+
+## IV. Build the Cluster Image
+
+After preparing all the necessary files and directories, we can start building the cluster image.
+
+```shell
+sealos build -t labring/nginx:v1.23.1 .
+```
+
+**Note:** Before starting the build, you need to install the sealos command on your local host.
+
+During the build, you can view the build log.
+
+## V. Verify the Image
+
+After the build is complete, you can view the built image with the following command:
+
+```shell
+root@ubuntu:~/cloud-images# sealos images
+labring/nginx v1.23.1 521c85942ee4 4 minutes ago 56.8 MB
+```
+
+## VI. Push the Image
+
+Finally, we can push the built image to any Docker image repository. The following command pushes it to DockerHub.
+
+```shell
+sealos push labring/nginx:v1.23.1
+```
+
+**Note:** Please use the sealos command to operate the cluster image, the Docker command is not supported.
+
+If you are using a private image repository, just use `sealos login` to log into the repository before pulling or pushing the image.
+
+```shell
+sealos login docker.io -u xxx -p xxx
+
+sealos login registry.cn-hangzhou.aliyuncs.com -u xxx -p xxx
+```
+
+At this point, the cluster image based on the deployment manifest is successfully built.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md
new file mode 100644
index 000000000000..c3337169e262
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md
@@ -0,0 +1,129 @@
+---
+sidebar_position: 6
+---
+
+# Building Cluster Images Using exec and scp Commands
+
+By default, `sealos run xx` only runs the command and copies the file on the first master node. When you want to run the command or copy files on specific nodes or all nodes, you can use the `sealos exec` or `sealos scp` commands when building the cluster image.
+
+- sealos exec: Connects to one or more nodes and runs any shell command;
+- sealos scp: Connects to one or more nodes and copies local files to remote nodes.
+
+Although you can directly use these commands on the host, this article mainly describes how to use these two commands when building the cluster image using sealos build.
+
+## sealos exec Example
+
+Below is an example of building an openebs cluster image. Before installing openebs maystor, some initialization operations need to be performed on the node, you can use sealos exec to achieve this.
+
+First, create a base directory for building.
+
+```shell
+$ mkdir ~/cloud-images
+```
+
+Create a `charts` directory to store the kubernetes nginx helm charts file.
+
+```shell
+$ cd cloud-images
+```
+
+Create a file named `Kubefile` for image building:
+
+```shell
+$ cat Kubefile
+FROM scratch
+COPY manifests manifests
+COPY registry registry
+COPY opt opt
+COPY mayastor.sh mayastor.sh
+CMD ["bash mayastor.sh"]
+```
+
+Create a script file named `mayastor.sh`, the shell command after sealos exec will be executed on all nodes (create hugepage, load kernel modules on all nodes), but other commands will only run on the master node.
+
+```shell
+$ cat mayastor.sh
+#!/usr/bin/env bash
+set -e
+
+sealos exec "
+echo vm.nr_hugepages = 1024 | sudo tee -a /etc/sysctl.d/mayastor.conf
+sysctl -p
+sudo modprobe -- nbd
+sudo modprobe -- nvmet
+sudo modprobe -- nvmet_rdma
+sudo modprobe -- nvme_fabrics
+sudo modprobe -- nvme_tcp
+sudo modprobe -- nvme_rdma
+sudo modprobe -- nvme_loop
+cat < /tmp/buildimage/kubeadm.yml < /tmp/buildimage/Kubefile < Clusterfile
+```
+
+Notice: labring/helm should be set before labring/calico.
+
+The generated Clusterfile is as follows:
+
+
+Clusterfile
+
+```yaml
+apiVersion: apps.sealos.io/v1beta1
+kind: Cluster
+metadata:
+ creationTimestamp: null
+ name: default
+spec:
+ hosts:
+ - ips:
+ - 192.168.0.2:22
+ - 192.168.0.3:22
+ - 192.168.0.4:22
+ roles:
+ - master
+ - amd64
+ - ips:
+ - 192.168.0.5:22
+ - 192.168.0.6:22
+ - 192.168.0.7:22
+ roles:
+ - node
+ - amd64
+ image:
+ - labring/kubernetes:v1.24.0
+ - labring/helm:v3.8.2
+ - labring/calico:v3.24.1
+ ssh:
+ passwd: xxx
+ pk: /root/.ssh/id_rsa
+ port: 22
+ user: root
+status: {}
+```
+
+
+
+2. After generating the Clusterfile, update the cluster configuration. For example, to modify the CIDR range of pods, you can modify the `networking.podSubnet` and `spec.data.spec.calicoNetwork.ipPools.cidr` fields. The final Clusterfile would look like this:
+
+
+Clusterfile
+
+```yaml
+apiVersion: apps.sealos.io/v1beta1
+kind: Cluster
+metadata:
+ creationTimestamp: null
+ name: default
+spec:
+ hosts:
+ - ips:
+ - 192.168.0.2:22
+ - 192.168.0.3:22
+ - 192.168.0.4:22
+ roles:
+ - master
+ - amd64
+ - ips:
+ - 192.168.0.5:22
+ - 192.168.0.6:22
+ - 192.168.0.7:22
+ roles:
+ - node
+ - amd64
+ image:
+ - labring/kubernetes:v1.25.0
+ - labring/helm:v3.8.2
+ - labring/calico:v3.24.1
+ ssh:
+ passwd: xxx
+ pk: /root/.ssh/id_rsa
+ port: 22
+ user: root
+status: {}
+---
+apiVersion: kubeadm.k8s.io/v1beta2
+kind: ClusterConfiguration
+networking:
+ podSubnet: 10.160.0.0/12
+---
+apiVersion: apps.sealos.io/v1beta1
+kind: Config
+metadata:
+ name: calico
+spec:
+ path: charts/calico/values.yaml
+ strategy: merge
+ data: |
+ installation:
+ enabled: true
+ kubernetesProvider: ""
+ calicoNetwork:
+ ipPools:
+ - blockSize: 26
+ cidr: 10.160.0.0/12
+ encapsulation: IPIP
+ natOutgoing: Enabled
+ nodeSelector: all()
+ nodeAddressAutodetectionV4:
+ interface: "eth.*|en.*"
+```
+
+
+
+3. Run `sealos apply -f Clusterfile` to start the cluster. After the cluster is successfully running, the Clusterfile will be saved in the `.sealos/default/Clusterfile` file. You can modify the fields in it to reapply changes to the cluster.
+
+**Note:**
+
+- You can refer to the [official documentation](https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-config/) or run the `kubeadm config print init-defaults` command to print the kubeadm configuration.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/run-cluster.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/run-cluster.md
new file mode 100644
index 000000000000..80a6adf33d5e
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/run-cluster.md
@@ -0,0 +1,14 @@
+---
+sidebar_position: 0
+---
+
+# Running a Sealos Cluster with Docker Images
+
+In this directory, we provide a series of detailed tutorials and resources to help you effectively run and manage a Sealos cluster. Here is a brief overview of each file or subdirectory:
+
+1. **Cluster Initialization**: This section provides all the necessary steps to start and initialize a new Sealos cluster. It includes information on how to configure Sealos and how to run initialization commands.
+2. **Node Management**: This section covers how to add and remove cluster nodes, as well as how to maintain and manage detailed information about the cluster nodes.
+3. **Service Management**: This section includes guides on how to deploy and manage various services on the Sealos cluster. It covers how to use Sealos' command-line tools to manage services and how to configure services to meet your specific requirements.
+4. **Practical Examples**: This section contains various practical examples, including how to run and manage various applications and how to handle specific operational issues.
+
+We hope this directory provides comprehensive assistance for running and managing a Sealos cluster. If you have any questions or suggestions, please feel free to contact us at any time.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/template-apply-cluster.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/template-apply-cluster.md
new file mode 100644
index 000000000000..ee1ec670ae3f
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/template-apply-cluster.md
@@ -0,0 +1,64 @@
+---
+sidebar_position: 2
+---
+
+# Template Configuration Installation
+
+We can also use Go template syntax to write the Clusterfile (similar to [Helm](https://helm.sh/)), but partial template functions such as `include`/`tpl`/`require`/`lookup` are not supported yet. For example, create a `Clusterfile.yaml` as follows:
+
+```yaml
+apiVersion: apps.sealos.io/v1beta1
+kind: Cluster
+metadata:
+ name: {{ .Values.clusterName }}
+spec:
+ hosts:
+ - ips: {{ .Values.masters | toYaml | nindent 8 }}
+ roles: ["master", "amd64"]
+ {{- with .Values.nodes }}
+ - ips: {{ . | toYaml | nindent 8 }}
+ roles: ["node", "amd64"]
+ {{- end }}
+ image: {{ .Values.images | toYaml | nindent 4 }}
+ ssh:
+ passwd: {{ env "SSH_PASSWORD" .Values.ssh.passwd }}
+ pk: {{ default "~/.ssh/id_rsa" .Values.ssh.pk }}
+ port: {{ default 22 .Values.ssh.port | int }}
+ user: {{ default "root" .Values.ssh.user }}
+---
+apiVersion: kubeadm.k8s.io/v1beta3
+kind: ClusterConfiguration
+networking:
+ dnsDomain: {{ default "cluster.local" .Values.networking.dnsDomain }}
+ serviceSubnet: {{ default "10.96.0.0/18" .Values.networking.serviceSubnet }}
+ podSubnet: {{ default "100.64.0.0/17" .Values.networking.podSubnet }}
+```
+
+Then, create a custom values file `example.values.yaml`:
+
+```yaml
+clusterName: default
+images:
+ - dockerhub.tencentcloudcr.com/labring/kubernetes:v1.23.8
+ - dockerhub.tencentcloudcr.com/labring/calico:v3.24.1
+masters:
+ - 10.74.16.27:22
+ - 10.74.16.140:22
+ - 10.74.16.101:22
+nodes: []
+ssh:
+ # passwd: notSetYet
+ pk: /path/to/private/key/file
+ port: 22
+ user: root
+networking:
+ dnsDomain: cluster.local
+ serviceSubnet: 10.96.0.0/18
+ podSubnet: 100.64.0.0/17
+```
+
+You can then deploy the cluster like this:
+
+```shell
+$ sealos apply -f Clusterfile.yaml --values example.values.yaml --set clusterName=testlocal --env SSH_PASSWORD=s3cret
+```
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/upgrade-cluster.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/upgrade-cluster.md
new file mode 100644
index 000000000000..d95b23e7c118
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/upgrade-cluster.md
@@ -0,0 +1,48 @@
+---
+sidebar_position: 3
+---
+
+# How to Upgrade the Cluster
+
+If you want to upgrade your Kubernetes cluster, you just need to run the following command:
+
+```sh
+sealos run labring/kubernetes:
+```
+
+Make sure you have already set up the cluster.
+
+## Example Scenario
+
+1. Let's say you have previously run the following command:
+
+```sh
+sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.8 --nodes 192.168.64.7
+```
+
+2. Now, if you want to upgrade the cluster to v1.25.0, you can do the following:
+
+```sh
+sealos run labring/kubernetes:v1.25.0
+```
+
+During the execution of 'kubeadm upgrade v1.25.0', you will see:
+
+```txt
+[upgrade/version] You have chosen to change the cluster version to "v1.25.0"
+[upgrade/versions] Cluster version: v1.24.0
+[upgrade/versions] kubeadm version: v1.25.0
+[upgrade] Are you sure you want to proceed? [y/N]:
+```
+
+Type 'y' to proceed with the upgrade.
+
+If there are any **errors**, you can rerun the command 'sealos run labring/kubernetes:v1.25.0'. Even if it fails, it will ensure the same result.
+
+## Important Notes
+
+1. **Upgrades cannot skip minor version numbers**. For example, upgrading from 'v1.23.0' to 'v1.25.0' is not allowed. If you do need to upgrade from 'v1.23.0' to 'v1.25.0', you can do it in two steps, such as upgrading from 'v1.23.0' to 'v1.24.0' first, and then from 'v1.24.0' to 'v1.25.0'.
+
+2. Once the upgrade is successful, the old version images mounted by the cluster will be replaced. Adding master or worker nodes will apply the new version.
+
+This is the entire process of upgrading a Kubernetes cluster. If you encounter any issues during the upgrade process, don't hesitate to refer to relevant documentation or seek assistance.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md
new file mode 100644
index 000000000000..a9cab905b7f0
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md
@@ -0,0 +1,63 @@
+---
+sidebar_position: 3
+---
+
+# Building an Ingress Cluster Image
+
+Here we demonstrate how to build an nginx-ingress cluster image using Helm.
+
+## Download the Helm Chart
+
+```shell
+$ mkdir ingress-nginx && cd ingress-nginx
+$ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
+$ helm pull ingress-nginx/ingress-nginx
+```
+
+You will find the downloaded chart:
+
+```shell
+$ ls
+ingress-nginx-4.1.0.tgz
+```
+
+## Add Image List
+
+Sealos will download the images in the image list and cache them in the registry directory.
+
+The directory must be in the format `images/shim/[your image list filename]`:
+
+```shell
+$ cat images/shim/nginxImages
+k8s.gcr.io/ingress-nginx/controller:v1.2.0
+k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1
+```
+
+## Write the Dockerfile
+
+```Dockerfile
+FROM scratch
+COPY ../examples .
+CMD ["helm install ingress-nginx ingress-nginx-4.1.0.tgz --namespace ingress-nginx --create-namespace"]
+```
+
+## Build the Cluster Image
+
+```shell
+$ sealos build -f Dockerfile -t docker.io/fanux/ingress-nginx:v1.2.0 .
+```
+
+Sealos will automatically add the image dependencies from the image list to the cluster image, magically saving the Docker images it depends on inside. When running in another environment, it will magically check if the Docker images exist in the cluster. If they do, it will automatically download them; otherwise, it will download them from k8s.gcr.io. Users do not need to modify the Docker image addresses in the Helm chart. This utilizes the black technology of image caching proxy.
+
+## Push to the Image Registry
+
+```shell
+$ sealos login docker.io
+$ sealos push docker.io/fanux/ingress-nginx:v1.2.0
+```
+
+## Run the Cluster Image
+
+```shell
+$ sealos run docker.io/fanux/ingress-nginx:v1.2.0
+```
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md
new file mode 100644
index 000000000000..0313c08fa442
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md
@@ -0,0 +1,191 @@
+---
+sidebar_position: 2
+keywords: [K8s, K8s install, K8s deploy, Kubernetes cluster setup, multi-node Kubernetes, offline Kubernetes installation]
+---
+
+# Install Kubernetes
+
+Sealos supports installing Kubernetes clusters on `amd64` and `arm64` architecture machines.
+
+## Prerequisites
+
+You'll first need to [download the Sealos CLI tool](/self-hosting/lifecycle-management/quick-start/install-cli.md). Sealos is a simple Golang binary that can be installed on most Linux operating systems.
+
+Here are some basic installation requirements:
+
+- Each cluster node should have a unique hostname without underscores.
+- System times must be synchronized across all nodes.
+- The `sealos run` command must be run on the **first master node** of the Kubernetes cluster. **Installation on nodes outside the cluster is not currently supported**.
+- It is recommended to use a clean OS to create the cluster. **Do not install Docker manually!**
+- Supports most Linux distributions like Ubuntu, CentOS, Rocky Linux.
+- Compatible with all Kubernetes versions available on [Docker Hub](https://hub.docker.com/r/labring/kubernetes/tags).
+- Supports Containerd as the container runtime.
+- When installing on public clouds, use **private IP** addresses.
+
+## View Available Cluster Images
+
+All Sealos cluster images are hosted in the [cluster-image-docs](https://github.com/labring-actions/cluster-image-docs) repository.
+
+You can browse all Sealos images on Docker Hub here: [https://hub.docker.com/u/labring](https://hub.docker.com/u/labring).
+
+Use [Registry Explorer](https://explore.ggcr.dev/) to view all versions of the Kubernetes cluster images by entering `labring/kubernetes` and clicking "Submit Query":
+
+
+
+This will display all available tags for that image.
+
+:::info Note
+
+Kubernetes cluster stability improves with higher minor version numbers. For example in v1.28.x, x is the minor version. It is recommended to use versions with relatively high minor numbers. Currently, the highest v1.27 version is v1.27.7, while the highest v1.28 version is v1.28.3, so **v1.27.7 is recommended**. Choose the optimal Kubernetes version based on your needs.
+
+:::
+
+## Install Single-Node Kubernetes
+
+```shell
+# sealos version must >= v4.1.0
+$ sealos run labring/kubernetes:v1.27.7 labring/helm:v3.9.4 labring/cilium:v1.13.4 --single
+```
+
+## Install Kubernetes Cluster
+
+```shell
+$ sealos run labring/kubernetes:v1.27.7 labring/helm:v3.9.4 labring/cilium:v1.13.4 \
+--masters 192.168.64.2,192.168.64.22,192.168.64.20 \
+--nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd]
+```
+
+Note: labring/helm must come before labring/cilium.
+
+Parameter descriptions:
+
+| Parameter | Example | Description |
+| --- | --- | --- |
+| --masters | 192.168.0.2 | List of master node IP addresses |
+| --nodes | 192.168.0.3 | List of worker node IP addresses |
+| --ssh-passwd | [your-ssh-passwd] | SSH password |
+| kubernetes | labring/kubernetes:v1.25.0 | Kubernetes cluster image |
+
+Running the above commands directly on clean servers will install a highly available Kubernetes cluster without any extra steps.
+
+## Install Various Distributed Apps
+
+```shell
+sealos run labring/helm:v3.9.4 # Install Helm
+
+sealos run labring/openebs:v3.9.0 # Install OpenEBS
+
+sealos run labring/minio-operator:v4.5.5 labring/ingress-nginx:4.1.0
+```
+
+This provides highly available apps like Minio without worrying about dependencies.
+
+## Add Kubernetes Nodes
+
+Add worker nodes:
+
+```shell
+$ sealos add --nodes 192.168.64.21,192.168.64.19
+```
+
+Add master nodes:
+
+```shell
+$ sealos add --masters 192.168.64.21,192.168.64.19
+```
+
+## Delete Kubernetes Nodes
+
+Delete worker nodes:
+
+```shell
+$ sealos delete --nodes 192.168.64.21,192.168.64.19
+```
+
+Delete master nodes:
+
+```shell
+$ sealos delete --masters 192.168.64.21,192.168.64.19
+```
+
+## Clean Up Kubernetes Cluster
+
+```shell
+$ sealos reset
+```
+
+## Offline Installation
+
+For offline environments, first import the images - subsequent steps are identical to online installation.
+
+First, export images from an online environment:
+
+```shell
+$ sealos pull registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7
+
+$ sealos save -o kubernetes.tar registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7
+```
+
+### Import and Install
+
+Copy kubernetes.tar to the offline environment and import it with:
+
+```shell
+$ sealos load -i kubernetes.tar
+```
+
+The rest of the installation process is identical to online installation:
+
+```shell
+$ sealos images # Verify import succeeded
+
+$ sealos run registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 # Single node
+```
+
+### Quick Cluster Install
+
+You can also install Kubernetes by directly running:
+
+```shell
+$ sealos run kubernetes.tar
+```
+
+## Cluster Image Version Compatibility
+
+### Kubernetes Compatibility with Containerd
+
+It's advised to use Containerd as the container runtime interface (CRI) in Kubernetes clusters. Containerd stands out for its lightweight structure and high efficiency, and it's fully compatible with Docker. Kubernetes images that utilize Containerd benefit from enhanced performance and optimized resource usage. Here are the specific supported versions:
+
+| Kubernetes Version | Required Sealos Version | CRI Version | Cluster Image Version |
+| ------------------ | ----------------------- | ----------- |----------------------------|
+| `<1.25` | `>=v4.0.0` | v1alpha2 | labring/kubernetes:v1.24.0 |
+| `>=1.25` | `>=v4.1.0` | v1alpha2 | labring/kubernetes:v1.25.0 |
+| `>=1.26` | `>=v4.1.4-rc3` | v1 | labring/kubernetes:v1.26.0 |
+| `>=1.27` | `>=v4.2.0-alpha3` | v1 | labring/kubernetes:v1.27.0 |
+| `>=1.28` | `>=v5.0.0` | v1 | labring/kubernetes:v1.28.0 |
+
+The choice of Sealos and CRI versions is dependent on the Kubernetes version in question. For instance, Kubernetes v1.26.0 would require Sealos v4.1.4-rc3 or newer, along with the v1 CRI version.
+
+### Kubernetes Compatibility with Docker
+
+Alternatively, Docker can also be used as the container runtime. The following table provides a breakdown of compatible Kubernetes versions with their corresponding Sealos and CRI versions for Docker-based setups:
+
+| Kubernetes Version | Required Sealos Version | CRI Version | Cluster Image Version |
+|--------------------|-------------------------| ----------- |-----------------------------------|
+| `<1.25` | `>=v4.0.0` | v1alpha2 | labring/kubernetes-docker:v1.24.0 |
+| `>=1.25` | `>=v4.1.0` | v1alpha2 | labring/kubernetes-docker:v1.25.0 |
+| `>=1.26` | `>=v4.1.4-rc3` | v1 | labring/kubernetes-docker:v1.26.0 |
+| `>=1.27` | `>=v4.2.0-alpha3` | v1 | labring/kubernetes-docker:v1.27.0 |
+| `>=1.28` | `>=v5.0.0` | v1 | labring/kubernetes-docker:v1.28.0 |
+
+As with the Containerd setup, the appropriate Sealos and CRI versions must be matched with the specific version of Kubernetes being used. For a Kubernetes v1.26.0 setup, this means selecting Sealos v4.1.4-rc3 or later, and a v1 CRI version.
+
+### k3s Compatibility with Containerd
+
+| k3s Version | Required Sealos Version | Cluster Image Version |
+|-------------|-------------------------|-----------------------|
+| `>=1.24` | `>=v5.0.0` | labring/k3s:v1.24.0 |
+
+## Summary
+
+You can choose from a variety of image types and versions to suit your individual needs and preferences. Don't forget to consult the [CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) to stay informed about the latest updates and bug fixes for each version.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/images/registry-explorer.png b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/images/registry-explorer.png
new file mode 100644
index 000000000000..a9e28004026c
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/images/registry-explorer.png differ
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/install-cli.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/install-cli.md
new file mode 100644
index 000000000000..855f1e2dcad6
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/install-cli.md
@@ -0,0 +1,95 @@
+---
+sidebar_position: 1
+keywords: [sealos, sealos cli]
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Download Sealos CLI
+
+You can get the list of versions by running:
+
+```bash
+curl --silent "https://api.github.com/repos/labring/sealos/releases" | jq -r '.[].tag_name'
+```
+
+Note: While choosing the version, it's recommended to use a stable version. The versions like `v4.3.0-rcx`, `v4.3.0-alpha1` are pre-releases, use them with caution.
+
+Set the 'VERSION' environment variable to the latest VERSION number, or replace 'version' with the Sealos version you want to install:
+
+```shell
+VERSION=`curl -s https://api.github.com/repos/labring/sealos/releases/latest | grep -oE '"tag_name": "[^"]+"' | head -n1 | cut -d'"' -f4`
+```
+
+## Binary Auto Download
+
+```bash
+curl -sfL https://raw.githubusercontent.com/labring/sealos/${VERSION}/scripts/install.sh |
+ sh -s ${VERSION} labring/sealos
+
+```
+
+## Binary Manual Download
+
+
+
+
+```bash
+$ wget https://github.com/labring/sealos/releases/download/${VERSION}/sealos_${VERSION#v}_linux_amd64.tar.gz \
+ && tar zxvf sealos_${VERSION#v}_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin
+```
+
+
+
+
+```bash
+$ wget https://github.com/labring/sealos/releases/download/${VERSION}/sealos_${VERSION#v}_linux_arm64.tar.gz \
+ && tar zxvf sealos_${VERSION#v}_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin
+```
+
+
+
+
+## Package Management Tool Installation
+
+### DEB Repository
+
+```bash
+echo "deb [trusted=yes] https://apt.fury.io/labring/ /" | sudo tee /etc/apt/sources.list.d/labring.list
+sudo apt update
+sudo apt install sealos
+```
+
+### RPM Repository
+
+```bash
+sudo cat > /etc/yum.repos.d/labring.repo << EOF
+[fury]
+name=labring Yum Repo
+baseurl=https://yum.fury.io/labring/
+enabled=1
+gpgcheck=0
+EOF
+sudo yum clean all
+sudo yum install sealos
+```
+
+## Source Code Installation
+
+### Prerequisites
+1. `linux`
+2. `git`
+3. `golang` 1.20+
+4. `libgpgme-dev libbtrfs-dev libdevmapper-dev`
+
+If you are in an `arm64` environment, add the `:arm64` suffix.
+
+### Build
+
+```bash
+# git clone the repo
+git clone https://github.com/labring/sealos.git
+# just make it
+make build BINS=sealos
+```
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/_category_.json b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/_category_.json
new file mode 100644
index 000000000000..950b839be0aa
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/_category_.json
@@ -0,0 +1,3 @@
+{
+ "position" : 3
+}
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md
new file mode 100644
index 000000000000..d861e1f7ed0e
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md
@@ -0,0 +1,124 @@
+---
+sidebar_position: 3
+---
+
+# image-cri-shim User Guide
+
+## Overview
+
+image-cri-shim is a gRPC (Google Remote Procedure Call) shim based on CRI (Container Runtime Interface) and kubelet. CRI is the interface used in Kubernetes to interact with container runtimes, while kubelet is the Kubernetes component responsible for maintaining container runtime status and node-level resource management.
+
+The main functionality of image-cri-shim is automatic image name recognition, which eliminates the need for users to manually specify the image name when deploying containers with Kubernetes. This simplifies the container image deployment process and improves the convenience for users.
+
+In practical usage, image-cri-shim serves as middleware that receives requests from kubelet and forwards them to the container runtime. By automatically recognizing the image name, image-cri-shim streamlines the deployment process of container images and reduces user burden.
+
+```
++------------+ +----------------+ +-------------------+
+| User | | Kubelet | | image-cri-shim |
+| (Kubernetes| | (Node agent) | | (Middleware) |
+| Manifest) | | | | |
++-----+------+ +-------+--------+ +-------+-----------+
+ | | |
+ | YAML Manifest | |
+ |---------------> | |
+ | | |
+ | | |
+ | | CRI Request |
+ | |------------------------> |
+ | | |
+ | | Image Name |
+ | | Auto-Recognition |
+ | | |
+ | | |
+ | | CRI Response |
+ | | <------------------------+
+ | | |
+ | | |
+ | Container | |
+ | Deployment | |
+ | <----------------------| |
+ | | |
+ | | |
++------------+ +-------+--------+ +-------+-----------+
+
+```
+
+From the above flowchart, it can be seen that a user creates a Kubernetes YAML manifest containing container information and submits the manifest to kubelet. kubelet, which acts as an agent on Kubernetes nodes, is responsible for managing containers.
+Next, kubelet sends a CRI request to the image-cri-shim middleware. The main task of image-cri-shim is to automatically recognize the image name. It processes the CRI request and retrieves relevant image information. Once image-cri-shim identifies the image name, it returns the CRI response to kubelet.
+
+Finally, kubelet deploys the container using the image name obtained from image-cri-shim. This process is transparent to the user, as they do not need to manually specify the image name, simplifying the container deployment process and improving convenience.
+
+## Architecture
+
+The architecture of image-cri-shim is illustrated in the following diagram:
+
+
+
+## Usage
+
+```yaml
+shim: /var/run/image-cri-shim.sock
+cri: /run/containerd/containerd.sock
+address: http://sealos.hub:5000
+force: true
+debug: true
+timeout: 15m
+auth: admin:passw0rd
+
+registries:
+- address: http://172.18.1.38:5000
+ auth: admin:passw0rd
+```
+
+The above configuration is a YAML file used to set the parameters of image-cri-shim. It includes several key parameters, and the explanation for each parameter is as follows:
+
+1. shim: Specifies the UNIX socket file path
+
+for image-cri-shim. This path is used for communication with kubelet.
+2. cri: Specifies the UNIX socket file path for the container runtime (e.g., containerd). image-cri-shim uses this path to communicate with the container runtime.
+3. address: Defines the address of the image registry. In this example, the registry address is http://sealos.hub:5000.
+4. force: When set to true, image-cri-shim forcefully starts the shim without waiting for the cri to start.
+5. debug: When set to true, enables debug mode and outputs more logging information.
+6. timeout: Defines the timeout for image operations. In this example, the timeout is set to 15 minutes (15m).
+7. auth: Defines the authentication credentials for accessing the image registry. In this example, the username is admin and the password is passw0rd.
+
+Additionally, the configuration file includes a list of registries, which defines other image registries and their authentication credentials. In this example, there is only one additional registry:
+- address: The address of the registry is http://172.18.1.38:5000.
+- auth: The authentication credentials for accessing the registry. In this example, the username is admin and the password is passw0rd.
+
+This configuration file provides image-cri-shim with the necessary information to communicate with kubelet, the container runtime (such as containerd), and access and manage the image registry.
+
+Note: image-cri-shim is compatible with both CRI API v1alpha2 and v1.
+
+### Service Management
+
+image-cri-shim is typically run as a system service. To manage image-cri-shim, you can use system service management tools (such as systemctl) to start, stop, restart, or view the status of the service. First, make sure you have correctly installed image-cri-shim and configured it as a system service.
+
+1. Start the service: `systemctl start image-cri-shim`
+2. Stop the service: `systemctl stop image-cri-shim`
+3. Restart the service: `systemctl restart image-cri-shim`
+4. View the service status: `systemctl status image-cri-shim`
+
+### Log Management
+
+To view the logs of the image-cri-shim service, you can use the journalctl command. journalctl is a tool used to query and display system logs, and it is used in conjunction with the systemd service manager.
+
+Here are the commands to view the logs of the image-cri-shim service using journalctl:
+
+```shell
+journalctl -u image-cri-shim
+```
+
+This will display all logs of the image-cri-shim service. If you want to view the logs in real-time, you can add the -f option:
+
+```shell
+journalctl -u image-cri-shim -f
+```
+
+Additionally, you can filter the logs by time. For example, if you only want to view the logs from the past hour, you can use the following command:
+
+```shell
+journalctl -u image-cri-shim --since "1 hour ago"
+```
+
+These commands should help you view and analyze the logs of the image-cri-shim service, allowing you to better understand the service's operation status and potential issues.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png
new file mode 100644
index 000000000000..5c8d5f81b23a
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png differ
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/_category_.json b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/_category_.json
new file mode 100644
index 000000000000..e47d9e07a6d5
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/_category_.json
@@ -0,0 +1,3 @@
+{
+ "position" : 99
+}
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/images/01.webp b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/images/01.webp
new file mode 100644
index 000000000000..0cc05e478e9e
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/images/01.webp differ
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/lvscare.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/lvscare.md
new file mode 100644
index 000000000000..33bb00cc6c5e
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/lvscare.md
@@ -0,0 +1,74 @@
+# LVScare User Guide
+
+## Introduction
+
+LVScare is a lightweight load balancing and health checking tool based on IPVS technology. It provides real-time monitoring and management of backend services. Sealos, a cloud operating system, is a Kubernetes HA installation tool based on kubeadm that can be used to install and upgrade high availability Kubernetes clusters on any Linux system. When combined, Sealos and LVScare enable high availability of Kubernetes master nodes. Leveraging the lifecycle management capabilities of Sealos and the lightweight, zero-dependency, and high availability features of LVScare, it ensures the stability and reliability of Kubernetes clusters effectively.
+
+### How LVScare Works and Its Features
+
+LVScare monitors the health status of backend services (real servers) in real-time using IPVS. If a service becomes unavailable, LVScare immediately sets its weight to 0 (for graceful TCP termination) and removes it from the service list during the next check. When the service recovers, LVScare automatically adds it back to the service list. This design of LVScare makes it lightweight, zero-dependency, and highly available. It occupies fewer resources, is stable and reliable, and similar to the implementation of kube-proxy, it can ensure the continuous availability of services through IPVS-based local load balancing.
+
+## Integration of Sealos and LVScare
+
+In Sealos, we utilize the recommended approach of static pods to automatically configure and manage LVScare for achieving high availability of Kubernetes clusters. During the installation process of a Kubernetes cluster, Sealos automatically uses LVScare for health checking and load balancing of the master nodes. This means that even if one of the master nodes fails, it does not affect the overall functionality of the Kubernetes cluster.
+
+
+
+### Running LVScare as a Static Pod
+
+First, use the `sealctl static-pod` command to generate the configuration for LVScare and place it in the `/etc/kubernetes/manifests` directory. This allows all nodes in the Kubernetes cluster to access this configuration.
+
+```bash
+lvscare care --vs 10.103.97.12:6443 --rs 192.168.0.2:6443 --rs 192.168.0.3:6443 --rs 192.168.0.4:6443 --interval 5 --mode route
+```
+
+### Generating and Adjusting Static Pod Configuration
+
+To start LVScare on each node, we can use the following command to generate the configuration for the static pod:
+
+```bash
+sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443 --print
+```
+
+When there are changes in the master nodes, Sealos only needs to rerun the `sealctl static-pod` command to adjust the master nodes, simplifying the logic of maintaining static pods. Before joining the cluster nodes, we need to manually start the IPVS rules by calling `sealctl ipvs` (which directly calls the LVScare SDK) to maintain the IPVS cluster. After the node joins successfully, Kubernetes static pods can take over the IPVS rules.
+
+## Advantages of Using LVScare
+
+### High Availability
+
+By combining LVScare with Sealos, high availability of Kubernetes cluster master nodes can be achieved.
+
+### Health Checking Mechanism
+
+The health checking mechanism of LVScare can promptly detect and handle issues, preventing them from causing larger problems due to the failure of a single node.
+
+### Seamless Integration
+
+As a static pod in Seal
+
+os, LVScare can seamlessly integrate with other parts of the Kubernetes cluster.
+
+### Simplified Operations
+
+Sealos automatically configures and manages LVScare, greatly simplifying the operational tasks of the Kubernetes cluster.
+
+## LVScare Usage Example
+
+Please note that all real servers need to listen on the same host and be set in "route" mode. Then, you can run LVScare in the foreground. For example:
+
+```bash
+docker run -p 8081:80 --name echoserver1 -d cilium/echoserver
+docker run -p 8082:80 --name echoserver2 -d cilium/echoserver
+docker run -p 8083:80 --name echoserver3 -d cilium/echoserver
+lvscare care --vs 169.254.0.1:80 --rs 127.0.0.1:8081 --rs 127.0.0.1:8082 --rs 127.0.0.1:8083 --logger DEBG --health-schem http --health-path /
+```
+
+## Cleanup
+
+Finally, you can use the following command to clean up:
+
+```bash
+lvscare care --vs 169.254.0.1:80 --logger DEBG -C
+```
+
+Conclusion: LVScare is a lightweight load balancing and health checking tool based on IPVS. When seamlessly integrated with Sealos, it greatly improves the availability and performance of Kubernetes clusters. Give it a try and see how LVScare can help you better manage your Kubernetes cluster!
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/_category_.json b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/_category_.json
new file mode 100644
index 000000000000..a788e786e6c5
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/_category_.json
@@ -0,0 +1,3 @@
+{
+ "position" : 2
+}
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/cert.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/cert.md
new file mode 100644
index 000000000000..fe2b676aed32
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/cert.md
@@ -0,0 +1,30 @@
+---
+sidebar_position: 1
+---
+
+
+# Certificate Management with `cert`
+
+The `cert` command is used to generate the necessary certificate files for a Kubernetes cluster. In a Kubernetes cluster, certificates are used to ensure secure communication between components such as the API server, kubelet, and etcd. Certificates provide encryption using the Transport Layer Security (TLS) protocol to ensure the confidentiality and integrity of data during transit.
+
+The `sealctl cert` command generates certificates automatically based on the provided parameters. These parameters include node IP, node name, service CIDR, DNS domain, and optional additional alternate names. By generating and configuring these certificates, you can ensure secure communication within your Kubernetes cluster.
+
+
+```
+The `cert` command is used to generate Kubernetes certificates.
+
+Options:
+ --alt-names Alternate names, such as sealos.io or 10.103.97.2. Can specify multiple alternate names.
+ --node-name Node name, such as master0.
+ --service-cidr Service CIDR, such as 10.103.97.2/24.
+ --node-ip IP address of the node, such as 10.103.97.2.
+ --dns-domain DNS domain for the cluster. Default value is cluster.local.
+ --cert-path Path to Kubernetes certificate files. Default value is /etc/kubernetes/pki.
+ --cert-etcd-path Path to Kubernetes etcd certificate files. Default value is /etc/kubernetes/pki/etcd.
+
+Examples:
+ sealctl cert --alt-names sealos.io --alt-names 10.103.97.2 \
+ --node-name master0 --service-cidr 10.103.97.2/24 \
+ --node-ip 10.103.97.2 --dns-domain cluster.local
+
+```
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/cri.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/cri.md
new file mode 100644
index 000000000000..5bae776b35d0
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/cri.md
@@ -0,0 +1,45 @@
+---
+sidebar_position: 2
+---
+
+# CRI Container Management
+
+The `cri` command is used to manage and inspect the Container Runtime Interface (CRI) environment in a Kubernetes cluster. The container runtime is the underlying technology responsible for running containers, such as Docker, containerd, or CRI-O. In Kubernetes, the container runtime is used to start, stop, and manage containers to support workloads in the cluster.
+
+The `sealctl cri` command provides a set of subcommands that allow you to perform various operations related to the container runtime, such as checking if the runtime is Docker, if it is running, listing Kubernetes containers, deleting containers, pulling images, checking image existence, and retrieving CGroup driver information.
+
+By using the `sealctl cri` command, you can easily manage and inspect the container runtime environment in your Kubernetes cluster to ensure proper configuration and smooth operation.
+
+
+```shell
+sealctl cri [flags]
+```
+
+
+Subcommands:
+
+1. `socket`: Check the CRI socket.
+
+```shell
+sealctl cri socket
+```
+
+2. `cgroup-driver`: Get the cgroup driver of the container runtime.
+
+```shell
+sealctl cri cgroup-driver [--short]
+```
+
+- `--short`: Print only the result.
+
+Global flags:
+
+- `--socket-path`: Path to the CRI socket.
+- `--config`: Path to the CRI configuration file.
+
+Examples:
+
+```shell
+sealctl cri socket
+sealctl cri cgroup-driver --short
+```
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/hostname.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/hostname.md
new file mode 100644
index 000000000000..82bb133846af
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/hostname.md
@@ -0,0 +1,19 @@
+---
+sidebar_position: 3
+---
+
+# Get Hostname
+
+Get the hostname of the operating system:
+
+```shell
+sealctl hostname
+```
+
+Example:
+
+```shell
+sealctl hostname
+```
+
+Executing this command will return the hostname of the operating system. No additional parameters need to be passed.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/hosts.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/hosts.md
new file mode 100644
index 000000000000..a218a94e18f7
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/hosts.md
@@ -0,0 +1,56 @@
+---
+sidebar_position: 4
+---
+
+# Manage Hosts
+
+The `hosts` command is used to manage the hosts file of the operating system. The hosts file is a file used for domain name resolution to IP addresses and is typically used locally to override DNS resolution. By modifying the hosts file, you can assign a custom IP address to a specific domain name without relying on a DNS server.
+
+`sealctl hosts` provides the following three subcommands to manage the hosts file:
+
+1. `list`: List all entries in the current hosts file.
+2. `add`: Add a new domain-to-IP mapping to the hosts file.
+3. `delete`: Delete a specified domain-to-IP mapping from the hosts file.
+
+With these subcommands, you can conveniently view, add, and delete mappings in the hosts file, allowing you better control over domain name resolution to IP addresses.
+
+1. `sealctl hosts list`: List all entries in the current hosts file.
+
+ Example:
+
+ ```shell
+ sealctl hosts list
+ ```
+
+2. `sealctl hosts add`: Add a new entry to the hosts file.
+
+ Parameters:
+
+ - `--ip`: IP address (required)
+ - `--domain`: Domain name (required)
+
+ Example:
+
+ ```shell
+ sealctl hosts add --ip 192.168.1.100 --domain example.com
+ ```
+
+3. `sealctl hosts delete`: Delete an entry from the hosts file.
+
+ Parameters:
+
+ - `--domain`: Domain name to delete (required)
+
+ Example:
+
+ ```shell
+ sealctl hosts delete --domain example.com
+ ```
+
+Note: You can specify the path of the hosts file by adding the `--path` parameter after any `hosts` subcommand. The default path is `/etc/hosts` (Linux systems).
+
+Example:
+
+```shell
+sealctl hosts list --path /custom/path/hosts
+```
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/ipvs.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/ipvs.md
new file mode 100644
index 000000000000..c5ca9a4c7b49
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/ipvs.md
@@ -0,0 +1,71 @@
+---
+sidebar_position: 6
+---
+
+# Manage IPVS
+
+The `ipvs` command is used to create and manage local IPVS (IP Virtual Server) load balancing. IPVS is a module in the Linux kernel that allows high-performance load balancing to be implemented in the kernel space. The `ipvs` command achieves load balancing of services by managing the mapping between virtual servers and real servers.
+
+`sealctl ipvs` supports the following features:
+
+1. Creation and management of mappings between virtual servers and real servers.
+2. Health-check functionality to periodically check the health status of real servers and perform necessary online/offline operations.
+3. Support for two proxy modes: `route` and `link`.
+4. Support for configuring proxy scheduling algorithms (e.g., round-robin, weighted round-robin, etc.).
+5. Support for one-time creation of proxy rules (`--run-once` flag) or continuous operation and management of proxy rules.
+6. Support for cleanup: Existing IPVS rules can be cleared and the command will exit using the `-C` or `--clean` flag.
+
+With the `sealctl ipvs` command, users can easily create and manage high-performance load balancing services locally.
+
+**Usage**
+
+```shell
+sealctl ipvs [flags]
+```
+
+**Options**
+
+- `-C`, `--clean`: Clear existing rules and then exit.
+- `--health-insecure-skip-verify`: Skip verification of insecure requests (default is true).
+- `--health-path string`: URL path for probing (default is "/healthz").
+- `--health-req-body string`: Request body sent by the health checker.
+- `--health-req-headers stringToString`: HTTP request headers (default is []).
+- `--health-req-method string`: HTTP request method (default is "GET").
+- `--health-schem string`: HTTP scheme for the probe (default is "https").
+- `--health-status ints`: Valid status codes.
+- `-h`, `--help`: Help for ipvs.
+- `-i`, `--iface string`: Name of the virtual interface to create, behaving the same as kube-proxy (default is "lvscare"). Enabled only in mode=link.
+- `--interval durationOrSecond`: Health check interval (default is 0s).
+- `--ip ip`: Target IP as the routing gateway, used together with mode=route.
+- `--logger string`: Log level: DEBG/INFO (default is "INFO").
+- `--masqueradebit int`: IPTables masquerade bit. Enabled only in mode=link.
+- `--mode string`: Proxy mode: route/link (default is "route").
+- `--rs strings`: Real server addresses, e.g., 192.168.0.2:6443.
+- `--run-once`: Create proxy rules and then exit.
+- `--scheduler string`: Proxy scheduler (default is "rr").
+- `--vs string`: Virtual server address, e.g., 169.254.0.1:6443.
+
+**Global Options**
+
+- `--debug`: Enable debug logging.
+- `--show-path`: Enable displaying code path.
+
+**Documentation**
+
+To use the `sealctl ipvs` command, follow these steps:
+
+1. Provide the necessary options and parameters for the command.
+2. Execute the command, which will create or manage local IPVS load balancing.
+
+**Examples**
+
+Create proxy rules and then exit:
+
+```shell
+sealctl ipvs --vs 169.254.0.1:6443 --rs 192.168.0.2:6443 --run-once
+```
+
+Clear existing IPVS rules:
+
+```shell
+sealctl ipvs --clean
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/registry.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/registry.md
new file mode 100644
index 000000000000..91d03d6133a6
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/registry.md
@@ -0,0 +1,47 @@
+---
+sidebar_position: 5
+---
+
+# Registry Image Repository
+
+## Sealos: Detailed Guide and Usage of `sealctl registry serve` Command
+
+Sealos provides the `sealctl registry serve` command to facilitate the construction and management of Docker image repositories. This document provides a detailed guide and usage examples for the `sealctl registry serve` command.
+
+### Introduction
+
+The `sealctl registry serve` command is primarily used to start a Docker distribution image repository server. It supports two modes: `filesystem` and `inmem`.
+
+1. **Filesystem Mode**: In this mode, `sealctl` runs a Docker distribution image repository server for a specified directory. The image data is stored on disk in this mode. **This command is also used by Sealos for incremental image synchronization**.
+
+2. **In-memory Mode**: In this mode, `sealctl` runs an in-memory Docker distribution image repository server. The image data is only stored in memory, and the data will be lost when the process exits.
+
+### Command Options
+
+The `sealctl registry serve filesystem` command supports the following options:
+
+- `--disable-logging`: Disable logging output (default is false).
+- `--log-level`: Configure the log level (default is 'error').
+- `-p, --port`: The port the server listens on (default is a randomly unused port).
+
+### Usage Examples
+
+Here are some usage examples of the `sealctl registry serve` command:
+
+#### Start a Filesystem Image Repository Server
+
+```bash
+sealctl registry serve filesystem --port=5000
+```
+
+The above command starts a filesystem image repository server on port 5000.
+
+#### Start an In-memory Image Repository Server
+
+```bash
+sealctl registry serve inmem
+```
+
+The above command starts an in-memory image repository server. The server will lose stored data when the process exits.
+
+With the `sealctl registry serve` command, users can easily manage and operate Docker image repositories. It is a powerful and user-friendly tool for both development and production environments.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/sealctl.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/sealctl.md
new file mode 100644
index 000000000000..882ab560cb05
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/sealctl.md
@@ -0,0 +1,64 @@
+---
+sidebar_position: 0
+---
+
+# Sealctl User Guide
+
+Sealos provides `sealctl`, a command-line tool for operating with Sealos and cluster nodes. It includes the following subcommands:
+
+1. `cert`: Manages certificates for generating, viewing, and updating TLS certificates.
+2. `cri`: Manages Container Runtime Interface (CRI) configuration, such as Docker or containerd.
+3. `hostname`: Views or sets the system hostname.
+4. `hosts`: Manages the system's hosts file, which defines static hostname-to-IP address mappings.
+5. `ipvs`: Manages IP Virtual Server (IPVS) rules for load balancing and proxying.
+6. `registry`: Manages image repositories for storing container images in container repository format and repository management.
+7. `static_pod`: Manages static Pods and creates static Pod configurations.
+8. `token`: Generates and manages access tokens for authorizing access to Kubernetes clusters.
+
+With these subcommands, you can conveniently manage and configure your Sealos system, enabling control over containers, image repositories, networks, and other aspects.
+
+# Sealos Dependent Commands
+
+1. **Add Hosts**
+
+ Adds a new hosts record on the node with the specified IP address. The parameters include the IP address, hostname, and domain name. Use the `sealctl hosts add` command.
+
+2. **Delete Hosts**
+
+ Deletes a hosts record on the node with the specified IP address. The parameters include the IP address and domain name. Use the `sealctl hosts delete` command.
+
+3. **Hostname**
+
+ Retrieves the hostname of the node with the specified IP address. Use the `sealctl hostname` command.
+
+4. **IPVS Load Balancing**
+
+ Configures IPVS for load balancing on the node with the specified IP address. The parameters include the node IP address, virtual IP address, and a list of master node IP addresses. Use the `sealctl ipvs` command.
+
+5. **Clear IPVS Rules**
+
+ Clears the IPVS configuration on the node with the specified IP address. The parameters include the node IP address and virtual IP address. Use the `sealctl ipvs` command.
+
+6. **Generate Static Pods**
+
+ Deploys a static Pod (lvscare) on the node with the specified IP address. The parameters include the node IP address, virtual IP address, Pod name, image name, and a list of master node IP addresses. Use the `sealctl static-pod lvscare` command.
+
+7. **Manage Cluster Interact Authentication Token**
+
+ Generates a token for the node with the specified IP address. The parameters include the node IP address, configuration file, and certificate key. Use the `sealctl token` command.
+
+8. **Get Node's CGroup Information**
+
+ Retrieves the CRI CGroup information of the node with the specified IP address. Use the `sealctl cri cgroup` command.
+
+9. **Get Node's CRI Socket Information**
+
+ Retrieves the CRI socket information of the node with the specified IP address. Use the `sealctl cri socket` command.
+
+10. **Generate Self-signed HTTPS Certificates on Node**
+
+ Generates certificates for the node with the specified IP address. The parameters include the node IP address, a list of alternate names, host IP address, hostname, service CIDR, and DNS domain. Use the `sealctl cert` command.
+
+11. **Start Registry on Node**
+
+ Starts the registry on the specified node for incremental image synchronization. Use the `sealctl registry serve` command.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/static-pod.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/static-pod.md
new file mode 100644
index 000000000000..e82cbecad743
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/static-pod.md
@@ -0,0 +1,39 @@
+---
+sidebar_position: 7
+---
+
+# Static Pod Configuration
+
+The `static-pod` command is used to generate static Pods that are managed directly by kubelet instead of the API server. Static Pods are useful in certain scenarios, such as setting up and managing control plane components in a Kubernetes cluster.
+
+The `sealctl static-pod` command provides a convenient way to generate static Pod configuration files for specific purposes. Currently, it mainly supports generating the `lvscare` static Pod, which is a tool for managing IPVS rules.
+
+Using `sealctl static-pod lvscare`, you can generate the `lvscare` static Pod YAML file based on specified parameters such as VIP, master node addresses, and image name. This file can then be stored in the static Pod path of kubelet, and kubelet will automatically create and manage the corresponding Pod.
+
+**Usage**
+
+```shell
+sealctl static-pod lvscare [flags]
+```
+
+**Options**
+
+- `--vip`: Default VIP IP (default is "10.103.97.2:6443").
+- `--name`: Name of the generated lvscare static Pod.
+- `--image`: Image for the generated lvscare static Pod (default is `sealos.hub:5000/sealos/lvscare:latest`).
+- `--masters`: List of master addresses for the generated static Pod.
+- `--print`: Whether to print the YAML.
+
+**Examples**
+
+Generate the lvscare static Pod file and print the YAML:
+
+```shell
+sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443 --print
+```
+
+If the `--print` option is not used, the configuration file will be directly generated in `/etc/kubernetes/manifests` and the static Pod will be enabled:
+
+```shell
+sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443
+```
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/tar&&untar.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/tar&&untar.md
new file mode 100644
index 000000000000..40e57478c431
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/tar&&untar.md
@@ -0,0 +1,60 @@
+---
+sidebar_position: 9
+---
+
+# Tar and Untar in Sealos
+
+Sealos provides the `sealctl tar` and `sealctl untar` commands for compressing and decompressing files or directories. This guide explains how to use these two commands in detail.
+
+## The `sealctl tar` Command
+
+The `sealctl tar` command is used to compress a specified directory path into an archive file. Note that it will strip the parent directory.
+
+**Command Options:**
+
+- `--clear`: Whether to delete the source files after compression, default is false.
+- `--compression`: Compression algorithm, available options are tar/gzip/zstd/disable, default is disable.
+- `-o, --output`: Path of the archive file.
+
+**Basic Usage:**
+
+```bash
+sealctl tar [flags] [options]
+```
+
+## The `sealctl untar` Command
+
+The `sealctl untar` command is used to search for archive files that match a glob pattern in the specified source path (`src`) and extract them to the destination path (`dst`).
+
+**Command Options:**
+
+- `--clear`: Whether to delete the source files after extraction, default is false.
+- `-o, --output`: Path to extract the archive file.
+
+**Basic Usage:**
+
+```bash
+sealctl untar [flags] [options]
+```
+
+## Usage Examples
+
+Here are some examples of using the `sealctl tar` and `sealctl untar` commands:
+
+**Create a compressed file:**
+
+```bash
+sealctl tar --output=/path/to/archive.tar /path/to/source
+```
+
+The above command compresses the `source` directory into the `archive.tar` file.
+
+**Extract a compressed file:**
+
+```bash
+sealctl untar --output=/path/to/destination /path/to/archive.tar
+```
+
+The above command extracts the `archive.tar` file to the `destination` directory.
+
+With the `sealctl tar` and `sealctl untar` commands, users can easily compress and decompress files or directories. These commands are useful tools for file management, particularly in backup and file migration scenarios.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/token.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/token.md
new file mode 100644
index 000000000000..311856c7ebc8
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/token.md
@@ -0,0 +1,36 @@
+---
+sidebar_position: 8
+---
+
+# Token Management
+
+The `sealctl token` command is primarily used to generate a token for connecting master and worker nodes in a Kubernetes cluster. In a Kubernetes cluster, when you want to add a new worker node, you typically need to provide a token for authentication. This token ensures that only worker nodes with the correct token can join the cluster.
+
+The `sealctl token` command generates a token for authentication by accepting a configuration file (optional) and a certificate key (optional) as parameters. By default, if no configuration file and certificate key are provided, the command uses built-in default settings to generate the token.
+
+In summary, the `sealctl token` command is used to generate a token for authentication, allowing worker nodes to securely join a Kubernetes cluster. Using this command simplifies the process of adding nodes to the cluster and ensures the security of the cluster.
+
+**Usage**
+
+```shell
+sealctl token [config] [certificateKey]
+```
+
+**Parameters**
+
+- `config`: Configuration file (optional).
+- `certificateKey`: Certificate key (optional).
+
+**Examples**
+
+Generate a token with default parameters:
+
+```shell
+sealctl token
+```
+
+Generate a token with a custom configuration file and certificate key:
+
+```shell
+sealctl token my-config my-certificate-key
+```
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/_category_.json b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/_category_.json
new file mode 100644
index 000000000000..c9cb98244ebc
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/_category_.json
@@ -0,0 +1,3 @@
+{
+ "position" : 1
+}
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md
new file mode 100644
index 000000000000..5f05248f881e
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md
@@ -0,0 +1,39 @@
+---
+sidebar_position: 11
+---
+
+# Application Cluster Image Usage Guide
+
+Sealos provides a repository called [cluster-image](https://github.com/labring-actions/cluster-image) on GitHub for building and publishing application images for Kubernetes clusters. These images can be created by submitting code to this repository and can be published to `docker.io/labring/` as official application images. It supports building Docker container images as well as cluster images for applications.
+
+## Types of Images
+
+The repository supports three types of image builds:
+
+- **Application Cluster Images**: These are primarily for building application images using GitHub Actions and support both amd64 and arm64 architectures.
+- **Configuration Cluster Images**: These are mainly for building configuration images using GitHub Actions. They are not container images and are not architecture-specific. They typically contain configuration scripts or customizations to default configurations.
+- **Docker Images**: These are mainly for building container images using GitHub Actions and support both amd64 and arm64 architectures.
+
+## Workflow for Image Builds
+
+You can trigger image builds directly in the GitHub repository by creating an issue. Here are a few examples:
+
+- `/imagebuild_dockerimages helm v3.8.2 Key1=Value1,Key2=Value2`
+- `/imagebuild_configs coredns v0.0.1`
+- `/imagebuild_apps helm v3.8.2`
+
+The format of the image build commands for each type is `/imagebuild_ [Key=Value,...]`, where `` can be `dockerimages`, `configs`, or `apps`, `` and `` represent the application name and version respectively, and `[Key=Value,...]` is optional buildArg parameters used only for the `dockerimages` type.
+
+## Location of Image Configurations
+
+You can place your configuration files in the `applications///` directory, including Dockerfiles, Kubefiles, and init.sh scripts, among others. The init.sh script is typically used for downloading dependencies such as Helm and kubectl-minio. You can choose to use either a Dockerfile or Kubefile to define your image build logic.
+
+## Image Build Rules
+
+The build rules vary slightly for each type of image. Generally, you need to create different subdirectories under the application directory and place different types of files in them, which Sealos will use to build the images. The specific rules are as follows:
+
+1. `charts` directory: Place the Helm charts required for the cluster images. Kubernetes will scan the charts and fetch the images for building, and the registry directory will be placed at the same level as the Kubefile.
+2. `manifests` directory: Place the Kubernetes yaml configurations directly. Kubernetes will scan all the images in the manifests directory and build the registry directory, which will be placed at the same level as the Kubefile.
+3. `images/shim` directory: Store additional image lists and build the registry directory, which will be placed at the same level as the Kubefile.
+4. If templates are required, place files with the `.tmpl` extension in `etc`, `charts`, or `manifests`. These files can be rendered by the `sealos run` command with environment variables and the `.tmpl` extension will be removed. For example, a file named `aa.yaml.tmpl` will be rendered as `aa.yaml`. Please ensure that the file names do not conflict with existing files.
+5. The `registry` directory must be placed at the same level as the Kubefile. Otherwise, it will not be copied to the private repository of master0. Also, ensure that the registry is not stored in a chart, as it may cause slow scanning by Helm and potentially lead to OOM (out-of-memory) issues during image builds.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/add.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/add.md
new file mode 100644
index 000000000000..74bee99631c1
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/add.md
@@ -0,0 +1,60 @@
+---
+sidebar_position: 4
+---
+
+# Adding Cluster Nodes with `sealos add`
+
+`sealos add` is a command in the Sealos command-line tool used to add nodes to a cluster. This guide provides detailed instructions on how to use the command and its options.
+
+**Note: Make sure the number of control nodes is odd to ensure proper etcd leader election.**
+
+## Basic Usage
+
+### Adding Nodes
+
+To add nodes to the cluster, you can use the `--nodes` option:
+
+```bash
+sealos add --nodes x.x.x.x
+```
+
+In the above command, replace `x.x.x.x` with the IP address of the node you want to add.
+
+### Adding Control Nodes
+
+To add control nodes to the cluster, you can use the `--masters` option:
+
+```bash
+sealos add --masters x.x.x.x
+```
+
+### Adding Control and Regular Nodes Together
+
+If you want to add both control nodes and regular nodes to the cluster, you can use the `--masters` and `--nodes` options together:
+
+```bash
+sealos add --masters x.x.x.x --nodes x.x.x.x
+sealos add --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y
+```
+
+## Options
+
+The `sealos add` command provides the following options:
+
+- `--cluster='default'`: The name of the cluster to perform the add operation. Defaults to `default`.
+
+- `--masters=''`: The control nodes to be added.
+
+- `--nodes=''`: The nodes to be added.
+
+Each option can be followed by an argument.
+
+## Usage Example
+
+Here's an example usage that adds a node with the IP address `192.168.0.2` to the cluster:
+
+```bash
+sealos add --nodes 192.168.0.2
+```
+
+That's it for the usage guide of the `sealos add` command. We hope this helps you. If you have any questions or encounter any issues during the process, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/apply.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/apply.md
new file mode 100644
index 000000000000..d6e657705cf1
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/apply.md
@@ -0,0 +1,85 @@
+---
+sidebar_position: 1
+---
+
+# Starting a Cluster with `sealos apply`
+
+`sealos apply` is an important command in the Sealos command-line tool used to run cluster images in a Kubernetes cluster. This guide provides detailed instructions on how to use the command and its options.
+
+## Basic Usage
+
+The basic usage of the `sealos apply` command is as follows:
+
+```shell
+$ sealos apply -f Clusterfile
+```
+
+Clusterfile content:
+
+```yaml
+apiVersion: apps.sealos.io/v1beta1
+kind: Cluster
+metadata:
+ name: default
+spec:
+ # Server IP addresses and roles
+ hosts:
+ - ips:
+ - 192.168.0.2:22
+ - 192.168.0.3:22
+ - 192.168.0.4:22
+ roles:
+ - master
+ - amd64
+ - ips:
+ - 192.168.0.5:22
+ - 192.168.0.6:22
+ - 192.168.0.7:22
+ roles:
+ - node
+ - amd64
+ image:
+ - labring/kubernetes:v1.25.0
+ - labring/helm:v3.8.2
+ - labring/calico:v3.24.1
+ ssh:
+ passwd: xxx
+ pk: /root/.ssh/id_rsa
+ port: 22
+ user: root
+```
+
+This command will run cluster images in the Kubernetes cluster based on the specified `Clusterfile`.
+
+
+## Options
+
+The `sealos apply` command provides several options to customize its behavior:
+
+- `-f, --Clusterfile='Clusterfile'`: Specifies the Clusterfile to apply. Defaults to `Clusterfile`.
+- `--config-file=[]`: Specifies the path to a custom config file to replace or modify resources.
+- `--env=[]`: Sets environment variables to be used during command execution.
+- `--set=[]`: Sets values on the command line, usually for replacing template values.
+- `--values=[]`: Specifies values files to be applied to the `Clusterfile`, usually used for templating.
+
+Each option can be followed by one or more parameters. Multiple parameters are separated by commas.
+
+For example, you can use the `--set` option to set values on the command line:
+
+```shell
+sealos apply -f Clusterfile --set key1=value1,key2=value2
+```
+
+This command will set the values of `key1` and `key2` to `value1` and `value2`, and then apply the `Clusterfile`.
+
+Similarly, you can use the `--values` option to specify a values file:
+
+```shell
+sealos apply -f Clusterfile --values values.yaml
+```
+
+This command will apply the `Clusterfile` based on the values in the `values.yaml` file.
+
+**For more examples, please refer to the [Run Cluster](/self-hosting/lifecycle-management/operations/run-cluster/.md) section.**
+
+That's it for the usage guide of the `sealos apply` command. We hope this helps you. If you have any questions or encounter any issues during the process, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/build.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/build.md
new file mode 100644
index 000000000000..d2cb207046dc
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/build.md
@@ -0,0 +1,70 @@
+---
+sidebar_position: 6
+---
+
+# Building Images with `sealos build`
+
+The `build` command in Sealos is used to build OCI images using instructions from Sealfiles, Kubefiles, Dockerfiles, or Containerfiles. This is the fundamental command in Sealos for building cluster images.
+
+If no parameters are specified, Sealos will use the current working directory as the build context and look for instruction files. If no Sealfile, Kubefile, Dockerfile, or Containerfile is found, the build will fail.
+
+Here are some key options for the `build` command:
+
+1. `--all-platforms`: Attempts to build the image for all supported base image platforms.
+2. `--authfile`: Path to the authentication file.
+3. `--build-arg`: Provides an `argument=value` to the builder.
+4. `--build-context`: Provides additional build context to the builder as `argument=value`.
+5. `--creds`: Credentials to access the registry as `[username[:password]]`.
+6. `-D, --disable-compression`: Disables layer compression by default.
+7. `--env`: Sets environment variables for the image.
+8. `-f, --file`: Pathname or URL of the Dockerfile.
+9. `--force-rm`: Always removes intermediate containers after a build, even if the build fails.
+10. `--format`: Format for the manifest and metadata of the built image.
+11. `--from`: Replaces the value of the first FROM instruction in the Containerfile with the specified image name.
+12. `--http-proxy`: Passes the HTTP Proxy environment variable.
+13. `--isolation`: Process isolation `type` to use, can be 'oci' or 'chroot'.
+14. `--max-pull-procs`: Maximum number of goroutines to use for pulling images.
+15. `--platform`: Sets the OS/ARCH/VARIANT for the image to the provided value instead of the host's current operating system and architecture.
+16. `--pull`: Pulls the image from the registry, if new or not present in the store. Can be set to false, always, or never.
+17. `-q, --quiet`: Suppresses the build output and image read/write progress.
+18. `--retry`: Number of times to retry on push/pull failure.
+19. `--retry-delay`: Delay in seconds between retries on push/pull failure.
+20. `--rm`: Removes intermediate containers after a successful build.
+21. `--save-image`: Saves resolved images from a specific directory in the registry format.
+22. `--sign-by`: Signs the image with the GPG key of the specified `FINGERPRINT`.
+23. `-t, --tag`: Name and optionally a tag in the 'name:tag' format to apply to the built image.
+24. `--target`: Sets the target build stage to build.
+25. `--timestamp`: Sets the created timestamp to the specified epoch seconds for reproducible builds. Default is the current time.
+
+These options provide flexibility for various build requirements, including platform-specific builds, environment variable settings, build context management, image signing, and more. With the `--save-image` option, Sealos can automatically recognize and save the required images (including those resolved from image lists, Helm charts, and manifests) in the Docker Registry format.
+
+The process isolation mode `--isolation` supports two parameters: 'oci' and 'chroot'. Choose 'oci' mode if OCI is supported locally and 'chroot' mode if OCI is not supported.
+
+The `--save-image` option is used in Sealos build commands to automatically find and save the required images during the build process. In Sealos, building an image may involve other dependent images. These dependent
+
+images can come from image lists, Helm charts, or cluster manifests. When using the `--save-image` option, Sealos will automatically resolve these dependencies based on the build context and save them in the Docker Registry format.
+
+For example, here is an example using the `--save-image` option:
+
+```bash
+sealos build -t myapp:v1.0.0 -f Dockerfile .
+```
+
+In this example, Sealos will use the current directory as the build context, read the build instructions from the Dockerfile, and attempt to build an image tagged as `myapp:v1.0.0`. Additionally, Sealos will resolve all base images referenced in the Dockerfile's `FROM` instructions and save those images as well. These images will be saved in the Docker Registry format and can be pushed directly to a Docker Registry.
+
+If your build context also includes Helm charts or cluster manifests, Sealos will also resolve the images referenced in those files and save them accordingly.
+
+Overall, the `--save-image` option provides a convenient way for Sealos to handle image dependencies during the build process, greatly improving the convenience and efficiency of building images.
+
+Here are some detailed examples:
+
+- [Build with Image Manifests](/self-hosting/lifecycle-management/operations/build-image/build-image-image_list.md)
+- [Build with Deploy Manifests](/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md)
+- [Build with Helm Charts](/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md)
+- [Build with Binary](/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md)
+- [Build with go-template](/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md)
+- [Build with exec and scp](/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md)
+
+With the `build` command in Sealos, you can build OCI images based on various instruction files to provide the required images for Sealos. This process includes handling various instructions in Dockerfiles or other instruction files, such as `FROM`, `RUN`, `ADD`, etc., as well as managing image layers, labels, and more. The build process also involves pulling base images, running commands, saving the results, and more. Each step can be finely controlled and customized using the options mentioned above to accommodate different build requirements.
+
+That's the usage guide for the `sealos build` command. We hope this helps you. If you have any questions or encounter any issues during the process, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/cert.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/cert.md
new file mode 100644
index 000000000000..8a595e99bd44
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/cert.md
@@ -0,0 +1,44 @@
+---
+sidebar_position: 3
+---
+
+# Updating Cluster Certificates with `sealos cert`
+
+The `cert` command in Sealos is used to update the API server certificates in a cluster. This guide provides detailed instructions on how to use this command and its options.
+
+## Basic Usage
+
+To add domain names or IP addresses to the certificate, you can use the `--alt-names` option:
+
+```bash
+sealos cert --alt-names sealos.io,10.103.97.2,127.0.0.1,localhost
+```
+
+In the above command, replace `sealos.io,10.103.97.2,127.0.0.1,localhost` with the domain names and IP addresses you want to add.
+
+**Note**: It is recommended to back up the old certificates before performing this operation.
+
+After executing the `sealos cert` command, the API server certificates in the cluster will be updated. You don't need to manually restart the API server as Sealos will automatically handle the restart.
+
+## Options
+
+The `cert` command provides the following options:
+
+- `--alt-names='`': Adds domain names or IP addresses to the certificate, e.g., `sealos.io` or `10.103.97.2`.
+
+- `-c, --cluster='default'`: Specifies the name of the cluster on which to perform the exec operation. Default is `default`.
+
+Each option can be followed by an argument.
+
+## Certificate Verification
+
+After updating the certificates, you can use the following commands for verification:
+
+```bash
+kubectl -n kube-system get cm kubeadm-config -o yaml
+openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text
+```
+
+The above commands retrieve the kubeadm-config ConfigMap in the kube-system namespace and display detailed information about the apiserver.crt certificate.
+
+That concludes the usage guide for the `sealos cert` command. We hope this helps you. If you have any questions or encounter any issues during the process, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/commands.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/commands.md
new file mode 100644
index 000000000000..7753c45d0f93
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/commands.md
@@ -0,0 +1,56 @@
+---
+sidebar_position: 0
+---
+
+# Sealos Command Reference
+
+## Cluster Management Commands
+
+- `apply`: Runs cluster images within a Kubernetes cluster using Clusterfile.
+- `cert`: Updates the certificates of the Kubernetes API server.
+- `run`: Easily runs cloud-native applications.
+- `reset`: Resets all content in the cluster.
+- `status`: Views the status of the Sealos cluster.
+
+## Node Management Commands
+
+- `add`: Adds nodes to the cluster.
+- `delete`: Removes nodes from the cluster.
+
+## Remote Operation Commands
+
+- `exec`: Executes shell commands or scripts on the specified node.
+- `scp`: Copies files to the remote location of the specified node.
+
+## Experimental Commands
+
+- `registry`: Commands related to the image registry.
+
+## Container and Image Commands
+
+- `build`: Builds images using instructions from Sealfile or Kubefile.
+- `create`: Creates a cluster but does not run CMD, used for image inspection.
+- `inspect`: Inspects the configuration of containers or images.
+- `images`: Lists images in local storage.
+- `load`: Loads images from a file.
+- `login`: Logs into a container registry.
+- `logout`: Logs out of a container registry.
+- `manifest`: Operates on manifest lists and image indexes.
+- `merge`: Merges multiple images into one.
+- `pull`: Pulls images from a specified location.
+- `push`: Pushes images to the specified destination.
+- `rmi`: Removes one or more images from local storage.
+- `save`: Saves images to an archive file.
+- `tag`: Adds an additional name to a local image.
+
+## Other Commands
+
+- `completion`: Generates autocompletion scripts for the specified shell.
+- `docs`: Generates API reference documentation.
+- `env`: Prints all environment information used by Sealos.
+- `gen`: Generates a Clusterfile with all default settings.
+- `version`: Prints version information.
+
+The `--debug` flag in Sealos is a global flag used to enable debug mode for more detailed information about the system's operation when issues occur.
+
+For installation instructions, please refer to the [Sealos Installation Guide](/self-hosting/lifecycle-management/quick-start/installation); for a quick start guide, please refer to the [Quick Start Guide](/self-hosting/lifecycle-management/quick-start/.md).
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/create.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/create.md
new file mode 100644
index 000000000000..004a4d8dd4ca
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/create.md
@@ -0,0 +1,43 @@
+---
+sidebar_position: 6
+---
+
+# Create Working Directory
+
+`sealos create` is a command in the Sealos command line tool, mainly used for creating the cluster working directory without executing the CMD, so as to review the image. This guide will detail its usage and options.
+
+## Basic Usage
+
+The `sealos create` command is used to create a cluster working directory but does not actually run, mainly used for debugging or testing. It can output the address of the cluster image, you can check whether the content of the cluster image is consistent with the expectation.
+
+```bash
+sealos create docker.io/labring/kubernetes:v1.24.0
+```
+
+In the above command, `clustername` represents the name of the cluster you want to create.
+
+## Options
+
+The `sealos create` command provides the following options:
+
+- `-c, --cluster='default'`: The name of the cluster to be created but not actually run. The default is `default`.
+
+- `--platform='linux/arm64/v8'`: Set the operating system/architecture/version of the image to the provided value, rather than the current operating system and architecture of the host (for example, `linux/arm`).
+
+- `--short=false`: If true, only print the mount path.
+
+- `-e, --env=[]`: Specify environment variables used during the rendering of template files.
+
+Each option can be followed by an argument.
+
+## Example
+
+For example, you can use the following command to create a cluster named `mycluster`, but do not actually run it:
+
+```bash
+sealos create -e registryPort=8443 docker.io/labring/kubernetes:v1.24.0
+```
+
+This command will create a cluster working directory with an image name of `docker.io/labring/kubernetes:v1.24.0` and output the address of the cluster image. The `-e registryPort=8443` option specifies the environment variable used during the rendering of template files, where `registryPort` is set to `8443`. Please note that in this example, the cluster is not actually run.
+
+The above is the usage guide for the `sealos create` command, hope it helps. If you encounter any problems during use, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/delete.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/delete.md
new file mode 100644
index 000000000000..6750281c9c5f
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/delete.md
@@ -0,0 +1,68 @@
+---
+sidebar_position: 4
+---
+
+# Delete Cluster Node Removal
+
+`sealos delete` is a command in the Sealos command line tool, mainly used to remove nodes from the cluster. This guide will detail its usage and options.
+
+**Note to ensure the number of control nodes is odd to ensure normal election of etcd**
+
+## Basic Usage
+
+### Delete Node
+
+To remove a node from the cluster, you can use the `--nodes` option:
+
+```bash
+sealos delete --nodes x.x.x.x
+```
+
+In the above command, `x.x.x.x` should be replaced with the IP address of the node you want to delete. If you accidentally delete the wrong node, you can use the `sealos add` command to recover it:
+
+```bash
+sealos add --nodes x.x.x.x
+```
+
+### Delete Control Node
+
+To remove a control node from the cluster, you can use the `--masters` option:
+
+```bash
+sealos delete --masters x.x.x.x
+```
+
+Please note, if the `--masters` parameter is specified, sealos will delete your control node.
+
+### Delete Control Node and Node
+
+If you want to delete both control nodes and nodes at the same time, you can use the `--masters` and `--nodes` options at the same time:
+
+```bash
+sealos delete --masters x.x.x.x --nodes x.x.x.x
+sealos delete --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y
+```
+
+## Options
+
+The `sealos delete` command provides the following options:
+
+- `--cluster='default'`: The name of the cluster to which the deletion operation applies. The default is `default`.
+
+- `--force=false`: You can enter a `--force` flag to force delete nodes.
+
+- `--masters=''`: The control nodes to be removed.
+
+- `--nodes=''`: The nodes to be removed.
+
+Each option can be followed by an argument.
+
+## Usage Example
+
+Here is a usage example that deletes a node with the IP address of `192.168.0.2`:
+
+```bash
+sealos delete --nodes 192.168.0.2
+```
+
+The above is the usage guide for the `sealos delete` command, hope it helps. If you encounter any problems during use, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/env.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/env.md
new file mode 100644
index 000000000000..4331432abf1c
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/env.md
@@ -0,0 +1,39 @@
+---
+sidebar_position: 7
+---
+
+# Environment Variables
+
+`sealos env` is a command in the Sealos command-line tool, used to display the current environment variables supported by sealos and their current values.
+
+## Basic Usage
+
+### Viewing Environment Variables
+
+To view the environment variables, you can use the `sealos env` command:
+
+```bash
+sealos env
+```
+
+### Viewing Environment Variables and Descriptions
+
+To view the environment variables and their descriptions, you can use the `sealos env -v` command:
+
+```bash
+sealos env -v
+```
+
+
+## How to Set Environment Variables
+
+```shell
+BUILDAH_LOG_LEVEL=debug sealos images
+```
+
+```shell
+SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t xxx .
+```
+
+
+That's the usage guide for the `sealos env` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/exec.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/exec.md
new file mode 100644
index 000000000000..f7ce53e19ffd
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/exec.md
@@ -0,0 +1,51 @@
+---
+sidebar_position: 5
+---
+
+# Execute Command
+
+`sealos exec` is a command in the Sealos command-line tool, used to execute Shell commands or scripts on specified cluster nodes. This guide will detail its usage and options.
+
+## Basic Usage
+
+The basic `sealos exec` command format is as follows:
+
+```bash
+sealos exec "shell command or script"
+```
+
+In the above command, `shell command or script` is the Shell command or script you want to execute on the cluster nodes.
+
+## Options
+
+The `sealos exec` command provides the following options:
+
+- `-c, --cluster='default'`: The name of the cluster on which the command will be executed. The default is `default`.
+
+- `--ips=[]`: Run commands on nodes with specified IP addresses.
+
+- `-r, --roles='':` Run commands on nodes with specified roles. Currently supports master,node,registry.
+
+Each option can be followed by one or more parameters.
+
+## Examples
+
+For example, you can use the following command to view the contents of the `/etc/hosts` file on all nodes of the default cluster:
+
+```bash
+sealos exec "cat /etc/hosts"
+```
+
+If you want to view the contents of the `/etc/hosts` file on nodes with `master` and `node` roles in a cluster named `my-cluster`, you can use the following command:
+
+```bash
+sealos exec -c my-cluster -r master,node "cat /etc/hosts"
+```
+
+If you only want to view the contents of the `/etc/hosts` file on a node with the IP address `172.16.1.38`, you can use the following command:
+
+```bash
+sealos exec -c my-cluster --ips 172.16.1.38 "cat /etc/hosts"
+```
+
+That's the usage guide for the `sealos exec` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/gen.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/gen.md
new file mode 100644
index 000000000000..7601c36f62ce
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/gen.md
@@ -0,0 +1,49 @@
+---
+sidebar_position: 3
+---
+
+# Generate Cluster Configuration
+
+Sealos' `gen` command is used to generate a Kubernetes cluster configuration file (Clusterfile), which can then be applied using the `sealos apply` command. The `gen` command can help users quickly generate a basic configuration file, which can then be modified and adjusted according to their needs.
+
+Here are the basic usage of `sealos gen` command and some common examples:
+
+1. Generate a single-node cluster with default configuration:
+
+ ```bash
+ sealos gen labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1
+ ```
+
+Notice: labring/helm should be set before labring/calico.
+
+2. Generate a cluster that includes multiple images and specifies the master and worker nodes:
+
+ ```bash
+ sealos gen labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \
+ --masters 192.168.0.2,192.168.0.3,192.168.0.4 \
+ --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx'
+ ```
+
+Notice: labring/helm should be set before labring/calico.
+
+3. Specify SSH port, for servers using the same SSH port:
+
+ ```bash
+ sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \
+ --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd 'xxx'
+ ```
+
+ For servers using different SSH ports:
+
+ ```bash
+ sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3:23,192.168.0.4:24 \
+ --nodes 192.168.0.5:25,192.168.0.6:25,192.168.0.7:27 --passwd 'xxx'
+ ```
+
+After the Clusterfile is generated, users can modify this file according to their needs. Add or modify environment variables; modify the cluster cidr configuration. Once the modifications are done, users can use the `sealos apply` command to create or update the cluster based on this configuration file.
+
+Example explanations:
+
+- [Custom Configuration Installation](/self-hosting/lifecycle-management/operations/run-cluster/gen-apply-cluster.md)
+
+That's the usage guide for the `sealos gen` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/images.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/images.md
new file mode 100644
index 000000000000..c3d79a4d6f4f
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/images.md
@@ -0,0 +1,65 @@
+---
+sidebar_position: 6
+---
+
+# Image List
+
+Sealos' `images` command is primarily used to view locally stored images. Users can use it to view all local images or to filter and view specific images. The command supports various parameters to help users view and manage images more conveniently.
+
+## Basic Usage
+
+The basic `sealos images` command will display all non-intermediate stage local images, for example:
+
+```bash
+sealos images
+```
+
+This will display all final stage images stored locally.
+
+## Examples
+
+Here are some common examples of the `sealos images` command:
+
+1. Display all images, including intermediate images built:
+
+ ```bash
+ sealos images --all
+ ```
+
+2. Display a specific image:
+
+ ```bash
+ sealos images [imageName]
+ ```
+
+3. Display images in a specified Go template format:
+
+ ```bash
+ sealos images --format '{{.ID}} {{.Name}} {{.Size}} {{.CreatedAtRaw}}'
+ ```
+
+## Parameters
+
+Here are some common parameters for the `sealos images` command:
+
+- `-a, --all`: Display all images, including intermediate images built.
+
+- `--digests`: Display the digests of images.
+
+- `-f, --filter`: Filter output based on provided conditions.
+
+- `--format`: Beautify the images printout using a Go template.
+
+- `--history`: Display the naming history of images.
+
+- `--json`: Output in JSON format.
+
+- `--no-trunc`: Do not truncate the output.
+
+- `-n, --noheading`: Do not print column headings.
+
+- `-q, --quiet`: Only display image IDs.
+
+By combining these parameters, users can easily retrieve and manage locally stored images. For example, using the `--all` parameter displays all images, including intermediate ones; using the `--filter` parameter filters images based on specific conditions; using the `--json` parameter outputs image information in JSON format, facilitating programmatic processing, etc.
+
+That's the usage guide for the `sealos images` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/inspect.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/inspect.md
new file mode 100644
index 000000000000..7ff8dfff3e87
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/inspect.md
@@ -0,0 +1,79 @@
+---
+sidebar_position: 6
+---
+
+# Inspect Details
+
+Sealos' `inspect` command is primarily used to view the configuration information of build containers or built images. The command supports viewing detailed information about an image or container, including its metadata, environment variables, startup commands, etc.
+
+## Basic Usage
+
+Use the `sealos inspect` command to view the configuration information of a specified container or image. For example, to view the configuration of a specified container:
+
+```bash
+sealos inspect containerID
+```
+
+Or to view the configuration of a specified image:
+
+```bash
+sealos inspect --type image imageWithTag
+```
+
+## Examples
+
+Here are some common examples of the `sealos inspect` command:
+
+1. View container configuration:
+
+ ```bash
+ sealos inspect containerID
+ ```
+
+2. View image configuration:
+
+ ```bash
+ sealos inspect --type image imageWithTag
+ ```
+
+3. View configuration information of an image ID:
+
+ ```bash
+ sealos inspect --type image @imageID # Or just input the imageID, '@' is optional
+ ```
+
+4. View configuration information of a remote image repository:
+
+ ```bash
+ sealos inspect --type image docker://alpine:latest
+ ```
+
+5. View configuration information of an image in a local OCI archive file:
+
+ ```bash
+ sealos inspect --type image oci-archive:/abs/path/of/oci/tarfile.tar
+ ```
+
+6. View configuration information of an image in a local Docker archive file:
+
+ ```bash
+ sealos inspect --type image docker-archive:/abs/path/of/docker/tarfile.tar
+ ```
+
+7. Display image environment variables in Go template format:
+
+ ```bash
+ sealos inspect --format '{{.OCIv1.Config.Env}}' alpine
+ ```
+
+## Parameters
+
+Here are some common parameters for the `sealos inspect` command:
+
+- `-f, --format`: Display output results in Go template format. **Template structure code [InspectOutput](https://github.com/labring/sealos/blob/f8a17787822714c5fdf21f2a75cc86fadb88adfa/pkg/buildah/inspect.go#L189)**
+
+- `-t, --type`: Specify the type to view, which can be a container (`container`) or an image (`image`).
+
+Depending on your needs, you can combine these parameters to get specific configuration information. For example, using the `-t` parameter can specify whether you want to view the configuration information of the container or the image; using the `-f` parameter, you can define a specific output format, which is convenient for processing or parsing the output results.
+
+That's the usage guide for the `sealos inspect` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/load.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/load.md
new file mode 100644
index 000000000000..49a9f84a3069
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/load.md
@@ -0,0 +1,25 @@
+---
+sidebar_position: 6
+---
+
+# Load Image
+
+`sealos load` is a command used to load images from archive files. This is very useful when you need to import images from existing archive files, especially in environments without a network connection.
+
+## Usage:
+
+`sealos load [flags] [options]`
+
+## Parameters:
+
+Here are the parameters for the `sealos load` command:
+
+- `-i, --input=''`: Load image from a tar archive file.
+
+## Examples:
+
+- Load an image from an archive file: `sealos load -i myimage.tar`
+
+Note that when using the `sealos load` command, you need to ensure that the specified archive file exists and is correctly formatted. If you encounter problems when importing images, you may need to check your archive files to ensure they have not been corrupted or incorrectly formatted.
+
+That's the usage guide for the `sealos load` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/login.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/login.md
new file mode 100644
index 000000000000..b755c25099a6
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/login.md
@@ -0,0 +1,39 @@
+---
+sidebar_position: 6
+---
+
+# Login to Repository
+
+The `sealos login` command is used to log into the container registry on the specified server. After logging into the registry, you can pull and push images.
+
+## Usage:
+
+`sealos login [flags] [options] registryName`
+
+## Parameters:
+
+Here are the parameters for the `sealos login` command:
+
+- `--authfile=''`: Path to the authentication file. It can be overridden with the REGISTRY_AUTH_FILE environment variable.
+
+- `--cert-dir=''`: Use certificates at the specified path to access the image repository.
+
+- `--get-login=true`: Return the current login user for the registry.
+
+- `-k, --kubeconfig=''`: Log into the sealos image repository hub.sealos.io using kubeconfig.
+
+- `-p, --password=''`: Password for the registry.
+
+- `--password-stdin=false`: Take the password from standard input.
+
+- `-u, --username=''`: Username for the registry.
+
+- `-v, --verbose=false`: Write more detailed information to standard output.
+
+## Examples:
+
+- Log into the quay.io registry: `sealos login -u myusername -p mypassword quay.io`
+
+Please note that when using the `sealos login` command, you need to make sure that you provide the correct username and password, otherwise the login process might fail. If you encounter problems during the login process, you might need to check your username and password to ensure they have not been entered incorrectly or forgotten.
+
+That's the usage guide for the `sealos login` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/logout.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/logout.md
new file mode 100644
index 000000000000..8584054cb52b
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/logout.md
@@ -0,0 +1,26 @@
+---
+sidebar_position: 6
+---
+
+# Logout from Repository
+
+The `sealos logout` command is used to remove the locally cached account and password of the image repository on the specified server.
+
+## Usage:
+
+`sealos logout [flags] [options] registryName`
+
+## Parameters:
+
+Here are the parameters for the `sealos logout` command:
+
+- `--authfile=''`: Path to the authentication file. It can be overridden with the REGISTRY_AUTH_FILE environment variable.
+
+- `-a, --all=false`: Delete all authentication information.
+
+
+## Examples:
+
+- Logout from the quay.io image repository: `sealos logout quay.io`
+
+That's the usage guide for the `sealos logout` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/manifest.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/manifest.md
new file mode 100644
index 000000000000..fa8e9160c617
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/manifest.md
@@ -0,0 +1,20 @@
+---
+sidebar_position: 6
+---
+
+# Image Manifest
+
+The `manifest` command of Sealos is used to create, modify, and push manifest lists and image indexes. These functions are mainly used to handle multi-architecture support of images. In Docker and OCI image specifications, manifest lists (also known as "fat manifests") or image indexes allow an image tag (like `myimage:latest`) to be used on various hardware architectures (like amd64, arm64, ppc64le, etc.).
+
+Here are some of the major `manifest` subcommands:
+
+1. `create`: Creates a new manifest list or image index. Example: `sealos manifest create localhost/list`
+2. `add`: Adds an image to the manifest list or image index. Example: `sealos manifest add localhost/list localhost/image`
+3. `annotate`: Adds or updates information in entries of the manifest list or image index. Example: `sealos manifest annotate --annotation A=B localhost/list localhost/image`
+4. `inspect`: Displays the content of the manifest list or image index. Example: `sealos manifest inspect localhost/list`
+5. `push`: Pushes the manifest list or image index to the registry. Example: `sealos manifest push localhost/list transport:destination`
+6. `remove` and `rm`: Removes entries from the manifest list or image index, or completely deletes the manifest list or image index. Example: `sealos manifest remove localhost/list sha256:entryManifestDigest` or `sealos manifest rm localhost/list`
+
+With the `sealos manifest` command, you can flexibly manage manifest lists or image indexes, providing support for multi-architecture Docker or OCI images. Users can create custom manifest lists according to their needs, making it convenient to deploy and run Docker images on different hardware architectures.
+
+Users who want to build multi-architecture images through the manifest command can refer to the document [Building Cluster Images that Support Multiple Architectures](/self-hosting/lifecycle-management/operations/build-image/build-multi-arch-image.md).
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/merge.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/merge.md
new file mode 100644
index 000000000000..74db43c175d1
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/merge.md
@@ -0,0 +1,21 @@
+---
+sidebar_position: 6
+---
+
+# Merge Image Merging
+
+The primary function of the `merge` command in Sealos is to merge multiple images into one. It does this by reading the Dockerfiles of each input image, merging the commands and layer structures into a new image. The running logic of this command is very similar to the `build` command, and many of the parameters are the same.
+
+This function is very useful when multiple images share layers, as it can reduce the size of the image and save storage space. Moreover, since the merged image contains all the functionalities of multiple images, it can help simplify application deployment.
+
+Here is a basic usage example of `sealos merge`:
+
+```bash
+sealos merge -t new:0.1.0 kubernetes:v1.19.9 mysql:5.7.0 redis:6.0.0
+```
+
+In this example, the three images `kubernetes:v1.19.9`, `mysql:5.7.0`, and `redis:6.0.0` are merged into a new image `new:0.1.0`.
+
+The `sealos merge` command provides rich options to customize the merging process, such as `--all-platforms` to attempt to build images for all base image platforms, `--build-arg` to provide parameters to the builder, `--no-cache` to disable existing cached images, and so on.
+
+Please note that the `sealos merge` command builds a new image based on the Dockerfiles of each input image. Therefore, if the Dockerfiles of the input images are incompatible, or there are any build errors, this command may fail. When using the `sealos merge` command, make sure you understand the Dockerfile of each input image and adjust as needed.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/pull.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/pull.md
new file mode 100644
index 000000000000..74ae19b2b566
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/pull.md
@@ -0,0 +1,51 @@
+---
+sidebar_position: 6
+---
+
+# Pull Image
+
+The `sealos pull` command is a very useful one that allows you to download images from the container image repository and store them locally. Users can obtain images by their tags or digests. If no tag is specified, the image with the 'latest' tag (if it exists) will be downloaded by default.
+
+By using this command, users can easily download the required images from remote repositories, greatly improving work efficiency.
+
+## Usage:
+
+`sealos pull [flags] [options] imageName`
+
+## Parameters:
+
+The following are the parameters of the `sealos pull` command:
+
+- `-a, --all-tags=false`: Download all tagged images in the repository.
+
+- `--authfile=''`: The path to the authentication file. The REGISTRY_AUTH_FILE environment variable can be used to override it.
+
+- `--cert-dir=''`: The specified path to the certificate for accessing the image repository.
+
+- `--creds=''`: Use `[username[:password]]` to access the image repository.
+
+- `--decryption-key=[]`: The key needed to decrypt the image.
+
+- `--platform=[linux/arm64/v8]`: When choosing an image, prioritize the specified OS/ARCH over the current operating system and architecture.
+
+- `--policy='missing'`: Set the policy, the optional values include 'missing', 'always', 'never'.
+
+- `-q, --quiet=false`: Do not output progress information when pulling images.
+
+- `--remove-signatures=false`: Do not copy signatures when pulling images.
+
+- `--retry=3`: The number of retries when the pull fails.
+
+- `--retry-delay=2s`: The delay between retries when the pull fails.
+
+## Examples:
+
+- Pull an image: `sealos pull my-image:latest`
+
+- Pull an image from the Docker daemon: `sealos pull docker-daemon:my-image:tag`
+
+- Pull an image from a specific repository: `sealos pull myregistry/myrepository/my-image:tag`
+
+- Pull multiple images: `sealos pull imageID1 imageID2 imageID3`
+
+The above is a usage guide for the `sealos push` command, hoping to help you. If you encounter any problems during use, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/push.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/push.md
new file mode 100644
index 000000000000..6b9b2a5677a0
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/push.md
@@ -0,0 +1,107 @@
+---
+sidebar_position: 6
+---
+
+# Push Image
+
+`sealos push` is a command in the Sealos command-line tool, used to push images to a specified location. This command is particularly useful when you need to push local Docker images to a remote image repository. This guide will provide a detailed introduction to its usage.
+
+## Basic Usage
+
+The basic format of the `sealos push` command is as follows:
+
+```bash
+sealos push IMAGE_ID DESTINATION
+```
+
+In the above command, `IMAGE_ID` is the ID of the image you want to push, and `DESTINATION` is the location where you want to push it. The `DESTINATION` uses the "transport:details" format. If not specified, the source IMAGE is reused as the DESTINATION.
+
+In Sealos, the transport defines the format and location of the source image and the target image during the copying process. Here are the various transports supported by Sealos:
+
+1. `containers-storage`: This transport is used for storing and managing containers running locally, such as images of containers created using Podman or CRI-O.
+
+2. `dir`: This transport stores the image in a directory in the local file system, the structure of which conforms to the OCI layout.
+
+3. `docker`: This transport is used for interacting with Docker registries, such as Docker Hub or any other compatible private registry.
+
+4. `docker-archive`: This transport stores the image as a local Docker tar file (`.tar`), which is Docker's native format.
+
+5. `docker-daemon`: This transport is used for interacting with the local Docker daemon. It can extract images from the Docker daemon or push images to the Docker daemon.
+
+6. `oci`: This transport stores the image in a directory that conforms to the OCI layout, which is an open container image format.
+
+7. `oci-archive`: This transport stores the image as a local OCI tar file (`.tar`).
+
+8. `ostree`: This transport stores the image in an OSTree repository, which is a file system that supports atomic upgrades and rollbacks.
+
+9. `sif`: This is the Singularity SIF format, mainly used for high-performance computing and data-intensive applications.
+
+Examples:
+
+- Push an image to a Docker registry: `sealos push my-image:latest docker://my-registry.example.com/my-image:latest`
+
+- Export an image from the Docker daemon: `sealos push docker-daemon:my-image:latest dir:/path/to/save/`
+
+- Push an image to local container storage: `sealos push my-image:latest containers-storage:my-new-image:latest`
+
+## Example
+
+For example, you can use the following command to push an image to the `registry.example.com` repository:
+
+```bash
+sealos push my_image_id docker://registry.example.com/my_repository:my_tag
+```
+
+## Optional Parameters
+
+- `--all`: This parameter is used to push all images referred by the manifest list.
+
+- `--authfile`: This parameter is used to specify the path to the authentication file. The REGISTRY_AUTH_FILE environment variable can be overridden.
+
+- `--cert-dir`: This parameter is used to specify the path to the certificate required to access the registry.
+
+- `--compression-format`: This parameter is used to specify the compression format to be used.
+
+- `--compression-level`: This parameter is used to specify the compression level to be used.
+
+- `--cr-option`: This parameter is used to control whether the image's Custom Resources (CR) are pushed to the target image repository.
+
+ Specifically, the optional values for this parameter include:
+
+ - "yes": The image and its associated CR will be pushed to the target image repository.
+
+ - "no": Only push the image, do not push any CR.
+
+ - "only": Only push the CR, do not push the image itself.
+
+ - "auto": Automatically decide whether to push based on the actual status of the image and CR. For example, if the CR has changed or does not exist in the target repository, it will be pushed.
+
+ Please note, this parameter is mainly used when dealing with images containing custom resources (such as Kubernetes CRD objects), allowing you to more flexibly control the push process of images and CRs.
+
+- `--creds`: This parameter is used to access the registry, in the form of `[username[:password]]`.
+
+- `--digestfile`: This parameter, after copying the image, writes the digest of the resulting image to a file.
+
+- `-D`, `--disable-compression`: This parameter is used to not compress layers.
+
+- `--encrypt-layer`: This parameter is used to specify the layer to be encrypted. 0-index layer index supports negative index (for example, 0 is the first layer, -1 is the last layer). If not defined, all layers will be encrypted when the encryption-key flag is specified.
+
+- `--encryption-key`: This parameter is used to specify the key needed to encrypt the image, used together with the encryption protocol (for example, jwe:/path/to/key.pem).
+
+- `-f`, `--format`: This parameter is used to specify the type of manifest to use in the target (oci, v2s1, or v2s2) (default is the source's
+
+manifest type, with fallback).
+
+- `-q`, `--quiet`: This parameter is used to not output progress information when pushing the image.
+
+- `--remove-signatures`: This parameter is used to not copy signatures when pushing the image.
+
+- `--retry`: This parameter is used to specify the number of retries when the push/pull fails.
+
+- `--retry-delay`: This parameter is used to specify the delay between retries when the push/pull fails.
+
+- `--rm`: This parameter is used to delete the manifest list after the push is successful.
+
+- `--sign-by`: This parameter is used to sign the image using a GPG key with the specified `FINGERPRINT`.
+
+That's the guide to using the `sealos push` command, and I hope it's helpful to you. If you encounter any problems during use, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/registry.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/registry.md
new file mode 100644
index 000000000000..dc886345c6c2
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/registry.md
@@ -0,0 +1,233 @@
+---
+sidebar_position: 8
+---
+
+# Registry Image Repository Commands
+
+## Sealos: Detailed Explanation and User Guide for the `sealos registry save` Command
+
+The `registry save` command is used to pull remote Docker images to the local and save them in a specified directory. This is particularly useful for deploying container images in offline or intranet environments.
+
+When executing the `registry save` command, it will automatically obtain the `sealos login` authentication information for repository authentication.
+
+**Usage Guide**
+
+1. Use context to automatically retrieve images
+
+ Pull and save images in the default manner. This mode will automatically parse the `charts` directory, `manifests` directory, and `images` directory to get the image list.
+
+ **Usage Example**
+
+ ```shell
+ sealos registry save --registry-dir=/tmp/registry1 my-context
+ ```
+
+
+2. Specified image list mode
+
+ Pass the image list using arguments
+
+ **Usage Example**
+
+ ```shell
+ sealos registry save --registry-dir=/tmp/registry2 --images=docker.io/library/busybox:latest
+ ```
+
+**Options**
+
+The following options apply to the `save` command and its subcommands:
+
+- `--max-procs`: The maximum number of parallel processes used to pull images.
+- `--registry-dir`: The local directory to save images.
+- `--arch`: The target architecture of the image, such as: `amd64`, `arm64`, etc.
+- `--images`: The image list to be pulled and saved, separated by commas. For example: "my-image1:latest,my-image2:v1.0".
+
+## Sealos: Detailed Explanation and User Guide for the `sealos registry serve` Command
+
+In the process of managing Docker image repositories, Sealos provides the `sealos registry serve` command to facilitate user operations. This article will detail the usage methods and examples of the `sealos registry serve` command.
+
+### Basic Introduction
+
+The main function of the `sealos registry serve` command is to start a Docker distribution image repository server, supporting two modes: `filesystem` and `inmem`.
+
+1. **Filesystem mode**: In this mode, sealctl will run a Docker distribution image repository server for the specified directory. In this mode, the image data will be stored on the hard drive.
+
+2. **In-memory mode**: In this mode, sealctl will run a Docker distribution image repository server in memory. In this mode, the image data is only stored in memory, and the data will be lost after the process exits.
+
+### Command Parameters
+
+The `sealos registry serve filesystem ` command supports the following parameters:
+
+- `--disable-logging`: Disable log output, default is false.
+- `--log-level`: Configure log level, default is 'error'.
+- `-p, --port`: The port the server listens to, default is a random unused port.
+
+### Usage Examples
+
+Here are some usage examples of the `sealos registry serve` command:
+
+#### Start an image repository server in the filesystem
+
+```bash
+sealos registry serve filesystem --port=5000
+```
+
+The above command will start a filesystem image repository server on port 5000.
+
+#### Start an image repository server in memory
+
+```bash
+sealos registry serve inmem
+```
+
+The above command will start an in-memory image repository server. The stored data of this server will be lost when the process exits.
+
+Through the `sealctl registry serve` command, users can easily manage and operate Docker image repositories. Whether in the development environment or in the production environment, it is a powerful and easy-to-use tool.
+
+## Sealos: Detailed Explanation and User Guide of the `sealos registry passwd` Command
+
+In the process of managing Docker image repositories, Sealos provides the `sealos registry passwd` command to facilitate users in modifying the password for the cluster registry. It offers a convenient method to help users change the password of the registry.
+
+### Basic Usage
+
+Use the `sealos registry passwd` command to modify the password of the registry.
+
+```bash
+sealos registry passwd
+```
+
+### Parameters
+
+Below are the parameters of the `sealos registry passwd` command:
+
+- `-c, --cluster-name`: Cluster name, the default is 'default'.
+
+- `-f, --cri-shim-file-path`: Image cri shim file path, if null it will not update the image cri shim file. The default path is '/etc/image-cri-shim.yaml'.
+
+- `-p, --htpasswd-path`: Registry password file path. The default path is '/etc/registry/registry_htpasswd'.
+
+### Usage Steps
+
+1. Execute the `sealos registry passwd` command, you can specify parameters according to the needs.
+
+2. According to the command prompt, input the new password.
+
+3. After the command is successfully executed, the registry's password will be changed to the new password.
+
+### Demo Explanation
+
+[](https://asciinema.org/a/Qu05jah4ZZmjMuFR4vHEKvBsQ)
+
+**In the usage process, it will let the user choose the registry type**
+
+- registry: Binary startup, execute `systemctl restart registry` to restart the image repository.
+
+- containerd: Containerd startup, execute "nerdctl restart sealos-registry" to restart the image repository.
+
+- docker: Docker startup, execute "docker restart sealos-registry" to restart the image repository.
+
+### Notice
+
+**After changing the registry password, modify the registry password in the Clusterfile**
+After changing the registry password, all nodes and services using this registry need to update their configurations to use the new password for authentication. Otherwise, they will not be able to pull or push images from this registry.
+
+If you are unsure about how to update the configuration of nodes and services, it is recommended to consult related documentation or seek professional technical support before changing the registry password.
+
+## Sealos: Detailed Explanation and User Guide of the `sealos registry sync` Command
+
+Sealos' `registry sync` command can help you synchronize all images between two registries. This can be used not only for image migration but also for backing up your images.
+
+### Basic Command Usage
+
+Execute the `sealos registry sync` command for image synchronization:
+
+```bash
+sealos registry sync source dst
+```
+
+Here `source` represents the address of the source registry, and `dst` is the address of the target registry.
+
+For example, if you want to synchronize all images in the registry with the address of 127.0.0.1:41669 to the registry with the address of sealos.hub:5000, you should execute the following command:
+
+```bash
+sealos registry sync 127.0.0.1:41669 sealos.hub:5000
+```
+
+### Authentication and Permissions
+
+Before executing the `sealos registry sync` command, please ensure that you have permissions to access the source registry and the target registry. You can use `sealos login` to authenticate the registry.
+
+### Synchronization Process
+
+Please note that image synchronization may take some time, depending on the number and size of images, as well as the speed of the network. During the synchronization process, please keep the network connected and ensure not to interrupt the execution of the command before synchronization is complete.
+
+Importantly, the `sealos registry sync` command supports incremental synchronization. Images that already exist in the target registry will not be re-synchronized.
+
+### Parameter Options
+
+The `sealos registry sync` command also provides some parameter options, allowing you to control the synchronization process more finely:
+
+- `--override-arch ARCH`: Use the specified `ARCH` to replace the current machine architecture to select images.
+
+- `--override-os OS`: Use the specified `OS` to replace the current operating system to select images.
+
+- `--override-variant VARIANT`: Use the specified `VARIANT` to replace the current architecture variant to select images.
+
+- `-a` or `--all`: If the source image is a list, synchronize all images. This is particularly useful in heterogeneous environments because by default, only images of the current architecture will be synchronized.
+
+For example, if you want to synchronize all architecture images, you can add the `-a` parameter:
+
+```bash
+sealos registry sync -a 127.0.0.1:41669 sealos.hub:5000
+```
+
+The above is a detailed explanation and usage guide for the `sealos registry sync` command. We hope this information helps you better understand and use this command. If you encounter any problems during use, feel free to ask at any time.
+
+
+## Sealos: Detailed Explanation and User Guide of the `sealos registry copy` Command
+
+The `registry copy` command in Sealos is used to copy a specified image from one registry to another registry. This can assist you in migrating or backing up images between different registries.
+
+### Basic Command Usage
+
+Use the `sealos registry copy` command for image copying:
+
+```bash
+sealos registry copy source-image dst
+```
+
+Here `source-image` represents the full name of the source image (including the address and image name), and `dst` is the address of the target registry.
+
+For example, to copy an image named `127.0.0.1:41669/my-image:tag` to a registry with the address `sealos.hub:5000`, you can execute the following command:
+
+```bash
+sealos registry copy 127.0.0.1:41669/my-image:tag sealos.hub:5000
+```
+
+### Authentication and Permissions
+
+Before executing the `sealos registry copy` command, please ensure that you have permissions to access the source image and the target registry. You can use `sealos login` to authenticate the registry.
+
+### Copying Process
+
+Please note that image copying may take some time, depending on the size of the image and the speed of the network. During the copying process, please keep the network connected and ensure not to interrupt the execution of the command before the copying is complete.
+
+### Parameter Options
+
+The `sealos registry copy` command provides some parameter options, allowing you to control the copying process more finely:
+
+- `--override-arch ARCH`: Use the specified `ARCH` to replace the current machine architecture to select images.
+
+- `--override-os OS`: Use the specified `OS` to replace the current operating system to select images.
+
+- `--override-variant VARIANT`: Use the specified `VARIANT` to replace the current architecture variant to select images.
+
+- `-a` or `--all`: If the source image is a list, copy all images. This is particularly useful in heterogeneous environments because, by default, only images of the current architecture will be copied.
+
+For example, if you want to copy all architecture images, you can add the `-a` parameter:
+
+```bash
+sealos registry copy -a 127.0.0.1:41669/my-image:tag sealos.hub:5000
+```
+
+The above is the usage guide for the `sealos registry copy` command. We hope it is helpful to you. If you encounter any problems during use, feel free to ask us any questions.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/reset.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/reset.md
new file mode 100644
index 000000000000..e25b3f3742fa
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/reset.md
@@ -0,0 +1,61 @@
+---
+sidebar_position: 2
+---
+
+# Reset Cluster
+
+The `sealos reset` is a command in the Sealos command-line tool for resetting the entire cluster. This command is particularly useful when you want to completely clear cluster data or rebuild the cluster. This guide will detail how to use it.
+
+## Basic Usage
+
+The basic format of the `sealos reset` command is as follows:
+
+```bash
+sealos reset --cluster cluster_name
+```
+
+In the above command, `cluster_name` is the name of the cluster you want to reset.
+
+## Example
+
+For instance, you can use the following command to reset a cluster named `mycluster`:
+
+```bash
+sealos reset --cluster mycluster
+```
+
+## Optional Parameters
+
+- `--force`: This parameter is used to force the reset of the cluster, even if the cluster reset operation has not been successfully completed.
+
+```bash
+sealos reset --cluster mycluster --force
+```
+
+- `--masters`: This parameter is used to specify the master nodes to be reset.
+
+```bash
+sealos reset --cluster mycluster --masters master1
+```
+
+- `--nodes`: This parameter is used to specify the worker nodes to be reset.
+
+```bash
+sealos reset --cluster mycluster --nodes node1 node2
+```
+
+- `-p`, `--passwd`: This parameter is used to provide a password for authentication.
+
+- `-i`, `--pk`: This parameter specifies the file to read the identity (private key) used for public key authentication.
+
+- `--pk-passwd`: This parameter is used for the passphrase to decrypt the PEM-encoded private key.
+
+- `--port`: This parameter is used to specify the port of the remote host to connect to.
+
+- `-u`, `--user`: This parameter is used to specify the username for authentication.
+
+```bash
+sealos reset --cluster mycluster --user username --pk /root/.ssh/id_rsa --pk-passwd yourpassword
+```
+
+The above is the usage guide for the `sealos reset` command. We hope it is helpful to you. If you encounter any problems during use, feel free to ask us any questions.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/rmi.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/rmi.md
new file mode 100644
index 000000000000..8bcfd1e6bf6b
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/rmi.md
@@ -0,0 +1,53 @@
+---
+sidebar_position: 6
+---
+
+# Rmi: Delete Local Images
+
+`sealos rmi` is a command in the Sealos command-line tool that is used to delete one or more images stored locally. This command can help you clean up useless or outdated images and save storage space. This guide will detail how to use it.
+
+## Basic Usage
+
+The basic format of the `sealos rmi` command is as follows:
+
+```bash
+sealos rmi imageID
+```
+
+In the above command, `imageID` is the ID of the image you want to delete.
+
+## Example
+
+For instance, you can use the following command to delete an image with ID `imageID`:
+
+```bash
+sealos rmi imageID
+```
+
+If you want to delete multiple images, just list all the image IDs in the command line, for example:
+
+```bash
+sealos rmi imageID1 imageID2 imageID3
+```
+
+## Optional Parameters
+
+- `-a`, `--all`: This parameter is used to delete all images. When using this option, the command will not accept any image IDs.
+
+```bash
+sealos rmi --all
+```
+
+- `-f`, `--force`: This parameter is used to forcefully delete an image and any containers using that image.
+
+```bash
+sealos rmi --force imageID
+```
+
+- `-p`, `--prune`: This parameter is used to prune dangling images (images without a tag and not referenced by any containers).
+
+```bash
+sealos rmi --prune
+```
+
+The above is the usage guide for the `sealos rmi` command. We hope it is helpful to you. If you encounter any problems during use, feel free to ask us any questions.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/run.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/run.md
new file mode 100644
index 000000000000..ac9e72d36c03
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/run.md
@@ -0,0 +1,84 @@
+---
+sidebar_position: 1
+---
+
+# Run: Execute Cluster Images
+
+The `run` command of Sealos is a powerful and flexible tool that supports cluster initialization, application installation, multi-image execution, single-node clusters, and more. Below is a detailed explanation and some usage examples of the `sealos run` command and its parameters.
+
+## Command Overview
+
+```
+sealos run --masters [arg] --nodes [arg] [Options]
+```
+
+The `` parameter is the name and version of the Docker image you want to run in the cluster. `--masters` and `--nodes` are the IP lists of the master and node nodes where you want to run this image.
+
+### Option Explanation
+
+- `--cluster='default'`: The name of the cluster where the operation is to be run.
+
+- `--cmd=[]`: Overwrite the CMD instruction in the image.
+
+- `--config-file=[]`: The path to the custom configuration file, used to replace resources.
+
+- `-e, --env=[]`: The environment variables set during command execution.
+
+- `-f, --force=false`: Forcefully overwrite the application in this cluster.
+
+- `--masters=''`: The master nodes to be run.
+
+- `--nodes=''`: The node nodes to be run.
+
+- `-p, --passwd=''`: Authenticate using the provided password.
+
+- `-i, --pk='/root/.ssh/id_rsa'`: Choose the private key file from which to read the public key authentication identity.
+
+- `--pk-passwd=''`: The password to decrypt the PEM-encoded private key.
+
+- `--port=22`: The connection port of the remote host.
+
+- `-t, --transport='oci-archive'`: Load image transport from a tar archive file. (Optional values: oci-archive, docker-archive)
+
+- `-u, --user=''`: The username for authentication.
+
+## Examples
+
+1. Create a cluster on your bare-metal servers, specifying the IP list:
+```
+sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \
+ --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx'
+```
+
+2. Run multiple images:
+```
+sealos run labring/kubernetes:v1.24.0 labring/helm:v3.11.3 calico:v3.24.6 \
+ --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19
+```
+
+3. Specify the InfraSSH port of the server:
+```
+sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \
+ --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd 'xxx'
+```
+
+4. Customize a VIP Kubernetes cluster:
+```
+sealos run -e defaultVIP=10.103.97.2 labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \
+ --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx'
+```
+
+5. Create a single-node Kubernetes cluster:
+```
+sealos run labring/kubernetes:v1.24.0
+```
+
+6. Create a cluster using custom environment variables:
+```
+sealos run -e DashBoardPort=8443 mydashboard:latest --masters 192.168.0.2,192.168.0.3,192.168.0.4 \
+ --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx'
+```
+
+These examples demonstrate the power and flexibility of the `sealos run` command, which can be customized and adjusted according to your needs.
+
+For more examples, please refer to [Run Cluster](/self-hosting/lifecycle-management/operations/run-cluster.md).
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/save.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/save.md
new file mode 100644
index 000000000000..ab6e80274ba0
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/save.md
@@ -0,0 +1,40 @@
+---
+sidebar_position: 6
+---
+
+# Save: Store Images
+
+`sealos save` is a command in the Sealos command-line tool used to save images to archive files. This command can help you conveniently back up and migrate your images. This guide will detail its usage.
+
+## Basic Usage
+
+The basic `sealos save` command format is as follows:
+
+```bash
+sealos save -o outputFilename imageName
+```
+
+In the above command, `outputFilename` is the name of the archive file you want to save, and `imageName` is the name of the image you want to save.
+
+## Example
+
+For example, you can use the following command to save an image named `labring/kubernetes:latest` to an archive file named `kubernetes.tar`:
+
+```bash
+sealos save -o kubernetes.tar labring/kubernetes:v1.24.0
+```
+
+## Optional Parameters
+
+- `--format`: This parameter is used to specify the transport format for saving the image. The currently available options are `oci-archive`, `docker-archive`, `oci-dir`, and `docker-dir`. The default value is `oci-archive`.
+- `-m`: This parameter can be used to save multiple images at the same time, but it is only applicable to the `docker-archive` format.
+
+
+For example, you can use the following command to save an image named `labring/kubernetes:latest` to an archive file named `kubernetes.tar` in the `docker-archive` method:
+
+```bash
+sealos save -o kubernetes.tar --format docker-archive labring/kubernetes:v1.24.0
+sealos save -o kubernetes.tar -m --format docker-archive labring/kubernetes:v1.24.0 labring/helm:v3.5.0
+```
+
+The above is the usage guide of the `sealos save` command, and we hope it is helpful to you. If you encounter any problems during use, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/scp.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/scp.md
new file mode 100644
index 000000000000..d31cf5e91892
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/scp.md
@@ -0,0 +1,51 @@
+---
+sidebar_position: 5
+---
+
+# Scp: Copy Files
+
+`sealos scp` is a command in the Sealos command-line tool, used for copying files to specified cluster nodes. This guide will detail its usage and options.
+
+## Basic Usage
+
+The basic `sealos scp` command format is as follows:
+
+```bash
+sealos scp "source file path" "destination file path"
+```
+
+In the above command, `source file path` is the local path of the file you want to copy, and `destination file path` is the remote node path you want to copy the file to.
+
+## Options
+
+The `sealos scp` command provides the following options:
+
+- `-c, --cluster='default'`: The name of the cluster to which the files should be copied. The default is `default`.
+
+- `--ips=[]`: Copies the files to nodes with the specified IP addresses.
+
+- `-r, --roles='':`: Copies the files to nodes with specified roles.
+
+Each option can be followed by one or more arguments.
+
+## Examples
+
+For example, you can use the following command to copy the local file `/root/aa.txt` to `/root/dd.txt` on all nodes in the default cluster:
+
+```bash
+sealos scp "/root/aa.txt" "/root/dd.txt"
+```
+
+If you want to copy files on the nodes with the `master` and `node` roles in the cluster named `my-cluster`, you can use the following command:
+
+```bash
+sealos scp -c my-cluster -r master,node "/root/aa.txt" "/root/dd.txt"
+```
+
+If you only want to copy files on the node with the IP address `172.16.1.38`, you can use the following command:
+
+```bash
+sealos scp -c my-cluster --ips 172.16.1.38 "/root/aa.txt" "/root/dd.txt"
+```
+
+The above is the usage guide for the `sealos scp` command, and we hope it is helpful to you. If you encounter any problems during use, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/tag.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/tag.md
new file mode 100644
index 000000000000..752cc7e57229
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/tag.md
@@ -0,0 +1,33 @@
+---
+sidebar_position: 6
+---
+
+# Tag: Add Image Names
+
+`sealos tag` is a command in the Sealos command-line tool, used to add one or more additional names to the images stored locally. This command can help you better manage your images. This guide will detail its usage.
+
+## Basic Usage
+
+The basic `sealos tag` command format is as follows:
+
+```bash
+sealos tag imageName newName
+```
+
+In the above command, `imageName` is the name of the image you want to operate on, and `newName` is the new tag you want to add.
+
+## Examples
+
+For example, you can use the following command to add a new name `firstNewName` to an image named `imageName`:
+
+```bash
+sealos tag imageName firstNewName
+```
+
+You can also add multiple names at once, for example, add two names `firstNewName` and `SecondNewName`:
+
+```bash
+sealos tag imageName firstNewName SecondNewName
+```
+
+The above is the usage guide for the `sealos tag` command, and we hope it is helpful to you. If you encounter any problems during use, feel free to ask us.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md
new file mode 100644
index 000000000000..f00cafdd02bb
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md
@@ -0,0 +1,55 @@
+---
+sidebar_position: 10
+---
+
+# Kubernetes Cluster Image Documentation
+
+This document provides an overview of the rootfs-type cluster images offered by Sealos, including the image names, types, and versions.
+
+## Image Names
+
+The official cluster images provided by Sealos include the following:
+
+1. kubernetes: Kubernetes image with containerd as the container runtime interface (CRI).
+2. kubernetes-docker: Kubernetes image with Docker as the CRI.
+3. kubernetes-crio: Kubernetes image with Crio as the CRI.
+
+Currently, Sealos primarily provides Kubernetes-related images and has not yet provided other types of cluster images such as k3s or k0s.
+
+## Image Types
+
+Sealos offers different types of Kubernetes cluster images based on the container runtime interface (CRI):
+
+1. Kubernetes image with containerd as the CRI.
+2. Kubernetes image with Docker as the CRI.
+3. Kubernetes image with Crio as the CRI.
+
+Users can choose the appropriate image type based on their requirements and preferences.
+
+## Image Versions
+
+Sealos offers multiple versions of cluster images. Examples include:
+
+### 1. Development Version
+
+This version is suitable for users who want to try out the latest features of the project. The development version may contain new features and improvements that have not been thoroughly tested and may not be as stable.
+
+Example: `v1.26(v1.26-amd64/v1.26-arm64)`
+
+### 2. Latest Version
+
+The latest version is typically more stable than the development version but may not include all the new features. This version is recommended for most users.
+
+Example: `v1.26.0(v1.26.0-amd64/v1.26.0-arm64)`
+
+### 3. Release Version (Including Historical Versions)
+
+The release version includes historical versions. Release versions have usually undergone rigorous testing and are considered stable.
+
+Example: `v1.26.0-4.1.5(v1.26.0-4.1.5-amd64/v1.26.0-4.1.5-arm64)` `4.1.5` is the corresponding version number for Sealos.
+
+When choosing an image version, users should consider their requirements and preferences. Additionally, Sealos provides sub-versions for different processor architectures to meet the needs of users on different hardware platforms.
+
+## Summary
+
+This document provides an overview of the rootfs-type cluster images offered by Sealos, including the image names, types, and versions. Users can select the appropriate image type and version based on their requirements and preferences to run containers in a Kubernetes cluster.
diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/sealos.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/sealos.md
new file mode 100644
index 000000000000..c5f8bcefd174
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/sealos.md
@@ -0,0 +1,14 @@
+---
+sidebar_position: 0
+---
+
+# Sealos User Guide
+
+Sealos is a unified cloud operating system designed to manage cloud-native applications. It provides a set of command-line tools to help users manage Kubernetes clusters, nodes, perform remote operations, manage containers and images, and perform other functions. Below are detailed introductions to these commands:
+
+This chapter provides a user guide for using Sealos, as well as information related to cluster images.
+
+- Sealos User Guide: For detailed information about each command, including all supported parameters and subcommands, please refer to the [sealos](/self-hosting/lifecycle-management/reference/sealos/commands.md) reference documentation.
+- Cluster Images:
+ - [Rootfs Cluster Images](/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md)
+ - [Application Cluster Images](/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md)
diff --git a/docs/archived/4.0/docs/self-hosting/sealos/QA.md b/docs/archived/4.0/docs/self-hosting/sealos/QA.md
new file mode 100644
index 000000000000..a7bbc88787de
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/sealos/QA.md
@@ -0,0 +1,113 @@
+---
+sidebar_position: 3
+---
+
+# Q&A
+
+Encountering issues during the deployment and use of Sealos Cloud is not uncommon. To assist you effectively, we have compiled a list of frequently encountered problems along with comprehensive solutions.
+
+## Deployment Related Issues
+
+This section details the problems you may face during the deployment phase and their respective solutions. For issues not covered here, please consult with us at the [Sealos Community](https://github.com/labring/sealos/discussions).
+
+### Q1: iptables / ip_forward Concerns
+
+**Problem Overview**: In some operating systems, such as older versions of Centos and RHEL, iptables or IPv4 IP forwarding is not enabled by default. This can hinder the creation of iptables rules or the forwarding of packets, potentially preventing the cluster from starting correctly.
+
+**Resolution Strategy**: To address this, execute the following commands on each node to activate iptables and IP forwarding:
+
+```shell
+$ modprobe br_netfilter
+$ echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables
+$ echo 1 > /proc/sys/net/ipv4/ip_forward
+```
+
+### Q2: Issues with System Kernel
+
+- **Problem Overview**: An outdated system kernel can impede the proper startup of the cluster. Also, certain applications, especially those dependent on MongoDB 5.0, might not function with an older kernel.
+- **Resolution Strategy**: Ensure your system's kernel version is at least 5.4 or higher before commencing the deployment.
+
+### Q3: System Resource Constraints
+
+- **Problem Overview**: Limited system resources can lead to deployment delays or even halts. If you encounter prolonged wait times, it's likely due to insufficient system resources.
+- **Resolution Strategy**: Check the resource status of your nodes using `kubectl describe nodes`, focusing on CPU, memory, and storage availability.
+
+### Q4: Networking Issues
+
+- **Problem Overview**: Incorrect server configuration can lead to various network issues during deployment. Common areas of concern include:
+ 1. Misconfiguration of http_proxy / https_proxy environment variables;
+ 2. Inadequate server firewall settings;
+ 3. Improper server routing configurations;
+- **Resolution Strategy**: Troubleshoot network issues by verifying the correctness of these configurations.
+
+## Certificate and Domain Name Issues
+
+### Certificate Renewal Process
+
+Certificates are crucial for the security of your Sealos cluster. Follow these steps to update your certificates, especially as they approach their expiration date:
+
+1. **Backup Existing Certificate**:
+
+ On the `master0` node, backup your current certificate. This step is crucial to prevent loss of the certificate during the update process. Use this command:
+
+ ```shell
+ $ kubectl get secret -n sealos-system wildcard-cert -o yaml > cert-backup.yaml
+ ```
+
+ This will save the `wildcard-cert` certificate in YAML format to `cert-backup.yaml`.
+
+2. **Storing the New Certificate**:
+
+ Place your new certificate files (.crt and .key) on the `master0` node.
+
+3. **Updating the Certificate**:
+
+ To update, use the script below, replacing `` and `` with the actual paths of your new certificate and key files.
+
+ ```shell
+ #!/bin/bash
+ # Set Variables
+ CRT_FILE=
+ KEY_FILE=
+
+ # Base64 encode the certificate and key files
+ CRT_BASE64=$(cat $CRT_FILE | base64 -w 0)
+ KEY_BASE64=$(cat $KEY_FILE | base64 -w 0)
+
+ # Create JSON for update
+ PATCH_JSON='{"data":{"tls.crt":"'$CRT_BASE64'","tls.key":"'$KEY_BASE64'"}}'
+
+ # Update the Secret using kubectl patch
+ kubectl patch secret wildcard-cert -n sealos-system -p $PATCH_JSON
+ ```
+
+ This script encodes the new certificate in Base64 and updates the Kubernetes cluster's Secret object using `kubectl patch`.
+
+### Changing the Domain Name
+
+Changing a domain name in a Sealos cluster is complex, often requiring adjustments in multiple components and services. We do not currently provide a comprehensive guide for this process in our documentation. However, future plans include the release of a Sealos Cluster Management Panel for easier domain name and certificate replacement.
+
+It's important to note that domain name changes demand deep knowledge of the cluster's network setup and may involve intricate DNS settings and service discovery. We recommend undertaking such changes only if you have the requisite expertise or with guidance from a professional.
+
+### user registration switch
+
+disabled user register:
+
+```shell
+kubectl get cm -n sealos desktop-frontend-config -o yaml | sed 's/signUpEnabled: true/signUpEnabled: false/g' | kubectl apply -f -
+kubectl rollout restart deployment desktop-frontend -n sealos
+```
+
+enabled user register:
+
+```shell
+kubectl get cm -n sealos desktop-frontend-config -o yaml | sed 's/signUpEnabled: false/signUpEnabled: true/g' | kubectl apply -f -
+kubectl rollout restart deployment desktop-frontend -n sealos
+```
+
+
+## Database Issues
+
+### Dify installation
+
+The Sealos offline package does not include the vector database by default (and the related image is not packaged), so it is currently not possible to install the Dify application in a privately deployed Sealos cluster.
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/chrome-certificate-1.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/chrome-certificate-1.jpg
new file mode 100644
index 000000000000..680d572141b3
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/chrome-certificate-1.jpg differ
diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/chrome-certificate-2.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/chrome-certificate-2.jpg
new file mode 100644
index 000000000000..8b6a354702c0
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/chrome-certificate-2.jpg differ
diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-1.png b/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-1.png
new file mode 100644
index 000000000000..6e2c46692cf6
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-1.png differ
diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-2.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-2.jpg
new file mode 100644
index 000000000000..a99aaeaf5a0a
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-2.jpg differ
diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-3.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-3.jpg
new file mode 100644
index 000000000000..8a42e6c08c8e
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-3.jpg differ
diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-1.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-1.jpg
new file mode 100644
index 000000000000..3a211ccd3e13
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-1.jpg differ
diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-2.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-2.jpg
new file mode 100644
index 000000000000..357d5880dbe4
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-2.jpg differ
diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-3.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-3.jpg
new file mode 100644
index 000000000000..9c33c03204f5
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-3.jpg differ
diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/windows-trust-certificate.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/windows-trust-certificate.jpg
new file mode 100644
index 000000000000..243d0d2d5583
Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/windows-trust-certificate.jpg differ
diff --git a/docs/archived/4.0/docs/self-hosting/sealos/install-object-storage.md b/docs/archived/4.0/docs/self-hosting/sealos/install-object-storage.md
new file mode 100644
index 000000000000..ab7a4a33e1d0
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/sealos/install-object-storage.md
@@ -0,0 +1,44 @@
+---
+sidebar_position: 2
+---
+
+# Install Object Storage
+
+Download the Helm charts.
+
+```bash
+curl -O https://raw.githubusercontent.com/minio/operator/master/helm-releases/operator-5.0.6.tgz
+```
+
+Install Operator.
+
+```bash
+helm install --namespace minio-system --create-namespace minio-operator operator-5.0.6.tgz
+```
+
+Install Minio, Controller, etc.
+
+```bash
+# DOMAIN is the domain name for the Sealos cluster
+# Set environment variables for Minio admin account (default Minio admin account is username/passw0rd)
+# -e minioAdminUser={16-character random alphanumeric string} -e minioAdminPassword={32-character random alphanumeric string}
+sealos run ghcr.io/labring/sealos-cloud-objectstorage:latest -e cloudDomain={DOMAIN}
+```
+
+# Uninstall Object Storage
+
+Installation of the Object Storage failed, you can use the script to clean up the remaining resources. Uninstalling the
+Object Storage can also be done using the script to clean up.
+
+```bash
+#!/usr/bin/env bash
+set +e
+
+kubectl delete app objectstorage -n app-system
+kubectl delete ns objectstorage-system objectstorage-frontend
+helm uninstall minio-operator -n minio-system
+kubectl delete ns minio-system
+kubectl delete crd objectstoragebuckets.objectstorage.sealos.io objectstorageusers.objectstorage.sealos.io
+kubectl delete clusterrole objectstorage-manager-role objectstorage-metrics-reader objectstorage-proxy-role
+kubectl delete clusterrolebinding objectstorage-manager-rolebinding objectstorage-proxy-rolebinding
+```
\ No newline at end of file
diff --git a/docs/archived/4.0/docs/self-hosting/sealos/installation.md b/docs/archived/4.0/docs/self-hosting/sealos/installation.md
new file mode 100644
index 000000000000..6f146822581b
--- /dev/null
+++ b/docs/archived/4.0/docs/self-hosting/sealos/installation.md
@@ -0,0 +1,362 @@
+---
+sidebar_position: 1
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Install Sealos Cluster
+
+export const Highlight = ({children, color}) => (
+
+ {children}
+
+);
+
+:::tip
+
+In the context of large-scale clusters and corporate production environments, [the enterprise or bespoke editions of the Sealos private cloud](/self-hosting) come highly recommended.
+
+:::
+
+## Before You Begin
+
+### Hardware
+
+- Unique hostnames for each cluster node.
+- Time synchronization across all nodes.
+- Preferably use a fresh operating system for setting up the cluster. **Refrain from installing Docker yourself!**
+- Compatible with numerous Linux versions like Ubuntu, Debian, CentOS, Rocky Linux.
+- **Ensure a system kernel version of 5.4 or higher**.
+
+Recommended System Configuration:
+
+We advise using Ubuntu 22.04 LTS with a kernel version of 5.4 or higher. The specifications are as follows:
+
+| Operating System | Kernel Version | CPU | Memory | Storage | Masters | Nodes |
+|------------------|----------------|-----|--------|---------|------------|-------|
+| Ubuntu 22.04 LTS | ≥ 5.4 | 8C | 16GB | 100GB | Odd Number | Any |
+
+:::info
+Kubernetes and Sealos Cloud Operating system require roughly 2 cores (2c) and 2GB of memory (2g) per Master node, and about 1 core (1c) and 1GB of memory (1g) per Node node. Ensure each node in your cluster is well-equipped for these system components.
+:::
+
+### Network Considerations
+
+- All nodes must be interconnected;
+- Installation scripts should be run on the **first Master node** of the Kubernetes cluster; **nodes outside the cluster aren't set up for installations yet**;
+- Make sure all nodes are capable of mutual communication.
+
+### Domain Name Usage
+
+- A domain name is necessary for accessing Sealos and its services;
+- If you lack a domain name, `nip.io` offers a free domain name service.
+
+### Certificate Requirements
+
+Sealos uses certificates for secure communication. Without your certificates, [cert-manager](https://cert-manager.io/docs/) will be employed for auto-certification.
+
+If you are providing your certificate, it should support the following domains (assuming your domain is cloud.example.io):
+
+- `*.cloud.example.io`
+- `cloud.example.io`
+
+## Installation Steps
+
+We offer a one-click installation script to streamline the deployment process. This script is capable of setting up a Sealos cluster from scratch or integrating it into an existing Kubernetes cluster (use with caution on pre-existing clusters).
+
+:::info
+This script is specifically designed for deploying Sealos clusters on "Kubernetes clusters installed via Sealos," and is not compatible with Kubernetes clusters set up in other ways.
+
+For instructions on deploying Kubernetes clusters using Sealos, refer to: [Installing Kubernetes Clusters](/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md). This method is compatible with most Kubernetes versions available on [Docker Hub](https://hub.docker.com/r/labring/kubernetes/tags), except for version 1.28 and higher.
+:::
+
+If your system is new to Kubernetes, **we advise using this script to install both Kubernetes and the Sealos cluster simultaneously.**
+
+There are different installation options based on your domain name needs:
+
+### 1. No public domain, don't want custom domains
+
+If you don't have a public domain or need custom domains, you can use the free `nip.io` wildcard DNS service. `nip.io` maps dynamic IP addresses to fixed subdomains, useful for local dev environments.
+
+It works by taking any IP address as part of a `nip.io` subdomain, and resolving requests back to that IP. For example, if your intranet IP is `192.168.1.10`, you can use the domain `192.168.1.10.nip.io`. Requests to this domain will resolve to `192.168.1.10`, eliminating the need to modify local hosts or set up intranet DNS.
+
+To use nip.io for Sealos, run the below on the first master node and enter prompts:
+
+```bash
+$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh
+```
+
+When prompted for the Sealos Cloud domain name, use a format like `[ip].nip.io`, where [ip] is your Master node's IP.
+
+After installation, the terminal will display the Sealos access domain and login credentials, as shown below:
+
+```shell
+Sealos cloud login info:
+Cloud Version: latest
+URL: https://10.214.210.102.nip.io
+admin Username: admin
+admin Password: sealos2023
+```
+
+### 2. Have public domain, want public access
+
+If you have a public domain and want public Sealos access, you'll need a trusted public SSL/TLS certificate. You can use acme.sh to automatically issue certs, or get free certs from your domain provider or purchase commercial certificates.
+
+Place the certificate files in a directory on the first master, like `/root/certs/`.
+
+:::info
+
+You'll also need to configure DNS records at your domain provider:
+
+```
+cloud.example.io A
+*.cloud.example.io A
+```
+
+This maps your domain and subdomains to the first master's public IP.
+
+:::
+
+Then run below on the first master, entering prompts:
+
+```bash
+$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \
+--cloud-domain= \
+--cert-path= \
+--key-path=
+```
+
++ `` is your public domain
++ `` is the certificate file path (`.crt` or `.pem`) e.g. `/root/certs/example.crt`
++ `` is the private key file path (`.key` or `.pem`) e.g. `/root/certs/example.key`
+
+### 3. Have public domain, want internal access
+
+If you have a public domain but only internal IPs, or only want internal Sealos access, you just need to configure DNS records resolving to the first master's internal IP:
+
+```
+cloud.example.io A
+*.cloud.example.io A
+```
+
+Then run the below on the first master, entering prompts:
+
+```bash
+$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \
+--cloud-domain=
+```
+
+Where `` is your public domain.
+
+The installer will use [cert-manager](https://cert-manager.io/docs/) to automatically sign certificates.
+
+### 4. No public domain, want custom domain
+
+If you don't have a public domain but need a custom domain, set up internal DNS resolving a custom domain to the first master's internal IP.
+
+:::note
+
+Assuming the first master internal IP is `192.168.1.10`, and your custom domain is `cloud.example.io`.
+
+:::
+
+You can use CoreDNS, Reference configuration:
+
+```
+(global_cache) {
+ cache {
+ # [5, 60]
+ success 65536 3600 300
+ # [1, 10]
+ denial 8192 600 60
+ prefetch 1 60m 10%
+ }
+}
+
+.:53 {
+ errors
+ health
+ ready
+
+ import global_cache
+
+ template IN A cloud.example.io {
+ answer "{{ .Name }} 60 IN A 192.168.1.10"
+ fallthrough
+ }
+
+ forward . 223.5.5.5
+
+ log
+ loop
+
+ reload 6s
+}
+```
+
+This resolves `cloud.example.io` and subdomains to the first master internal IP.
+
+Then run below on the first master, entering prompts:
+
+```bash
+$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \
+--cloud-domain=
+```
+
+Where `` is your custom domain.
+
+The installer uses [cert-manager](https://cert-manager.io/docs/) for certificates.
+
+## Ensuring Browsers Trust Your Self-Signed Certificate
+
+When using installation options 1, 3, or 4, as highlighted earlier, your browser won't automatically recognize and trust your certificate. This leads to a warning message when you try to enter Sealos Cloud:
+
+
+
+Continuing to Sealos Cloud despite the warning results in issues like the App icons not appearing correctly, and Apps failing to launch.
+
+
+
+To address this, you'll need to export the self-signed certificate and set up your system to trust it. Follow these steps to do so.
+
+### Exporting Self-Signed Certificates
+
+The process of exporting self-signed certificates differs slightly in various browsers. Here's a guide for exporting these certificates in some of the most commonly used browsers:
+
+#### Chrome (includes browsers based on Chromium, like the updated Edge and Brave)
+
+1. In the address bar, click the "Not Secure" warning on the left side.
+2. Select "Certificate is invalid" to bring up a window with certificate information.
+3. In this window, go to the "Details" tab.
+4. Within "Details", find and click on the "Export" option.
+5. Choose a file name and location for saving, then complete the exporting procedure.
+
+#### Firefox
+
+1. On the webpage, click on "Advanced".
+
+ 
+
+2. Next, click "View Certificate".
+
+ 
+
+3. On the certificate page, select "PEM (certificate)".
+
+ 
+
+#### Safari
+
+1. Select "Show Details" on the page.
+
+ 
+
+2. Click on "View this Certificate".
+
+ 
+
+3. You'll see a window displaying the certificate chain.
+
+ 
+
+4. To export the certificate, drag the one highlighted in red to either your desktop or a specific folder.
+
+
+
+### Trusting Self-Signed Certificates
+
+#### macOS
+
+To trust a self-signed certificate on macOS, use the following command:
+
+```bash
+# Assuming the certificate is stored at ~/Downloads/10.214.210.102.nip.io.cer
+$ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/Downloads/10.214.210.102.nip.io.cer
+```
+
+You can also trust the certificate through these steps:
+
+##### 1. Adding the Certificate to Keychain
+
+1. Open **Keychain Access**, click on the "System" category under "System Keychains."
+2. Navigate to the certificate's location in "Finder" and **double-click the certificate file** (usually `.cer`, `.crt`, or `.pem`).
+3. Authenticate with your username and password to allow changes.
+4. The certificate is now added to your keychain.
+
+##### 2. Trusting the Certificate
+
+1. In Keychain Access, locate and double-click the newly added self-signed certificate in the "System" category.
+2. In the certificate window, expand the "Trust" section.
+3. Change the setting next to "When using this certificate" from "Use System Defaults" to "Always Trust."
+4. Close the window and authenticate if prompted to confirm the changes.
+5. The certificate is now trusted.
+
+:::info Note
+
+Self-signed certificates, not issued by a third-party Certificate Authority (CA), won't be trusted by other devices. Manually perform these steps on each device within your company or organization that needs to trust the certificate.
+
+:::
+
+#### Windows
+
+1. Right-click the root certificate file and select "Install Certificate."
+2. Choose "Current User" or "Local Machine," then "Next."
+3. Select "Place all certificates in the following store," click "Browse," choose "Trusted Root Certification Authorities," "OK," then "Next."
+4. Click "Finish," "Yes," then "OK."
+
+
+
+#### Linux
+
+Linux distributions vary in updating the root certificate store and paths for private certificates. First, copy the self-signed CA certificate to the designated path, then run the command to update the root certificate store.
+
+
+
+
+ ```bash
+ # Debian/Ubuntu/Gentoo
+ $ sudo cp root_ca.crt /usr/local/share/ca-certificates/root_ca.crt
+ $ sudo update-ca-certificates
+ $ sudo rm /usr/local/share/ca-certificates/root_ca.crt
+ $ sudo update-ca-certificates --fresh
+ ```
+
+
+
+
+ ```bash
+ # CentOS/Fedora/RHEL
+ $ yum install ca-certificates
+ $ update-ca-trust force-enable
+ $ cp root_ca.crt /etc/pki/ca-trust/source/anchors/
+ $ update-ca-trust
+ ```
+
+
+
+
+ ```bash
+ # Alpine
+ $ apk update && apk add --no-cache ca-certificates
+ $ cp root_ca.crt /usr/local/share/ca-certificates/
+ $ update-ca-certificates
+ ```
+
+
+
+
+ ```bash
+ # OpenSUSE/SLES
+ $ cp root_ca.crt /etc/pki/trust/anchors/
+ $ update-ca-certificates
+ ```
+
+
+
diff --git a/docs/archived/4.0/i18n/zh-Hans/Community.md b/docs/archived/4.0/i18n/zh-Hans/Community.md
new file mode 100644
index 000000000000..c015bb6aee20
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/Community.md
@@ -0,0 +1,11 @@
+# 社区
+
+Sealos 是一个由用户和贡献者参与推动的开源项目,如果您对产品使用存在疑问和建议,可尝试以下方式寻求支持。我们的团队与社区会竭尽所能为您提供帮助。
+
++ 📱 扫码加入社区微信交流群👇
+
+ 
+
++ 💬 加入我们的 [Discord 服务器](https://discord.gg/qzBmGGZGk7),与 Sealos 开发者和终端用户进行交流。
++ 🐦 在 [Twitter](https://twitter.com/Sailos_io) 上关注我们。
++ 🐞 请将任何 Sealos 的 Bug、问题和需求提交到 [GitHub Issue](https://github.com/labring/sealos/issues/new/choose)。
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/Intro.md b/docs/archived/4.0/i18n/zh-Hans/Intro.md
new file mode 100644
index 000000000000..c4ac398826ef
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/Intro.md
@@ -0,0 +1,53 @@
+import Highlight from '@site/src/components/Highlight'
+
+# 什么是 Sealos?
+
+
+
+云原生生态之庞大与复杂,无疑让许多企业感到手足无措,这个生态依然缺乏好用的、开箱即用的发行版。在云原生市场还急需一款云操作系统以进一步降低云原生门槛与成本。
+
+Sealos 的问世为企业开启了一扇新窗。它为企业与开发者提供了一种全新的选择,无论在公有云还是私有云中,都只需安装一个云操作系统。它能让各类应用在操作系统上稳定安全地运行,一并解决了应用所需的各种依赖问题。
+
+Sealos 坚守的理念是:**云可以像 Linux 一样简单**。你可以像使用 Linux 一样安装后立即投入使用,使用过程无需过多纠结。唯一的差别仅在于,Linux 是安装在单一服务器上,而 Sealos 则是安装在整个数据中心。
+
+在 Sealos 的架构中,**公有云与私有云并无本质上的差异**,他们应是同一套代码的不同实例,唯一的区别仅在于配置与所安装应用的差异。在内网安装即是私有云,而在公网对外提供服务则成为公有云。
+
+我们坚信,随着 Sealos 的不断发展与进步,未来的云将更加开放,更加简单。云计算的美好将属于所有算力的提供者,云的价值将由所有云计算的参与者共享。**他们将能够在多元化的市场环境中,以更经济的方式享受云计算所带来的种种便利。**
+
+## 概述
+
+Sealos 是一款以 Kubernetes 为内核的**云操作系统发行版**。它以云原生的方式,抛弃了传统的云计算架构,转向以 Kubernetes 为云内核的新架构,使企业能够**像使用个人电脑一样**简单地使用云。
+
+用户将可以像使用个人电脑一样在 Kubernetes 上一键安装任意高可用分布式应用,几乎不需要任何专业的交付和运维成本。同时,利用独特的集群镜像能力,用户可将任意分布式应用打包成 OCI 镜像,自由组合各种分布式应用,轻松订制所需的云。通过强大且灵活的应用商店功能,可满足各类用户的多样化需求。
+
+
+
+## 适用场景 & 优势
+
+Sealos 是一款强大的业务运行平台,它能完美支持 Java、Go、Python、PHP 等各类应用,不受编程语言限制。该平台为应用提供稳定的运行环境,并解决后端依赖问题,例如数据库、对象存储和消息队列等。更进一步,它还能灵活处理应用配置管理、服务发现、公网暴露,以及自动伸缩等问题。
+
+### 公有云
+
+如果您的业务需要运行在公有云环境中,那么您可以直接使用 [Sealos 提供的公有云服务](https://cloud.sealos.io)。
+
+#### Sealos 的能力
+
+- 🚀 **应用管理**:在应用商店中轻松管理并快速发布可公网访问的分布式应用。
+- 🗄️ **数据库管理**:秒级创建高可用数据库,支持 MySQL、PostgreSQL、MongoDB 和 Redis。
+- 🌥️ **公私一致**:即是公有云也是私有云,支持传统应用无缝迁移到云环境。
+
+#### Sealos 的优势
+
+- 💰 **高效 & 经济**:仅需为容器付费,自动伸缩杜绝资源浪费,大幅度节省成本。
+- 🌐 **通用性强,无心智负担**:专注于业务本身,无需担心复杂性,几乎没有学习成本。
+- 🛡️ **灵活 & 安全**:多租户共享机制在确保安全的同时,实现资源隔离与高效协作。
+
+### 私有云
+
+Sealos 云操作系统 **100% 开源**,您可在 [GitHub](https://github.com/labring/sealos) 上找到所有源代码,因此 **Sealos 公有云**的全部功能也可以部署在私有云环境中,以满足您更多样化的业务需求。
+
+### 强大的应用交付能力
+
+- Sealos 具备优秀的 Kubernetes 生命周期管理能力,还可以自由定制 Kubernetes 环境。
+- Sealos 可以将整个集群打包,并一键交付到客户环境中。所有服务和业务都可以整体交付。
+- 不同于 Docker 主要关注单机镜像,Sealos 更进一步,可以将**整个集群**或者某个分布式应用进行打包。
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/Architecture.md b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/Architecture.md
new file mode 100644
index 000000000000..cfa4b5c9d89c
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/Architecture.md
@@ -0,0 +1,22 @@
+# 架构说明
+
+Sealos 采用应用分离与应用互联的方式构建,不同应用可以独立部署,也可以与其他应用联合起来提供服务。
+所有服务全部通过 `kubeconfig` 作为应用身份验证,使得不管是在浏览器,还是本地使用 `sealos` 命令行,甚至是本地的第三方客户端,都能够获得一致的使用体验。
+
+## 整体架构
+
+
+
+## 应用架构
+
+Sealos 上的应用采用前后端分离的架构,同时前端能够提供 `SSR` 能力,使得应用也可以单独对外提供服务,而不需要绑定在 Sealos 单体上。
+
+以下图为例:
+
+**应用管理 App Launchpad** 是 Sealos 提供的一个单镜像部署工具, **终端 Terminal** 是 Sealos 云操作作系统的终端应用,提供和单机操作系统终端一致的命令行服务。
+对于 Sealos 上的各种应用而言,它们可以直接与 `Kubernetes` 的服务进行交互,也可以与各种 CRD Controller 比如 Terminal Controller 和 Ingress-Nginx Controller 等等进行交互。
+应用之间也可以互相调用,比如用户在**数据库**应用中新建的数据库,可以直接唤起**终端**应用并自动连接上数据库进行调试。
+
+详细信息可以点击 [链接](../../guides) 文档了解更多。
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/application_dark.png b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/application_dark.png
new file mode 100644
index 000000000000..403e9223d16c
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/application_dark.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/application_light.png b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/application_light.png
new file mode 100644
index 000000000000..500f67bbe589
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/application_light.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/architecture_dark.png b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/architecture_dark.png
new file mode 100644
index 000000000000..6d14555ef4d6
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/architecture_dark.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/architecture_light.png b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/architecture_light.png
new file mode 100644
index 000000000000..17bf8cf5d6ae
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/architecture_light.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/how-to-deploy-the-application-to-desktop.md b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/how-to-deploy-the-application-to-desktop.md
new file mode 100644
index 000000000000..c47e344a0b80
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/how-to-deploy-the-application-to-desktop.md
@@ -0,0 +1,39 @@
+# 如何部署应用到 desktop
+
+## 使用 Terminal 编写 app.yaml
+
+### 关键信息
+
+- type: iframe,标识为一个 web 应用
+- spec name 为桌面显示的名称
+- url 应用地址
+- icon 是在 desktop 上显示的图标
+
+```yaml
+apiVersion: app.sealos.io/v1
+kind: App
+metadata:
+ name: app-sample
+spec:
+ name: Image Hub Demo
+ icon:
+ type: iframe
+ data:
+ url: https://hub.sealos.io/
+ desc:
+ icon: https://cloud.sealos.io/logo.svg
+ menuData:
+ displayType: normal
+```
+
+
+
+### apply yaml
+
+```
+kubectl apply -f app.yaml
+```
+
+
+
+### 刷新浏览器,桌面即可看到 App
diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/images/app-apply-command.png b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/images/app-apply-command.png
new file mode 100644
index 000000000000..f8a5fe3e636f
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/images/app-apply-command.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/images/app-yaml.png b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/images/app-yaml.png
new file mode 100644
index 000000000000..e9ae045a5134
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/images/app-yaml.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/imgs/sealos1.png b/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/imgs/sealos1.png
new file mode 100644
index 000000000000..7313d36f5487
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/imgs/sealos1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/imgs/sealos2.png b/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/imgs/sealos2.png
new file mode 100644
index 000000000000..32f0fb67f319
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/imgs/sealos2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/install-fastgpt-on-desktop.md b/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/install-fastgpt-on-desktop.md
new file mode 100644
index 000000000000..011060f9c9c7
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/install-fastgpt-on-desktop.md
@@ -0,0 +1,15 @@
+# 快速安装 FastGpt AI 知识库
+
+无需服务器、无需魔法、无需域名,点击即可部署 👇
+
+[](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dfastgpt)
+
+由于需要部署数据库,部署完后需要等待 2~4 分钟才能正常访问。默认用了最低配置,首次访问时会有些慢。
+
+
+
+## 运行
+
+点击 sealos 提供的【外网地址】即可使用。登录用户名为: root,密码是刚设置的环境变量,上图中设置了: 1234
+
+
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/baas/install-fireboom.md b/docs/archived/4.0/i18n/zh-Hans/examples/baas/install-fireboom.md
new file mode 100644
index 000000000000..891248b8de3e
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/baas/install-fireboom.md
@@ -0,0 +1,106 @@
+# 快速安装 Fireboom
+
+[Fireboom](https://fireboom.io) ⾯向开发者的可视化 API 开发平台,为开发者提供**声明式开发框架**、**可视化编辑套件**和**钩子机制**。
+
+
+
+官网地址: https://fireboom.io
+
+**功能特性**
+
+- 分钟级交付: 将传统模式下 2天才能完成的接⼝缩短⾄ 2分钟
+
+- 前端变全栈: 可视化开发,显著降低接⼝开发⻔槛,让前端搞定前后端秒变全栈
+
+- 后端不搬砖: 声明式编程,避免复制粘贴,减少bug概率,让后端专注于业务开发
+
+## 步骤 1:新建应用
+
+- 在 [Sealos](https://cloud.sealos.io) 桌面环境中打开 「应用管理」 应用
+
+
+
+- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。
+
+
+
+
+## 步骤 2:应用部署
+
+- 基础配置:
+
+ - 应用名称(自定义):fireboom
+
+ - 镜像名(默认最新版本):fireboomapi/fireboom_server:latest
+
+ - CPU(推荐):2 Core ( 最低1 Core)
+
+ - 内存(推荐):4 G (最低 512 M)
+
+- 部署模式:
+
+ - 实例数(自定义):1
+
+
+
+- 网络配置:
+
+ - 控制面板:9123
+ - API端点:9991,开启外网
+
+
+
+- 高级配置:
+
+ - 命令设置:
+ - 参数设置: dev (dev表示开发模式,start表示生产模式)
+
+ - 持久化存储:
+
+ - 元数据:/fbserver/store
+ - 上传文件:/fbserver/upload
+
+
+
+
+## 步骤 3:部署应用
+
+1, 点击「部署应用」开始部署应用。
+
+
+
+2, 点击「详情」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。
+
+
+
+## 步骤 4:访问应用
+
+- 访问控制面板:9123对应的域名
+
+
+
+- 访问API端点:9991对应的域名
+
+```
+status ok
+```
+
+## 步骤 5:配置Fireboom(可选)
+
+设置后,可方便使用swagger文档~
+
+1, 查看API外网地址: 设置-> 系统
+
+
+
+2, 修改为:API端点地址,9991对应的公网地址
+
+ - 静态值:选择静态值,设置为 **API端点** 公网域名
+ - 环境变量:前往 环境变量 ,找到 FB_API_PUBLIC_URL 设置为 **API端点** 公网域名
+
+
+
+
+3, 使用swagger文档测试API
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/blog-platform/install-halo.md b/docs/archived/4.0/i18n/zh-Hans/examples/blog-platform/install-halo.md
new file mode 100644
index 000000000000..120af78ff064
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/blog-platform/install-halo.md
@@ -0,0 +1,143 @@
+---
+sidebar_position: 2
+---
+
+# 快速安装 Halo
+
+在 Sealos 上快速部署 Halo 博客平台
+
+
+
+[Halo](https://github.com/halo-dev/halo) 是一款强大易用的开源建站工具,本文将介绍如何在 Sealos 上部署 Halo,同时在 [Sealos 上部署 PostgreSQL](../../quick-start/install-db-with-database.md) 可以帮助你轻松地管理和维护数据库,以满足不同的业务需求。
+
+## 步骤 1:准备 PostgreSQL
+
+### 使用 Sealos 数据库安装 PostgreSQL
+
+参考 [Sealos 上部署 PostgreSQL](../../quick-start/install-db-with-database.md) ,一分钟即可成功安装和使用 PostgreSQL。
+
+### 查看数据库配置:
+
+保存这里生成的配置中包含数据库的用户名密码以及数据库连接方式,用于下文 Halo 中配置:
+
+
+
+> 图中可以看到 PostgreSQL 连接配置为:
+>
+> Username: postgres
+>
+> Password: rcx7f47m
+>
+> Connection: postgresql://postgres:rcx7f47m@dbname-postgresql.ns-sy32q9p9.svc:5432
+
+## 步骤 2: 在 Sealos 中部署 Halo应用
+
+### 打开 应用管理
+
+
+
+
+
+### 填写配置
+
+- 自定义应用名称
+
+- 镜像名称设置为 `halohub/halo:2.5`
+
+- CPU 和存储应根据实际情况进行配置,memory 需要 `500Mi` 的内存才能启动 Halo,这里可以参考官方 prepare:https://docs.halo.run/getting-started/prepare
+
+- 暴露端口应为 `8090`。同时,设置为外网访问将自动为应用配置一个出口域名用于外网访问,同时需要在 Halo 中进行配置该地址
+
+- 环境变量包括数据库配置和其他相关配置。需要注意的是,你需要根据你在 [步骤 1](查看数据库配置:) 中设置的用户名、密码和 DNS 来配置数据库连接信息。同时,你还需要为 Halo 配置超级管理员的用户名和密码。
+
+配置 Halo 环境变量
+
+```Bash
+spring.sql.init.platform=postgresql
+spring.r2dbc.url=r2dbc:pool:postgresql://postgres:rcx7f47m@dbname-postgresql.ns-sy32q9p9.svc:5432/halo
+spring.r2dbc.username=postgres
+spring.r2dbc.password=rcx7f47m
+halo.external-url=tmtdvfjiyyfh.cloud.sealos.io
+halo.security.initializer.superadminusername=root
+halo.security.initializer.superadminpassword=sealos
+```
+
+环境变量配置详解:
+
+| 参数名 | 描述 |
+| -------------------------------------------- | -------------------------------------------------------- |
+| spring.r2dbc.url | 数据库连接地址,详细可查阅下方的 数据库链接格式 |
+| spring.r2dbc.username | 数据库用户名 |
+| spring.r2dbc.password | 数据库密码 |
+| spring.sql.init.platform | 数据库平台名称,支持 postgresql、mysql、h2 |
+| halo.external-url | 外部访问链接,如果需要在公网访问,需要配置为实际访问地址 |
+| halo.security.initializer.superadminusername | 初始超级管理员用户名 |
+| halo.security.initializer.superadminpassword | 初始超级管理员密码 |
+
+数据库的链接格式(这里我们使用 postgresql 的格式):
+
+| 链接方式 | 链接地址格式 | spring.sql.init.platform |
+| ----------- | ---------------------------------------------------------------------------------- | ------------------------ |
+| PostgreSQL | r2dbc:pool:postgresql://`{HOST}`:`{PORT}`/`{DATABASE}` | postgresql |
+| MySQL | r2dbc:pool:mysql://`{HOST}`:`{PORT}`/`{DATABASE}` | mysql |
+| MariaDB | r2dbc:pool:mariadb://`{HOST}`:`{PORT}`/`{DATABASE}` | mysql |
+| H2 Database | r2dbc:h2:file:///`${halo.work-dir}`/db/halo-next?MODE=MySQL&DB_CLOSE_ON_EXIT=FALSE | h2 |
+
+### 网络配置
+
+
+
+### 环境变量配置
+
+
+
+### 配置持久化存储卷
+
+挂载 `/root/.halo2` 目录来持久化 halo 数据 :
+
+
+
+## 步骤 3: 使用外网访问 Halo
+
+成功启动应用后,即可通过外网访问地址访问 Halo 进行配置:
+
+
+
+
+
+### 初始化 Halo
+
+
+
+
+
+### First Halo
+
+
+
+
+
+
+
+## FAQ
+
+### 忘记密码
+
+#### 进入 「应用管理」,点击日志查看
+
+
+
+
+
+可以看到这里的日志中:用户名:`admin` 密码:`QTu2J2xgUQ3ngqUo`
+
+#### 或终端执行如下命令查看日志
+
+
+
+```
+root@td3q8uc46:~# kubectl logs halo-0 | grep 'Generated random password:' | tail -1
+2023-05-30T13:32:16.942+08:00 INFO 7 --- [-controller-t-1] r.h.app.security.SuperAdminInitializer : === Generated random password: QTu2J2xgUQ3ngqUo for super administrator: admin ===
+```
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/blog-platform/install-wordpress.md b/docs/archived/4.0/i18n/zh-Hans/examples/blog-platform/install-wordpress.md
new file mode 100644
index 000000000000..41f1c465897f
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/blog-platform/install-wordpress.md
@@ -0,0 +1,103 @@
+---
+sidebar_position: 1
+---
+
+# 快速安装 WordPress
+
+[WordPress](https://github.com/WordPress/WordPress) 是一款功能强大、易用、开源的内容管理系统 (CMS), 可以用于在互联网上发布网站、新闻、博客等内容。它提供了便捷的内容编辑、发布和更新方法 , 同时也可以通过主题和插件进行高度自定义。WordPress 已经成为非常成功和普及的 CMS 系统 , 在全球有超过 3 千万个网站使用 WordPress 搭建。
+
+> 使用 Sealos 安装 WordPress,可以大幅度减少安装时间,极速开启用户的内容输出之旅。
+
+WordPress 需要搭配 MySQL 数据库一同安装使用。作为使用 Sealos 的最佳实践,建议使用**数据库**应用独立安装 MySQL 数据库,依托独立数据库,快速安装 WordPress。
+
+## 新建 MySQL 实例
+
+进入 [Sealos](https://cloud.sealos.io) 桌面环境,在「数据库」中,快速配置需要的数据库实例,等待数据库部署完成。
+
+
+
+
+
+
+
+**注:此时的 MySQL 实例中并不存在用户自定义数据库,需要在终端中手动创建**
+
+## 新建数据库
+
+通过**数据库**应用安装的 MySQL 实例时,并不会自动创建用户自定义数据库。在使用应用程序连接 MySQL 数据库时,您需要在 MySQL 实例中手动创建相应的数据库。
+
+可以通过如下方式进入数据库的终端命令行
+
+
+
+
+也可以在终端中自行输入命令
+
+`mysql -u username -p -h yourhost`
+
+
+
+数据库连接成功后,可以按照如下命令创建数据库
+
+`CREATE DATABASE yourdb;`
+
+至此,您已经在 MySQL 实例中手动创建了一个用户自定义数据库。
+
+## 应用管理
+
+点击应用管理,进入安装界面。
+
+
+
+## 基础配置
+
+首先设置自定义的应用名称,选择需要安装的镜像。Sealos 既支持公有镜像源,也支持私有镜像源。本示例选用的官方镜像源,不指定镜像版本时,默认拉取 latest 版本。
+
+其次,为 WordPress 配置 CPU 与内存资源,WordPress 十分的轻量级,较少的资源就可以保证程序的正常运行。
+
+
+
+## 网络配置
+
+网络配置方面,打开外网访问,Sealos 会为用户自动分配出口域名用作外网访问,如果用户已经注册了自己的自定义域名,那么可以将自定义域名 cname 到该出口域名,这样就可以使用该自定义域名访问用户安装的应用。
+
+注:后续演示,使用的是 Sealos 随机生成的出口域名。
+
+
+
+## 高级配置
+
+在高级配置中,需要根据数据库的实际情况配置 WordPress 的环境变量。
+
+高级配置中,需要为 WordPress 配置环境变量与本地存储。
+
+WordPress 常见环境变量如下所示:
+
+```Plain
+WORDPRESS_DB_HOST 数据库实例的ip地址
+WORDPRESS_DB_USER 连接数据库的用户
+WORDPRESS_DB_PASSWORD 连接库所需的用户密码
+WORDPRESS_DB_NAME 连接的数据库名称
+```
+
+根据之前安装的 MySQL 示例,可以这样配置环境变量:
+
+```Plain
+WORDPRESS_DB_HOST=mysql-host:3306
+WORDPRESS_DB_USER=root
+WORDPRESS_DB_PASSWORD=root_passwd
+WORDPRESS_DB_NAME=yourdb
+```
+
+WordPress 官方镜像的存储卷挂载地址为:`/var/www/html`,用户根据需求为应用分配存储空间。
+
+这样,WordPress 也一并安装成功,点击详情,通过外网地址就可以成功访问。
+
+
+
+点击该链接,如下页面,证明安装成功
+
+
+
+**注:如部署失败 , 优先排查 WordPress 访问的用户自定义数据库是否存在于 MySQL 实例中**
+
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/dial-testing-system/install-uptime-kuma.md b/docs/archived/4.0/i18n/zh-Hans/examples/dial-testing-system/install-uptime-kuma.md
new file mode 100644
index 000000000000..293547d7a0df
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/dial-testing-system/install-uptime-kuma.md
@@ -0,0 +1,60 @@
+---
+sidebar_position: 1
+---
+
+# 快速安装 Uptime Kuma
+
+
+
+[Uptime Kuma](https://github.com/louislam/uptime-kuma) 是一款开源的、易于使用的服务器监控工具。它可以帮助你实时监控服务器的运行状态、响应时间以及其他关键指标,以确保你的服务器始终保持最佳状态。如果你想快速安装 Uptime Kuma,可以按照以下步骤进行操作:
+
+### 步骤 1:首先进入 [Sealos](https://cloud.sealos.io) 桌面环境并打开「应用管理」
+
+
+
+### 步骤 2: 新建应用
+
+在 「应用管理」 中,点击「新建应用」来创建一个新的应用。
+
+
+
+### 步骤 3: 设置启动参数
+
+在启动参数中,按照以下方式进行设置:
+
+- 容器暴露端口指定为 3001。
+- 设置为外网访问即可通过给出的域名访问。
+
+
+
+
+
+### 步骤 4: 设置存储卷
+
+在高级配置中,添加存储卷挂载来持久化 Uptime Kuma 数据目录 `/app/data`。这样可以确保应用数据在容器重启后不会丢失。
+
+
+
+### 步骤五:部署应用
+
+点击「部署应用」后即可启动应用:
+
+
+
+### 步骤 6: 访问应用
+
+一旦应用启动成功,你就可以使用应用的外网地址进行访问了。在浏览器中输入应用的域名,即可访问应用的主界面。
+
+
+
+### 步骤 7: 创建管理员账户
+
+为了保护 Uptime Kuma 的安全性,首次进入你需要创建一个管理员账户,填写管理员账户的基本信息,包括用户名、密码。填写完毕后,点击创建按钮进行注册。
+
+
+
+### 步骤 8: 添加自定义监控项
+
+
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/gitea/install-gitea.md b/docs/archived/4.0/i18n/zh-Hans/examples/gitea/install-gitea.md
new file mode 100644
index 000000000000..28808a705686
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/gitea/install-gitea.md
@@ -0,0 +1,113 @@
+# 快速安装 Gitea
+
+喝杯茶吧!无痛自托管多合一软件开发服务,包括 Git 托管、代码审查、团队协作、包注册和 CI/CD。
+
+## 步骤 1:登陆 Sealos
+
+- 进入 [Sealos](https://cloud.sealos.io/) 官网
+
+
+
+## 步骤 2:打开 「应用管理」 应用
+
+
+
+## 步骤 3:新建应用
+
+- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。
+
+
+
+## 步骤 4:应用部署
+
+- 基础配置:
+
+ - 应用名称(自定义):gitea
+ - 镜像名(默认最新版本):gitea/gitea:latest-rootless
+ - CPU(推荐):1 Core
+ - 内存(推荐):1 G
+
+- 部署模式:
+
+ - 实例数(自定义):1
+
+ 
+
+- 网络配置:
+
+ - 容器暴露端口:3000
+ - 外网访问:开启
+
+ 
+
+- 高级配置:
+
+ - 自定义本地存储,持久化 Gitea 的数据(推荐 1 G)。
+
+
+
+## 步骤 5:部署应用
+
+- 点击「部署应用」开始部署应用。
+
+
+
+## 步骤6:配置数据库
+
+- 通过 Sealos 为 Gitea 配置 MySQL 数据库
+- 进入 Database
+
+
+
+- 新建数据库
+
+
+
+- 部署集群
+
+- 基础配置:
+
+ - 集群类型:mysql
+ - 数据库版本:ac-mysql-8.0.30
+ - CPU(推荐):1 Core
+ - 内存(推荐):1 G
+
+
+
+
+- 部署成功后进入详情页面查看 MySQL 连接信息
+
+
+
+- 点击一键连接进入 MySQL 终端连接
+
+
+
+- 执行
+
+```sql
+CREATE DATABASE giteadb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';
+```
+
+创建 Gitea 所需要的数据库
+
+
+
+## 步骤 7:访问应用
+
+- 点击「App Launchpad」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。
+
+- 当 STATUS 为 Running,即可直接访问外网地址。
+
+
+
+- 访问外网地址后进入到配置页面,根据刚刚创建的 MySQL 的详情页面进行配
+
+
+
+配置好后点击安装,等待一会便可进入到登陆页面,部署成功!
+
+
+
+
+
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-10-52-H2ZWUZ.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-10-52-H2ZWUZ.png
new file mode 100644
index 000000000000..025a0c4e45a9
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-10-52-H2ZWUZ.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-31-vrNHJU.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-31-vrNHJU.png
new file mode 100644
index 000000000000..1ca1cd0ed53a
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-31-vrNHJU.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-32-1iudM0.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-32-1iudM0.png
new file mode 100644
index 000000000000..5ba9b3ad8784
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-32-1iudM0.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-32-ADfk8T.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-32-ADfk8T.png
new file mode 100644
index 000000000000..ac8de099a1ae
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-32-ADfk8T.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-54-EIVahX.jpg b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-54-EIVahX.jpg
new file mode 100644
index 000000000000..1b1fe554ea55
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-54-EIVahX.jpg differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-55-NDkuEg.jpg b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-55-NDkuEg.jpg
new file mode 100644
index 000000000000..a773666c4ff2
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-55-NDkuEg.jpg differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-59-FxJE12.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-59-FxJE12.png
new file mode 100644
index 000000000000..fdc343a80edc
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-59-FxJE12.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-01-DKect7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-01-DKect7.png
new file mode 100644
index 000000000000..b4d2db4792a7
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-01-DKect7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-05-CWczxm.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-05-CWczxm.png
new file mode 100644
index 000000000000..d512ad14195a
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-05-CWczxm.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-06-lvv6ms.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-06-lvv6ms.png
new file mode 100644
index 000000000000..28a241ade20c
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-06-lvv6ms.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-07-s8W7iu.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-07-s8W7iu.png
new file mode 100644
index 000000000000..aa8e415fb5b7
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-07-s8W7iu.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-09-RslDGj.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-09-RslDGj.png
new file mode 100644
index 000000000000..0935a645d999
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-09-RslDGj.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-24-QHYKZt.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-24-QHYKZt.png
new file mode 100644
index 000000000000..a446015a5c59
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-24-QHYKZt.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-26-HYOaBJ.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-26-HYOaBJ.png
new file mode 100644
index 000000000000..6996e718eb53
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-26-HYOaBJ.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-28-ccnUOj.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-28-ccnUOj.png
new file mode 100644
index 000000000000..c76f2e0535ae
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-28-ccnUOj.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-29-z5E9gi.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-29-z5E9gi.png
new file mode 100644
index 000000000000..ba4d166331c6
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-29-z5E9gi.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-39-1jsF0t.jpeg b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-39-1jsF0t.jpeg
new file mode 100644
index 000000000000..7ca839b52b2f
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-39-1jsF0t.jpeg differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-FwsbfW.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-FwsbfW.png
new file mode 100644
index 000000000000..ac7134504f86
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-FwsbfW.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-Vs9ccy.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-Vs9ccy.png
new file mode 100644
index 000000000000..59460d087f22
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-Vs9ccy.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-YFHPYc.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-YFHPYc.png
new file mode 100644
index 000000000000..0fc1e2d152b9
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-YFHPYc.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-hdHfxP.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-hdHfxP.png
new file mode 100644
index 000000000000..00e7aa10391a
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-hdHfxP.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-nwrxrv.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-nwrxrv.png
new file mode 100644
index 000000000000..08ff96c0c126
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-nwrxrv.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-1.png
new file mode 100644
index 000000000000..700c4e0d4d30
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-2.png
new file mode 100644
index 000000000000..bca16d416695
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-3.png
new file mode 100644
index 000000000000..01876eeff7a0
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-4.png
new file mode 100644
index 000000000000..fad0d90a71df
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-5.png
new file mode 100644
index 000000000000..d6567da245ae
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-6.png
new file mode 100644
index 000000000000..78b505e75298
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-7.png
new file mode 100644
index 000000000000..9771645376a2
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-8.png
new file mode 100644
index 000000000000..ca7f7e62e60c
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-8.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-1.png
new file mode 100644
index 000000000000..5fb4c0ec51f5
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-2.png
new file mode 100644
index 000000000000..714ad3891845
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-3.png
new file mode 100644
index 000000000000..e0d5a1a605d4
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/3-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/3-1.png
new file mode 100644
index 000000000000..10d9ef1b793a
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/3-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/3-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/3-2.png
new file mode 100644
index 000000000000..eef62cdfc5f9
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/3-2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/4-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/4-1.png
new file mode 100644
index 000000000000..880e86b32ce3
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/4-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-1.png
new file mode 100644
index 000000000000..b8f83e07b0d6
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-2.png
new file mode 100644
index 000000000000..b9b2404dc9ca
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-3.png
new file mode 100644
index 000000000000..5e22398cdfcd
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/fireboom-architecture.jpeg b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/fireboom-architecture.jpeg
new file mode 100644
index 000000000000..7f9660c847fa
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/fireboom-architecture.jpeg differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/fireboom-intro.jpeg b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/fireboom-intro.jpeg
new file mode 100644
index 000000000000..848bd2b5d97d
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/fireboom-intro.jpeg differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-0.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-0.png
new file mode 100644
index 000000000000..51d1b8564725
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-0.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-1.png
new file mode 100644
index 000000000000..403270869164
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-10.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-10.png
new file mode 100644
index 000000000000..ebcad36657b9
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-10.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-11.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-11.png
new file mode 100644
index 000000000000..7de1f375aa42
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-11.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-12.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-12.png
new file mode 100644
index 000000000000..32fea7a571b3
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-12.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-13.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-13.png
new file mode 100644
index 000000000000..da74e2ae6af6
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-13.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-14.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-14.png
new file mode 100644
index 000000000000..70d6971d9906
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-14.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-15.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-15.png
new file mode 100644
index 000000000000..cd370d79a7bf
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-15.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-16.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-16.png
new file mode 100644
index 000000000000..1c2de6f7d4c2
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-16.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-17.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-17.png
new file mode 100644
index 000000000000..a41d1df04ff1
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-17.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-2.png
new file mode 100644
index 000000000000..bca16d416695
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-3.png
new file mode 100644
index 000000000000..7cbc77cebe6d
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-4.png
new file mode 100644
index 000000000000..7011b7769934
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-5.png
new file mode 100644
index 000000000000..edadcc1ae00b
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-6.png
new file mode 100644
index 000000000000..4044b3367783
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-7.png
new file mode 100644
index 000000000000..e2cd50bc191b
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-8.png
new file mode 100644
index 000000000000..9db3b3c06cf8
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-8.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-9.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-9.png
new file mode 100644
index 000000000000..e8dffac2625c
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-9.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-1.png
new file mode 100644
index 000000000000..51d7a08dd355
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-10.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-10.png
new file mode 100644
index 000000000000..47ac9bd3b6ac
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-10.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-11.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-11.png
new file mode 100644
index 000000000000..986e50a61e62
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-11.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-12.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-12.png
new file mode 100644
index 000000000000..21e55e93ae98
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-12.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-13.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-13.png
new file mode 100644
index 000000000000..7911c018cf43
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-13.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-14.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-14.png
new file mode 100644
index 000000000000..c3d2cc3bde08
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-14.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-15.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-15.png
new file mode 100644
index 000000000000..11197b975ece
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-15.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-16.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-16.png
new file mode 100644
index 000000000000..d443d3d8dfe7
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-16.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-17.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-17.png
new file mode 100644
index 000000000000..1e88fe4f3c51
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-17.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-18.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-18.png
new file mode 100644
index 000000000000..b93a0dcfc9f5
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-18.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-19.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-19.png
new file mode 100644
index 000000000000..17c863592f83
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-19.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-2.png
new file mode 100644
index 000000000000..5a9d401f2631
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-20.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-20.png
new file mode 100644
index 000000000000..9ed9ed3435f6
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-20.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-21.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-21.png
new file mode 100644
index 000000000000..6a1c4a896f58
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-21.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-22.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-22.png
new file mode 100644
index 000000000000..d5e70681e57e
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-22.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-3.png
new file mode 100644
index 000000000000..29ba87c9cc57
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-4.png
new file mode 100644
index 000000000000..0790b5ce2e43
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-5.png
new file mode 100644
index 000000000000..f35eebc16cda
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-6.png
new file mode 100644
index 000000000000..933d0efde05f
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-7.png
new file mode 100644
index 000000000000..93a5e88a6e85
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-8.png
new file mode 100644
index 000000000000..9b41cf4dee07
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-8.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-9.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-9.png
new file mode 100644
index 000000000000..609c7c0959c2
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-9.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-1.png
new file mode 100644
index 000000000000..700c4e0d4d30
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-10.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-10.png
new file mode 100644
index 000000000000..0aa0f2546ce3
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-10.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-11.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-11.png
new file mode 100644
index 000000000000..ef702aadb7e3
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-11.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-2.png
new file mode 100644
index 000000000000..bca16d416695
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-3.png
new file mode 100644
index 000000000000..00edfb9382b2
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-4.png
new file mode 100644
index 000000000000..c947ad852ca0
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-5.png
new file mode 100644
index 000000000000..289789fd639f
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-6.png
new file mode 100644
index 000000000000..a58fbc16537b
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-7.png
new file mode 100644
index 000000000000..414ff44e067b
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-8.png
new file mode 100644
index 000000000000..dc7bb21bd4eb
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-8.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-9.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-9.png
new file mode 100644
index 000000000000..0011841a7d14
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-9.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/0.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/0.png
new file mode 100644
index 000000000000..3b8766799280
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/0.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/1.png
new file mode 100644
index 000000000000..8594460ab60b
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/10.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/10.png
new file mode 100644
index 000000000000..a8785255d657
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/10.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/11.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/11.png
new file mode 100644
index 000000000000..4582e3ffe3f2
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/11.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/12.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/12.png
new file mode 100644
index 000000000000..1eca4f3c1aba
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/12.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/13.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/13.png
new file mode 100644
index 000000000000..1c592128b98b
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/13.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/14.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/14.png
new file mode 100644
index 000000000000..d5ab998613d2
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/14.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/15.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/15.png
new file mode 100644
index 000000000000..d8ae3e93589d
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/15.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/16.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/16.png
new file mode 100644
index 000000000000..ab8def218f71
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/16.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/17.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/17.png
new file mode 100644
index 000000000000..6dedc0883d31
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/17.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/2.png
new file mode 100644
index 000000000000..0cbffe89e7bc
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/3.png
new file mode 100644
index 000000000000..d08b062b0a44
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/4.png
new file mode 100644
index 000000000000..97b8d6a7c99e
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/5.png
new file mode 100644
index 000000000000..3568a104c8c7
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/6.png
new file mode 100644
index 000000000000..6a25d0523ce2
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/7.png
new file mode 100644
index 000000000000..05553d347357
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/8.png
new file mode 100644
index 000000000000..50dbf2e31bc0
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/8.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/9.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/9.png
new file mode 100644
index 000000000000..5b117a1c33f6
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/9.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-0.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-0.png
new file mode 100644
index 000000000000..2281aaa41234
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-0.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-1.png
new file mode 100644
index 000000000000..a19a6b255b39
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-10.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-10.png
new file mode 100644
index 000000000000..6a7fb98ddb46
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-10.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-2.png
new file mode 100644
index 000000000000..0cbffe89e7bc
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-3.png
new file mode 100644
index 000000000000..56f6c056be69
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-4.png
new file mode 100644
index 000000000000..abda6fa2d152
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-5.png
new file mode 100644
index 000000000000..8c9b87c88d9e
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-6.png
new file mode 100644
index 000000000000..8aa71909987b
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-7.png
new file mode 100644
index 000000000000..810d8e793df6
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-8.png
new file mode 100644
index 000000000000..1a657270d137
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-8.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-9.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-9.png
new file mode 100644
index 000000000000..6aeebd4b6de1
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-9.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_1.png
new file mode 100644
index 000000000000..d641378d4201
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_10.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_10.png
new file mode 100644
index 000000000000..d1cd7993028d
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_10.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_11.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_11.png
new file mode 100644
index 000000000000..103bf474654c
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_11.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_12.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_12.png
new file mode 100644
index 000000000000..80c69d185806
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_12.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_13.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_13.png
new file mode 100644
index 000000000000..250114b79ef9
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_13.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_14.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_14.png
new file mode 100644
index 000000000000..d180432b02c4
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_14.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_15.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_15.png
new file mode 100644
index 000000000000..877112038094
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_15.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_16.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_16.png
new file mode 100644
index 000000000000..b490fade878c
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_16.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_17.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_17.png
new file mode 100644
index 000000000000..9d0bc0391604
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_17.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_2.png
new file mode 100644
index 000000000000..83254bdb6cd1
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_3.png
new file mode 100644
index 000000000000..1b0a1f926bb8
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_4.png
new file mode 100644
index 000000000000..86ad0a605c7d
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_5.png
new file mode 100644
index 000000000000..1a9a1566ae1c
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_6.png
new file mode 100644
index 000000000000..52b0dc908d42
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_7.png
new file mode 100644
index 000000000000..a18bfa6d1703
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_8.png
new file mode 100644
index 000000000000..484684f79ae4
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_8.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_9.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_9.png
new file mode 100644
index 000000000000..24db5cadb900
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_9.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/low-code-platform/install-appsmith.md b/docs/archived/4.0/i18n/zh-Hans/examples/low-code-platform/install-appsmith.md
new file mode 100644
index 000000000000..ff4f000b6d71
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/low-code-platform/install-appsmith.md
@@ -0,0 +1,65 @@
+# 快速安装 Appsmith
+
+[Appsmith](https://github.com/appsmithorg/appsmith) 是一个用于构建、部署和维护内部应用程序的开源平台。您可以构建任何东西,从简单的 CRUD 应用程序、管理面板、仪表板到自定义业务应用程序和复杂的多步骤工作流。
+
+## 步骤 1:在 [Sealos](https://cloud.sealos.io) 桌面环境中打开 「应用管理」 应用
+
+
+
+## 步骤 2:新建应用
+
+- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。
+
+
+
+## 步骤 3:应用部署
+
+- 基础配置:
+
+ - 应用名称(自定义):appsmith
+
+ - 镜像名(默认最新版本):appsmith/appsmith-ce
+
+ - CPU(推荐):2 Core
+
+ - 内存(推荐):4 G
+
+- 部署模式:
+
+ - 实例数(自定义):1
+
+
+
+- 网络配置:
+
+ - 容器暴露端口:80
+
+ - 外网访问:开启
+
+
+
+- 高级配置:
+
+ - 自定义本地存储,持久化 Appsmith 的数据(推荐 15 G)。
+
+
+
+## 步骤 4:部署应用
+
+- 点击「部署应用」开始部署应用。
+
+
+
+## 步骤 5:访问应用
+
+- 点击「详情」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。
+
+- 当 STATUS 为 Running,即可直接访问外网地址。
+
+
+
+- 访问成功!
+
+
+
+
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/low-code-platform/install-pageplug.md b/docs/archived/4.0/i18n/zh-Hans/examples/low-code-platform/install-pageplug.md
new file mode 100644
index 000000000000..2e80833180ba
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/low-code-platform/install-pageplug.md
@@ -0,0 +1,71 @@
+# 快速安装 PagePlug
+
+[PagePlug](https://github.com/cloudtogo/pageplug) 是 [Appsmith](https://github.com/appsmithorg/appsmith) 的中国化项目,基于 Appsmith 做了整体性能优化及汉化,也集合了特色表单解决方案 Formily 组件、图表解决方案 Echarts 组件、低代码小程序开发等,是面向研发使用的一个开源的、声明式的、可视化的、符合开发者直觉的前端低代码框架。
+
+## 步骤 1:在 [Sealos](https://cloud.sealos.io) 桌面环境中打开 「应用管理」 应用
+
+
+
+## 步骤 2:新建应用
+
+- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。
+
+
+
+## 步骤 3:应用部署
+
+- 基础配置:
+
+ - 应用名称(自定义):pageplug
+
+ - 镜像名(默认最新版本):cloudtogouser/pageplug-ce
+
+ - CPU(推荐):2 Core
+
+ - 内存(推荐):4 G
+
+- 部署模式:
+
+ - 实例数(自定义):1
+
+
+
+- 网络配置:
+
+ - 容器暴露端口:80
+
+ - 外网访问:开启
+
+
+
+- 高级配置:
+
+ - 自定义本地存储,持久化 PagePlug 的数据。
+
+
+
+## 步骤 4:部署应用
+
+- 点击「部署应用」开始部署应用。
+
+
+
+## 步骤 5:访问应用
+
+- 点击「详情」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。
+
+
+
+
+
+- 当 STATUS 为 Running,即可直接访问外网地址。
+
+
+
+- 若访问出现 503 异常,等待一会重试即可。
+
+
+
+- 访问成功!
+
+
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/others/install-anki.md b/docs/archived/4.0/i18n/zh-Hans/examples/others/install-anki.md
new file mode 100644
index 000000000000..2c5a619a4405
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/others/install-anki.md
@@ -0,0 +1,142 @@
+# 快速安装 Anki 自定义同步服务器
+
+Anki 是一个辅助记忆软件,其本质是一个卡片排序工具--**即依据使用者对卡片上的自定义内容进行主动测试、自我评判后,其内部算法根据评判结果更改每张卡片下次测试时间的排序工具。**
+
+所谓的卡片,专业说法叫 Flash Card(抽认卡或闪卡),是一小块纸片,分为正反两面,将问题和提示写在一面,将答案写在另一面。使用方法就是先看正面的问题与提示,在脑中回想答案,然后翻出反面进行对照验证。
+
+闪卡的核心制作原则就是:**一个知识点一张卡**。因此非常适合用来学习英文,也可以用来记忆历史事件时间、公式等等。
+
+Anki 的同步服务器在国外,还是一个个人项目,带宽很小,同步速度很慢,如果我们想在多个客户端之间同步学习进度和新增的知识点,那将非常痛苦。
+
+为了解决这个问题,我们需要部署一个自定义的同步服务器,然后让客户端去使用这个同步服务器。
+
+## Anki 同步服务器部署
+
+自从 2023 年 2 月份,Anki 发布了 PC 端 2.1.57 版本以后,Anki 的 PC 端,安卓端,iOS 端用户都可以自定义同步服务器了,并且不再需要安装插件。从此 Anki 小伙伴再也不用担心 Anki 同步的问题了,困扰 Anki 用户多年的同步问题终于得到彻底解决。
+
+社区目前只有 [anki-sync-server-rs](https://github.com/ankicommunity/anki-sync-server-rs) 这个项目支持最新的 Anki 版本,其他的同步服务器项目基本上都失效了。这个项目是用 Rust 写的,追踪 [Anki 官方](https://github.com/ankitects/anki) 同步服务端的进度,它们都是基于sqlite c 作为数据存储后端。
+
+下面我们将在 Sealos 中部署并配置 anki-sync-server-rs。
+
+首先在浏览器地址栏输入网址 [https://cloud.sealos.io/](https://cloud.sealos.io/) 进入 Sealos 桌面。然后打开「应用管理」:
+
+
+
+点击「新建应用」:
+
+
+
+依次输入应用名和镜像名,容器暴露端口是 `27701`,并开启外网访问:
+
+
+
+继续向下,展开「高级配置」,点击「编辑环境变量」:
+
+
+
+将以下内容粘贴到环境变量输入框中:
+
+```bash
+ANKISYNCD_USERNAME=
+ANKISYNCD_PASSWORD=
+```
+
+请将 `` 替换为你自己的用户名,将 `` 替换为你自己的密码。
+
+
+
+点击「新增存储卷」:
+
+
+
+挂载路径填入 `/app`,然后点击确认:
+
+
+
+最后点击右上角的部署应用即可。
+
+部署完成后点击「详情」进入应用详情界面。
+
+
+
+这里可以看到实例的运行状态,一定要等到状态是 running 才算是部署成功。如果一段时间以后状态还不是 running,可以点击「详情」查看故障原因:
+
+
+
+部署成功后,可以看到应用的运行情况,包括 CPU 占用、内存占用等。点击外网地址即可通过外网域名直接打开同步服务器的 Web 界面。
+
+
+
+如果出现以下的界面,则表示部署成功:
+
+
+
+查看日志的方法也很简单,直接点击实例右侧的「三个点」,然后点击「日志」即可查看日志:
+
+
+
+
+
+## 客户端设置
+
+### 桌面端
+
+桌面客户端(macOS/Windows/Linux)配置方法如下:
+
+1. 先打开「首选项」
+
+ 
+
+2. 点击「**网络**」,往下看,可以看到标有 `self-hosted sync server(自定义同步服务器)` 的方框,在里面填写您的服务端的地址:
+
+ 
+
+3. 重启 Anki,然后点击「**同步**」:
+
+ 
+
+4. 这时候会弹出一个输入框让你输入用户名和密码,你需要将你之前设置的用户名和密码输入进去:
+
+ 
+
+5. 点击确认后,就会开始同步了。
+
+
+### 安卓端
+
+安卓端也是直接配置即可,我的 AnkiDroid 版本是 `2.15.6`。你可以通过「设置 -> 高级设置 -> 自定义同步服务器」找到配置页面。
+
+
+
+再填写用户名和密码:
+
+> 设置 -> 常用设置 -> AnkiWeb账户
+
+这样就算配置完成了,所有的牌组都同步过来了。
+
+
+
+
+官方的版本比较旧,如果你想使用更激进的社区版本,可以到这个页面下载最新的 Beta 版:
+
++ [https://github.com/ankidroid/Anki-Android/releases](https://github.com/ankidroid/Anki-Android/releases)
+
+建议下载 **arm64-v8a** 版本。
+
+安装完成后,可以通过「设置 -> 同步 -> 自定义同步服务器」找到配置页面:
+
+
+
+再填写用户名和密码:
+
+> 设置 -> 同步 -> AnkiWeb账户
+
+### iOS 端
+
+AnkiMobile 也已经支持和自建的同步服务器同步了。至少对于版本 Ankimobile 2.0.90(20090.2) 来说,似乎是可行的,这是一位 iOS 系统用户[在 Anki 论坛报告的](https://forums.ankiweb.net/t/ankimobile-self-sync-server-failure-the-one-bundled-in-version-2-1-60-qt6/27862)。
+
+如果设置完成后发现不能同步可以参考下面的内容再试一次:
+
+> If you're using AnkiMobile and are unable to connect to a server on your local network, please go into the iOS settings, locate Anki near the bottom, and toggle "Allow Anki to access local network" off and then on again.
+
+上面的内容摘自 [ANki tutorial](https://docs.ankiweb.net/sync-server.html#client-setup)
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Go Apps.md b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Go Apps.md
new file mode 100644
index 000000000000..6d6a4f1adb0f
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Go Apps.md
@@ -0,0 +1,151 @@
+# 快速安装 Go 程序
+
+首先,确保你已经安装了以下工具:
+
+- Docker
+
+## 步骤1:编写你的 Go 程序
+
+```go
+package main
+
+import (
+ "fmt"
+ "net/http"
+)
+
+func main() {
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "Hello,Wolrd!")
+ })
+
+ http.ListenAndServe(":8080", nil)
+}
+```
+
+## 步骤2:创建Docker镜像
+
+- 在与`main.go`相同的目录中,创建一个名为`Dockerfile`的文件,包含以下内容:
+
+```
+FROM golang:1.17 as builder
+
+WORKDIR /app
+COPY . .
+
+RUN go mod init myapp
+RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
+
+FROM alpine:latest
+RUN apk --no-cache add ca-certificates
+WORKDIR /app
+COPY --from=builder /app/main /app/
+
+EXPOSE 8080
+ENTRYPOINT ["/app/main"]
+```
+
+这个`Dockerfile`定义了一个多阶段构建过程。在第一阶段,我们使用官方的`golang`镜像作为基础镜像,然后编译Go应用程序。在第二阶段,我们使用轻量级的`alpine`镜像,将编译好的二进制文件复制到`/app`目录,并将8080端口暴露。
+
+- 在`Dockerfile`所在的目录运行以下命令,为Go应用程序构建Docker镜像:
+
+```
+docker build -t your_image_name .
+```
+
+将`your_image_name`替换为您的镜像名称和标签。
+
+
+
+## 步骤3:推送Docker镜像
+
+- 将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像
+
+ - 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签:
+
+ ```
+ docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名,`your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。
+
+ 例如:
+
+ ```
+ docker tag demo damager6666/demo:latest
+ ```
+
+ - 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub:
+
+ ```
+ docker push your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如:
+
+ ```
+ docker push damager6666/demo:latest
+ ```
+
+## 步骤4:登陆 Sealos
+
+- 进入 [Sealos](https://cloud.sealos.io/) 官网
+
+
+
+## 步骤5:打开 「应用管理」 应用
+
+
+
+## 步骤 6:新建应用
+
+- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。
+
+
+
+## 步骤7:应用部署
+
+- 基础配置:
+
+ - 应用名称(自定义):go-demo
+ - 镜像名(**步骤5**中推送的镜像):damager6666/demo:latest
+ - CPU(推荐):1 Core
+ - 内存(推荐):1 G
+
+- 部署模式:
+
+ - 实例数(自定义):1
+
+
+
+- 网络配置:
+
+ - 容器暴露端口:8080
+
+ - 外网访问:开启
+
+
+
+## 步骤8:部署应用
+
+- 点击「部署应用」开始部署应用。
+
+
+
+## 步骤9:访问应用
+
+- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。
+- 当 STATUS 为 Running,即可直接访问外网地址。
+
+
+
+- 在浏览器上输入
+
+```
+https://tmgkflgdlstl.cloud.sealos.io/hello
+```
+
+
+
+- 页面上显示 ”Hello,World! “ ,表示你的 Go 应用程序已经在 Sealos 上运行
+
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Java Apps.md b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Java Apps.md
new file mode 100644
index 000000000000..9a036f53487a
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Java Apps.md
@@ -0,0 +1,272 @@
+# 快速安装 Java 程序
+
+首先,确保你已经安装了以下工具:
+
+- Docker
+- Maven
+
+## 步骤1:编写你的 Java 程序
+
+- 这里直接通过 [Spring Initializr](https://start.spring.io/) 生成生成一个基本的 Spring Boot 项目。
+
+
+
+- 打开项目的 `pom.xml` 文件,在 `` 部分添加以下内容,以包含MySQL JDBC驱动:
+
+ ```
+
+ mysql
+ mysql-connector-java
+
+ ```
+
+- 接下来,在 `src/main/resources` 目录下的 `application.properties` 文件中添加以下内容,以配置数据库连接:
+
+ ```
+ spring.datasource.url=jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
+ spring.datasource.username=root
+ spring.datasource.password=your_password
+ spring.jpa.hibernate.ddl-auto=update
+ ```
+
+## 步骤2:创建实体类和存储类
+
+在项目中创建一个新的Java类,如 `Person.java`,用于表示数据库中的实体。添加以下内容:
+
+```java
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+
+@Entity
+public class Person {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+ private String name;
+
+ // Getters and setters
+ // ...
+}
+```
+- 然后创建一个存储库接口,如 `PersonRepository.java`:
+
+ ```java
+ import org.springframework.data.jpa.repository.JpaRepository;
+ import org.springframework.stereotype.Repository;
+
+ @Repository
+ public interface PersonRepository extends JpaRepository {
+ }
+ ```
+
+## 步骤3:创建 Web 控制器
+
+- 在项目的 `src/main/java` 目录下,创建一个简单的 Web 控制器。例如,创建一个名为 `PersonController.java` 的文件,并添加以下内容:
+
+```java
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+public class PersonController {
+ @Autowired
+ private PersonRepository personRepository;
+
+ @GetMapping("/getPersons")
+ public String getAllPersons() {
+ List persons = personRepository.findAll();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < persons.size(); i++) {
+ sb.append("id: " + persons.get(i).getId() + " name: " + persons.get(i).getName() + "/n");
+ }
+ System.out.println(sb.toString());
+ return sb.toString();
+ }
+
+ @PostMapping(value = "/addPerson")
+ public Person addPerson(@RequestBody Person person) {
+ return personRepository.save(person);
+ }
+
+}
+```
+
+## 步骤4:构建应用程序
+
+- 使用maven构建项目,运行以下命令
+
+ ```
+ mvn clean install
+ ```
+
+ 
+
+## 步骤5:创建Docker镜像
+
+- 在 Java 项目的根目录下,创建一个名为 `Dockerfile` 的文件。这个文件将包含构建 Docker 镜像所需的指令。以下是一个简单的示例 Dockerfile:
+
+ ```
+ # 使用官方的 OpenJDK 镜像作为基础镜像
+ FROM openjdk:8-jre-slim
+
+ # 设置工作目录
+ WORKDIR /app
+
+ # 复制构建好的 JAR 文件到镜像中
+ COPY target/demo-0.0.1-SNAPSHOT.jar /app/demo-0.0.1-SNAPSHOT.jar
+
+ # 暴露应用程序的端口
+ EXPOSE 8080
+
+ # 设置启动命令
+ CMD ["java", "-jar", demo-0.0.1-SNAPSHOT.jar"]
+ ```
+
+- 在项目根目录下,运行以下命令构建 Docker 镜像:
+
+```
+docker build -t java-demo .
+```
+
+## 步骤6:推送Docker镜像
+
+- 将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像
+
+ - 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签:
+
+ ```
+ docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名,`your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。
+
+ 例如:
+
+ ```
+ docker tag java-demo damager6666/demo:v2
+ ```
+
+ - 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub:
+
+ ```
+ docker push your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如:
+
+ ```
+ docker push damager6666/java-demo:v2
+ ```
+
+## 步骤7:登陆 Sealos
+
+- 进入 [Sealos](https://cloud.sealos.io/) 官网
+
+
+
+## 步骤8:打开 「数据库」 应用
+
+
+
+## 步骤9:创建数据库
+
+- 点击新建数据库
+
+
+
+- 基础配置:
+ - 集群类型:mysql
+ - 集群名称 : demo-db
+ - CPU(推荐):1 Core
+ - 内存(推荐):1 G
+ - 实例数:1
+ - 存储容量: 1 Gi
+- 点击 部署集群
+
+## 步骤10:配置数据库
+
+
+
+- 点击一键连接到数据库执行以下语句:
+
+ - 创建 test_db 数据库
+
+ ```sql
+ create database test_db;
+ ```
+
+ - 创建 test 表
+
+ ```sql
+ CREATE TABLE `name_info` (
+ `id` int(11) NOT NULL,
+ `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ PRIMARY KEY (`id`)
+ );
+ ```
+
+ - 插入数据
+
+ ```sql
+ insert into test values (1,'Sealos');
+ ```
+
+## 步骤11:打开 「应用管理」 应用
+
+
+
+## 步骤 12:新建应用
+
+- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。
+
+
+
+## 步骤13:应用部署
+
+- 基础配置:
+
+ - 应用名称(自定义):java-demo
+ - 镜像名:damager6666/java-demo:v2
+ - CPU(推荐):1 Core
+ - 内存(推荐):1 G
+
+- 部署模式:
+
+ - 实例数(自定义):1
+
+
+
+- 网络配置:
+
+ - 容器暴露端口:8080
+
+ - 外网访问:开启
+
+
+
+## 步骤14:部署应用
+
+- 点击「部署应用」开始部署应用。
+
+
+
+## 步骤15:访问应用
+
+- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。
+- 当 STATUS 为 Running,即可直接访问外网地址。
+
+
+
+- 在浏览器上输入
+
+```
+https://tmgkflgdlstl.cloud.sealos.io/getPersons
+```
+
+
+
+- 页面上显示之间插入到数据库的数据 ,表示你的 Java 应用程序已经在 Sealos 上运行
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Node.js Apps.md b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Node.js Apps.md
new file mode 100644
index 000000000000..410c2a5ba93e
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Node.js Apps.md
@@ -0,0 +1,227 @@
+# 快速安装 Node.js 程序
+
+首先,确保你已经安装了以下工具:
+
+- Docker
+
+## 步骤1:编写你的 Node.js 程序
+
+- 在目录下创建一个名为 `app.js` 的文件,并添加以下代码:
+
+ ```js
+ const express = require('express');
+ const mysql = require('mysql');
+ const app = express();
+
+ const db = mysql.createConnection({
+ host: process.env.DB_HOST,
+ user: process.env.DB_USER,
+ password: process.env.DB_PASSWORD,
+ database: process.env.DB_NAME,
+ });
+
+ db.connect((err) => {
+ if (err) throw err;
+ console.log('Connected to the database.');
+ });
+
+ app.get('/', (req, res) => {
+ db.query('SELECT id, name FROM users', (err, results) => {
+ if (err) throw err;
+ res.send(JSON.stringify(results));
+ });
+ });
+
+ app.listen(8080, () => {
+ console.log('Server is running on port 8080');
+ });
+
+ ```
+
+- 在项目根目录下创建一个名为 `package.json` 的文件,并包含以下内容:
+
+ ```json
+ {
+ "name": "nodejs-web-app-mysql",
+ "version": "1.0.0",
+ "main": "app.js",
+ "scripts": {
+ "start": "node app.js"
+ },
+ "dependencies": {
+ "express": "^4.17.1",
+ "mysql": "^2.18.1"
+ }
+ }
+ ```
+
+## 步骤2:创建 Docker 镜像
+
+- 在项目的根目录下,创建一个名为 `Dockerfile` 的文件。这个文件将包含构建 Docker 镜像所需的指令。以下是一个简单的示例 Dockerfile:
+
+```dockerfile
+FROM node:16
+
+WORKDIR /app
+
+COPY package*.json ./
+
+RUN npm install
+
+COPY . .
+
+EXPOSE 8080
+
+CMD ["npm", "start"]
+```
+
+## 步骤3:构建 Docker 镜像
+
+- 在包含 `Dockerfile` 的目录中运行以下命令来构建 Docker 镜像:
+
+ ```
+ docker build -t nodejs-demo .
+ ```
+
+## 步骤4:推送Docker镜像
+
+- 将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像
+
+ - 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签:
+
+ ```
+ docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名,`your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。
+
+ 例如:
+
+ ```
+ docker tag nodejs-demo damager6666/nodejs-demo:v1
+ ```
+
+ - 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub:
+
+ ```
+ docker push your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如:
+
+ ```
+ docker push damager6666/nodejs-demo:v1
+ ```
+
+
+
+## 步骤5:登陆 Sealos
+
+- 进入 [Sealos](https://cloud.sealos.io/) 官网
+
+
+
+## 步骤6:打开 「数据库」 应用
+
+
+
+## 步骤7:创建数据库
+
+- 点击新建数据库
+
+
+
+- 基础配置:
+ - 集群类型:mysql
+ - 集群名称 : demo-db
+ - CPU(推荐):1 Core
+ - 内存(推荐):1 G
+ - 实例数:1
+ - 存储容量: 1 Gi
+- 点击 部署集群
+
+## 步骤8:配置数据库
+
+
+
+- 点击一键连接到数据库执行以下语句:
+
+ - 创建 test_db 数据库
+
+ ```sql
+ create database test_db;
+ ```
+
+ - 创建 test 表
+
+ ```sql
+ CREATE TABLE `name_info` (
+ `id` int(11) NOT NULL,
+ `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ PRIMARY KEY (`id`)
+ );
+ ```
+
+ - 插入数据
+
+ ```sql
+ insert into test values (1,'Sealos');
+ ```
+
+
+
+## 步骤9:打开 「应用管理」 应用
+
+
+
+## 步骤 10:新建应用
+
+- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。
+
+
+
+## 步骤11:应用部署
+
+- 基础配置:
+
+ - 应用名称(自定义):nodejs-demo
+ - 镜像名:damager6666/nodejs-demo:v1
+ - CPU(推荐):1 Core
+ - 内存(推荐):1 G
+
+- 部署模式:
+
+ - 实例数(自定义):1
+
+
+
+- 网络配置:
+
+ - 容器暴露端口:8080
+
+ - 外网访问:开启
+
+
+
+## 步骤12:部署应用
+
+- 点击「部署应用」开始部署应用。
+
+
+
+## 步骤13:访问应用
+
+- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。
+- 当 STATUS 为 Running,即可直接访问外网地址。
+
+
+
+- 在浏览器上输入
+
+```
+https://tmgkflgdlstl.cloud.sealos.io/getPersons
+```
+
+
+
+- 页面上显示之间插入到数据库的数据 ,表示你的 Node.js应用程序已经在 Sealos 上运行
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Python Apps.md b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Python Apps.md
new file mode 100644
index 000000000000..dcaae39c6e39
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Python Apps.md
@@ -0,0 +1,247 @@
+# 快速安装 Python 程序
+
+首先,确保你已经安装了以下工具:
+
+- Docker
+
+## 步骤1:编写你的 Python 程序
+
+- 在目录下创建一个名为 `app.py` 的文件,并添加以下代码:
+
+ ```python
+ from flask import Flask, render_template
+ import mysql.connector
+
+ app = Flask(__name__)
+
+ def get_data_from_database():
+ cnx = mysql.connector.connect(
+ host="java-demo-db-mysql.ns-7otl3mb2.svc",
+ user="root",
+ password="l9h8f24b",
+ database="test_db"
+ )
+
+ cursor = cnx.cursor()
+ cursor.execute("SELECT * FROM test")
+ data = cursor.fetchall()
+ cursor.close()
+ cnx.close()
+
+ return data
+
+ @app.route('/')
+ def index():
+ data = get_data_from_database()
+ return render_template('index.html', data=data)
+
+ if __name__ == '__main__':
+ app.run(host='0.0.0.0', debug=True)
+ ```
+
+- 在项目目录下创建一个名为 `requirements.txt` 的文件,以列出应用程序所需的依赖。
+
+```
+Flask
+mysql-connector-python
+```
+
+- 在项目目录中创建一个名为 `templates` 的文件夹,并在其中创建一个名为 `index.html` 的文件。将以下内容添加到 `index.html`:
+
+ ~~~html
+
+
+
+
+ Data from MySQL
+
+
+ Data from MySQL
+
+
+ | Column1 |
+ Column2 |
+ Column3 |
+
+ {% for row in data %}
+
+ | {{ row[0] }} |
+ {{ row[1] }} |
+ {{ row[2] }} |
+
+ {% endfor %}
+
+
+
+ ```
+
+ 请将 `Column1`、`Column2` 和 `Column3` 替换为您要显示的实际列名。
+ ~~~
+
+## 步骤2:创建 Docker 镜像
+
+- 在项目的根目录下,创建一个名为 `Dockerfile` 的文件。这个文件将包含构建 Docker 镜像所需的指令。以下是一个简单的示例 Dockerfile:
+
+ ```dockerfile
+ FROM python:3.8
+
+ WORKDIR /app
+
+ COPY requirements.txt .
+
+ RUN pip install --no-cache-dir -r requirements.txt
+
+ COPY . .
+
+ EXPOSE 5000
+
+ CMD ["python", "app.py"]
+ ```
+
+## 步骤3:构建 Docker 镜像
+
+- 在包含 `Dockerfile` 的目录中运行以下命令来构建 Docker 镜像:
+
+ ```
+ docker build -t python-demo .
+ ```
+
+## 步骤4:推送Docker镜像
+
+- 将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像
+
+ - 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签:
+
+ ```
+ docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名,`your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。
+
+ 例如:
+
+ ```
+ docker tag python-demo damager6666/python-demo:v1
+ ```
+
+ - 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub:
+
+ ```
+ docker push your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如:
+
+ ```
+ docker push damager6666/python-demo:v1
+ ```
+
+
+
+## 步骤5:登陆 Sealos
+
+- 进入 [Sealos](https://cloud.sealos.io/) 官网
+
+
+
+## 步骤6:打开 「数据库」 应用
+
+
+
+## 步骤7:创建数据库
+
+- 点击新建数据库
+
+
+
+- 基础配置:
+ - 集群类型:mysql
+ - 集群名称 : demo-db
+ - CPU(推荐):1 Core
+ - 内存(推荐):1 G
+ - 实例数:1
+ - 存储容量: 1 Gi
+- 点击 部署集群
+
+## 步骤8:配置数据库
+
+
+
+- 点击一键连接到数据库执行以下语句:
+
+ - 创建 test_db 数据库
+
+ ```sql
+ create database test_db;
+ ```
+
+ - 创建 test 表
+
+ ```sql
+ CREATE TABLE `name_info` (
+ `id` int(11) NOT NULL,
+ `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ PRIMARY KEY (`id`)
+ );
+ ```
+
+ - 插入数据
+
+ ```sql
+ insert into test values (1,'Sealos');
+ ```
+
+
+
+## 步骤9:打开 「应用管理」 应用
+
+
+
+## 步骤 10:新建应用
+
+- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。
+
+
+
+## 步骤11:应用部署
+
+- 基础配置:
+
+ - 应用名称(自定义):python-demo
+ - 镜像名:damager6666/python-demo:v1
+ - CPU(推荐):1 Core
+ - 内存(推荐):1 G
+
+- 部署模式:
+
+ - 实例数(自定义):1
+
+
+
+- 网络配置:
+
+ - 容器暴露端口:5000
+
+
+ - 外网访问:开启
+
+
+
+## 步骤12:部署应用
+
+
+
+## 步骤13:访问应用
+
+- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。
+- 当 STATUS 为 Running,即可直接访问外网地址。
+
+
+
+- 在浏览器上输入外网地址,可以访问到刚刚存入到 MySQL 中的数据,表示程序已经成功 Sealos 上
+
+ ```http
+ https://mkqreaqfgwac.cloud.sealos.io/
+ ```
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Vue Apps.md b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Vue Apps.md
new file mode 100644
index 000000000000..1b9873b42e09
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Vue Apps.md
@@ -0,0 +1,144 @@
+# 快速安装 Vue 程序
+
+首先,确保你已经安装了以下工具:
+
+- Docker
+- Node.js
+- npm
+
+## 步骤1:安装 Vue CLI
+
+```bash
+npm install -g @vue/cli
+```
+
+## 步骤2:使用 Vue CLI 创建一个新项目
+
+```bash
+vue create my-vue-app
+```
+
+## 步骤3:使用 Vue CLI 构建项目的生产版本
+
+```bash
+cd my-vue-app
+npm run build
+```
+
+## 步骤4:创建Docker镜像
+
+- 在与my-vue-app目录中,创建一个名为`Dockerfile`的文件,包含以下内容:
+
+```dockerfile
+# 使用 nginx 镜像作为基础镜像
+FROM nginx:1.21-alpine
+
+# 复制构建的前端文件到 nginx 容器中
+COPY dist/ /usr/share/nginx/html
+
+# 暴露端口
+EXPOSE 80
+
+# 启动 nginx
+CMD ["nginx", "-g", "daemon off;"]
+```
+
+- 在`Dockerfile`所在的目录运行以下命令,为 Vue 应用程序构建 Docker 镜像:
+
+```bash
+docker build -t my-vue-app .
+```
+
+## 步骤5:推送Docker镜像
+
+将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像
+
+- 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签:
+
+ ```
+ docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名,`your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。
+
+ 例如:
+
+ ```
+ docker tag my-vue-app damager6666/my-vue-app:v1
+ ```
+
+- 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub:
+
+ ```
+ docker push your-dockerhub-username/your-repo-name:your-tag
+ ```
+
+ 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如:
+
+ ```
+ docker push damager6666/my-vue-app:v1
+ ```
+
+
+## 步骤6:登录 Sealos
+
+- 进入 [Sealos](https://cloud.sealos.io/) 官网
+
+
+
+## 步骤7:打开 「应用管理」 应用
+
+
+
+## 步骤 8:新建应用
+
+- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。
+
+
+
+## 步骤9:应用部署
+
+- 基础配置:
+
+ - 应用名称(自定义):my-vue-app
+ - 镜像名(**步骤5**中推送的镜像):damager6666/my-vue-app:v1
+ - CPU(推荐):1 Core
+ - 内存(推荐):1 G
+
+- 部署模式:
+
+ - 实例数(自定义):1
+
+
+
+- 网络配置:
+
+ - 容器暴露端口:80
+
+ - 外网访问:开启
+
+
+
+## 步骤10:部署应用
+
+- 点击「部署应用」开始部署应用。
+
+
+
+## 步骤11:访问应用
+
+- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。
+- 当 STATUS 为 Running,即可直接访问外网地址。
+
+
+
+- 在浏览器上输入
+
+```
+https://mvpztqzczudy.cloud.sealos.io
+```
+
+
+
+- 页面上显示 ”Hello,Vue.js! “ ,表示你的 Vue 应用程序已经在 Sealos 上运行
+
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-0.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-0.png
new file mode 100644
index 000000000000..f856ec873a65
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-0.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-1.png
new file mode 100644
index 000000000000..83b3f85e4441
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-10.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-10.png
new file mode 100644
index 000000000000..7164ba6a31fc
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-10.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-11.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-11.png
new file mode 100644
index 000000000000..b1cf562ca8c0
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-11.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-12.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-12.png
new file mode 100644
index 000000000000..6eedee90f87c
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-12.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-13.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-13.png
new file mode 100644
index 000000000000..3548b505e15f
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-13.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-2.png
new file mode 100644
index 000000000000..179f4b85a5fb
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-3.png
new file mode 100644
index 000000000000..07db46ce9e6f
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-4.png
new file mode 100644
index 000000000000..670338bb9b00
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-5.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-5.png
new file mode 100644
index 000000000000..a79b84e285a8
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-6.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-6.png
new file mode 100644
index 000000000000..d5f28559a335
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-7.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-7.png
new file mode 100644
index 000000000000..4a14b632edd4
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-8.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-8.png
new file mode 100644
index 000000000000..19a4f0f3276e
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-8.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-9.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-9.png
new file mode 100644
index 000000000000..b5582f255cd6
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-9.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/nodejs-example-0.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/nodejs-example-0.png
new file mode 100644
index 000000000000..629be8f5f144
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/nodejs-example-0.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/nodejs-example-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/nodejs-example-1.png
new file mode 100644
index 000000000000..83d4acd117f7
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/nodejs-example-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-0.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-0.png
new file mode 100644
index 000000000000..86d53a8b85c7
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-0.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-1.png
new file mode 100644
index 000000000000..2f03cf92b397
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-2.png
new file mode 100644
index 000000000000..19dcda99374a
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-3.png
new file mode 100644
index 000000000000..54ceb5547f34
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-4.png
new file mode 100644
index 000000000000..3ffbe07560ba
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-0.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-0.png
new file mode 100644
index 000000000000..648276d14120
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-0.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-1.png
new file mode 100644
index 000000000000..e47a1b27115d
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-2.png
new file mode 100644
index 000000000000..dab8d56621b8
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-3.png
new file mode 100644
index 000000000000..9ac654f24398
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-4.png
new file mode 100644
index 000000000000..b00209521090
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/social-communication/install-tailchat.md b/docs/archived/4.0/i18n/zh-Hans/examples/social-communication/install-tailchat.md
new file mode 100644
index 000000000000..b2c53ab1004d
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/examples/social-communication/install-tailchat.md
@@ -0,0 +1,177 @@
+# 快速安装 Tailchat
+
+[Tailchat](https://tailchat.msgbyte.com/) 是一款插件化易拓展的开源 IM 应用。可拓展架构赋予 Tailchat 无限可能性。
+
+前端微内核架构 + 后端微服务架构 使得 Tailchat 能够驾驭任何定制化/私有化的场景
+
+面向企业与私域用户打造,高度自由的群组管理与定制化的面板展示可以让私域主能够更好的展示自己的作品,管理用户,打造自己的品牌与圈子。
+
+
+
+
+## 首先在 [Sealos](https://cloud.sealos.io) 桌面环境中打开「应用管理」
+
+
+
+## 新建应用
+
+
+
+### 创建依赖
+
+`tailchat` 作为企业级的应用,最小依赖: `mongodb`, `redis`, `minio`.
+
+接下来让我们来一一创建。
+
+#### mongodb
+
+为了方便起见我们固定一个实例,并且为数据库绑定本地存储。
+
+使用的镜像是 `mongo:4`
+
+需要注意的是因为我没有给数据库设置密码,因此不要对外网提供网络服务。容器暴露端口填数据库默认服务端口 `27017` 即可
+
+内容如下:
+
+
+
+点击部署应用提交部署
+
+耐心等待一会,就可以看到应用已经启动起来了
+
+
+
+> 需要注意的是初始分配的64m对于mongodb来说实在太小了,所以通过变更应用改为了128m。可以随时分配资源大小这也是sealos/k8s很方便的一点
+
+#### minio
+
+接下来我们创建minio, minio是一个开源的对象存储服务。我们同样可以通过`sealos`的点点点来快速创建
+
+使用的镜像是: `minio/minio`
+
+需要注意的是我们要进行一些调整:
+
+- 暴露端口: 9000
+- 运行命令改为: `minio`
+- 命令参数改为: `server /data`
+- 设置环境变量:
+ - MINIO_ROOT_USER: tailchat
+ - MINIO_ROOT_PASSWORD: com.msgbyte.tailchat
+- 本地存储: `/data`
+
+最终结果如下:
+
+
+
+点击部署按钮同样看到服务已经正常启动起来了。
+
+#### redis
+
+最后我们需要部署redis作为内容缓存与信息转发。
+
+使用镜像: `redis:alpine`
+
+暴露端口: `6379`
+
+最终结果如下:
+
+
+
+
+### 创建 Tailchat 本体
+
+此时Tailchat所需要的依赖均已部署完毕,如下:
+
+
+
+现在我们来部署 `Tailchat` 本体。
+
+`Tailchat` 的本体会相对复杂一点,不过因为`sealos`纯UI操作也不会太过复杂。
+
+- 使用镜像: `moonrailgun/tailchat`
+- 暴露端口: `11000`(记得要打开外网访问)
+- 配置环境变量如下:
+ ```
+ SERVICEDIR=services,plugins
+ TRANSPORTER=redis://redis:6379
+ REDIS_URL=redis://redis:6379
+ MONGO_URL=mongodb://mongo/tailchat
+ MINIO_URL=minio:9000
+ MINIO_USER=tailchat
+ MINIO_PASS=com.msgbyte.tailchat
+ ```
+
+最终效果如下:
+
+
+
+耐心等待一段时间后可以看到`Tailchat` 服务已经启动起来了
+
+
+
+## 预览服务
+
+首先我们可以先检查一下`Tailchat`服务的可用性,可以通过外网地址提供的服务后面加上 `/health` 来检查服务可用性, 如: `https://.cloud.sealos.io/health`
+
+当启动完毕后,Tailchat服务会返回如下内容:
+
+
+
+这段json字符串中包含了使用的镜像版本,节点名称,系统占用,微服务加载情况。
+
+这里我们可以看到我的常见的服务, 如`user`/`chat.message`以及一些带有插件前缀的服务如`plugin.registry`都已经正常启动起来了,说明我们的服务端是正常运行的。
+
+现在我们可以直接访问我们的外网地址,可以看到经过短暂的加载后,页面正常打开自动跳转到了登录界面。
+
+
+
+随便注册一个账号,可以看到我们可以正常进入Tailchat的主界面, 如下图:
+
+
+
+至此我们的服务已经成功在 sealos 中落地了。
+
+## 扩容服务
+
+当然,作为一个分布式架构的系统,`Tailchat` 天然是支持水平扩容的。而在 `sealos` 想要实现扩容也非常简单, 只需要通过变更操作修改实例数即可:
+
+
+
+
+
+
+
+此时当我们访问 `https://.cloud.sealos.io/health` 可以看到我们可以访问到不同的节点
+
+
+
+## 添加 Tailchat 入口到桌面
+
+打开 Terminal, 输入`vim app.yml`创建并编辑一个配置文件
+
+输入以下内容,注意url要换成自己部署的网址
+
+```yml
+apiVersion: app.sealos.io/v1
+kind: App
+metadata:
+ name: tailchat-app-entry
+spec:
+ name: Tailchat
+ icon:
+ type: iframe
+ data:
+ url:
+ desc:
+ icon: https://tailchat.msgbyte.com/img/logo.svg
+ menuData:
+ displayType: normal
+```
+
+按`esc`退出编辑模式, 按`:wq`保存并退出vim
+
+输入`kubectl apply -f app.yml`启动配置。
+
+完毕后刷新页面,此时我们可以看到我们的入口就出现在`sealos`的桌面上了
+
+
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/add-domain.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/add-domain.md
new file mode 100644
index 000000000000..564035e57741
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/add-domain.md
@@ -0,0 +1,31 @@
+---
+sidebar_position: 2
+---
+
+# 自定义域名
+
+想让您的用户能够享受与您的品牌一致的专属体验?只需要在 [Sealos](https://cloud.sealos.io) 中为你的项目指定一个自定义域名。
+
+## 部署时接入
+
+如果您在部署应用时接入自定义域名,只需要在部署应用时打开「外网访问」,Sealos 会给您的应用自动分配一个外网域名。
+
+
+
+然后需要到您的域名服务商处,添加该域名的 `CNAME` 解析到上面分配的外网域名。以阿里云为例:
+
+
+
+等待解析生效后即可回到 Sealos 中绑定自定义域名,直接点击右侧的「自定义域名」:
+
+
+
+在弹出的界面中输入您的自定义域名,然后点击确认即可。
+
+
+
+最终点击右上角的「部署」开始部署应用,部署完成后点击外网地址即可通过自定义域名访问应用。
+
+## 部署完成后接入
+
+已经部署完成的应用可以在应用详情页面的右上角点击「变更」,然后参考之前的步骤接入自定义域名即可。
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/applaunchpad.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/applaunchpad.md
new file mode 100644
index 000000000000..558d9e3c362d
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/applaunchpad.md
@@ -0,0 +1,55 @@
+---
+sidebar_position: 1
+---
+
+# 应用管理
+
+**应用管理** 是 Sealos 内置的单镜像部署工具,主要用于简化和加速应用程序的部署过程,可以帮助您在 5 分钟内完成应用的部署和上线。
+
+目前「应用管理」具备以下功能:
+
+- 支持使用私有镜像部署应用;
+- 支持根据应用需求,自定义所需的 CPU 和内存资源;
+- 支持多副本;
+- 弹性伸缩 (HPA);
+- 提供外网访问地址,便于公网访问;
+- 允许用户为应用配置自定义域名,提高品牌识别度和用户体验;
+- ConfigMap 配置文件;
+- 应用数据的持久化存储,保障数据的安全性和持续性;
+- 提供应用和 Pod 的实时监控,帮助用户及时发现并解决问题;
+- 记录和管理应用日志,便于问题追踪和性能分析;
+- 分析系统事件(Events),提供关键信息帮助优化应用性能;
+- 一键进入容器终端,方便管理和调试;
+- 支持将应用的多个端口暴露到外网。
+
+## [快速开始](/quick-start/use-app-launchpad.md)
+
+快速安装一些比较常见的应用。
+
+## [更新应用](/guides/applaunchpad/update-app.md)
+
+应用部署完成后修改应用配置。
+
+## [自定义域名](/guides/applaunchpad/add-domain.md)
+
+为应用接入自定义域名。
+
+## [暴露多端口](/guides/applaunchpad/expose-multi-ports.md)
+
+将应用的多个端口暴露到外网中。
+
+## [环境变量](/guides/applaunchpad/environment.md)
+
+通过环境变量为应用提供配置信息。
+
+## [配置文件](/guides/applaunchpad/configmap.md)
+
+通过配置文件为应用提供配置信息。
+
+## [弹性伸缩](/guides/applaunchpad/autoscale.md)
+
+通过弹性伸缩来根据负载自动调整应用的实例数量。
+
+## [持久化存储](/guides/applaunchpad/persistent-volume.md)
+
+使用持久化存储来保障数据的持久化。
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/autoscale.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/autoscale.md
new file mode 100644
index 000000000000..8675aa2553a4
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/autoscale.md
@@ -0,0 +1,24 @@
+---
+sidebar_position: 6
+---
+
+# 弹性伸缩
+
+「[应用管理](/guides/applaunchpad/applaunchpad.md)」可以在 Sealos 集群中自动调整应用的实例数量,以适应不同的负载条件。这个功能叫「**弹性伸缩**(Horizontal Pod Autoscaler,简称 HPA)」。
+
+「弹性伸缩」通过监测特定的度量指标(如 CPU 使用率或内存使用率)来动态增加或减少应用的实例数量,以确保应用程序运行效率和资源利用的最优化。
+
+> 注意:在弹性伸缩的上下文中,提到的“使用率”通常是指应用实例的平均使用率。例如,如果一个应用有两个实例,那么其 CPU 平均使用率将是这两个实例的 CPU 使用率的平均值。
+
+弹性伸缩的工作原理:
+
+1. **监控**:持续监控应用的关键性能指标,如 CPU 使用率或内存使用量。
+2. **决策**:根据设定的目标(例如 CPU 使用率不超过 50%),系统会计算出为了达到这一目标所需增加或减少的应用实例数量。
+3. **调整**:基于计算结果,弹性伸缩会自动指示 Sealos 的控制器增加或减少应用实例的数量,以保持资源使用率在预定目标范围内。
+
+假设我们为一个应用设置了如下规则:CPU 使用率不得超过 50%,实例数量可在 1 至 5 之间变动。在这种设定下,弹性伸缩将会:
+
+- 当应用的平均 CPU 使用率超过 50% 时,系统会增加实例数量,最多增至 5 个。
+- 当应用的平均 CPU 使用率低于 50% 时,系统会减少实例数量,但至少保持 1 个实例运行。
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/configmap.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/configmap.md
new file mode 100644
index 000000000000..df100c517418
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/configmap.md
@@ -0,0 +1,17 @@
+---
+sidebar_position: 5
+---
+
+# 配置文件
+
+在 [Sealos](https://cloud.sealos.io) 应用程序中,配置文件的使用非常重要,尤其是当应用程序需要处理大量或复杂的配置信息时。与环境变量相比,配置文件可以提供一种更灵活、更可靠的配置管理方式。环境变量更适合用于传递少量、简单的配置数据。
+
+配置文件的主要优势在于其能够存储和管理复杂的配置数据,这些配置数据可以包含配置文件、命令行参数、环境变量等信息。这些数据可以在应用容器启动时注入到容器中,从而允许你对应用程序的行为进行调整,而无需重新构建镜像。
+
+下面以 Nginx 容器为例,说明如何使用配置文件来调整应用的行为:
+
+- **文件名**: 对应 Nginx 容器里的文件,可以参考镜像提供者给出的说明。
+- **文件值**: 文件对应的内容。如果内容过于复杂,建议在本地编辑完成后,再复制粘贴到相应位置。
+- **注意事项**:配置文件是挂载单个文件,而不是挂载目录。你需要明确指定要挂载的具体文件,而非仅仅指定一个目录路径。
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/environment.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/environment.md
new file mode 100644
index 000000000000..3c9c97ba0d10
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/environment.md
@@ -0,0 +1,28 @@
+---
+sidebar_position: 4
+---
+
+# 环境变量
+
+在 [Sealos](https://cloud.sealos.io) 中,环境变量扮演着至关重要的角色,它们是管理容器应用配置数据的关键工具。通过使用环境变量,可以为应用提供必要的配置信息,而无需直接修改应用的代码或其镜像,从而提高应用的可维护性和可扩展性。
+
+在「[应用管理](/guides/applaunchpad/applaunchpad.md)」界面,环境变量是通过批量输入的方式来定义的。用户可以通过按行输入的方式来定义多个环境变量,其中每个环境变量由键(key)和值(value)组成,两者之间可以使用等号(=)或冒号(:)作为分隔符,「[应用管理](/guides/applaunchpad/applaunchpad.md)」会自动清除键(key)中的无效字符,以确保环境变量的正确性和有效性。
+
+
+
+**可正常解析的环境变量格式:**
+
+```bash
+host=127.0.0.1
+port:3000
+name: sealos
+- username=123
+- password:123
+# 这一行的注释会被忽略。因为不包含等号(=)或冒号(:)这两种标记字符。
+```
+
+**不可解析的环境变量格式:**
+
+```bash
+host=127.0.0.1 # 这一行会被解析,因为包含了等号(=)或冒号(:)这两种标记字符。这里的注释也会被解析,因为前面已经有等号(=)了。
+```
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/expose-multi-ports.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/expose-multi-ports.md
new file mode 100644
index 000000000000..7983429dcbbd
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/expose-multi-ports.md
@@ -0,0 +1,19 @@
+---
+sidebar_position: 3
+---
+
+# 暴露多端口
+
+在复杂的应用环境中,经常需要应用服务同时暴露多个端口以满足不同的需求。这种需求可能出现在多种场景下:
+
++ **多协议支持**:例如,一个应用可能同时支持 HTTP 和 HTTPS 协议,需要暴露 80 和 443 端口。
++ **应用的多功能**:例如,一个应用可能有一个 Web 服务和一个 Admin 服务,它们分别监听不同的端口。
++ **兼容性考虑**:为了与旧版本或其他服务兼容,可能需要同时暴露新旧两种接口的端口。
++ **Prometheus 监控和应用服务**:如果你的应用既有业务端口,又有一个用于 Prometheus 监控的 `/metrics` 端口,你可能需要同时暴露两者。
++ **GRPC 和 RESTful 服务并存**:如果你的应用同时提供 GRPC 和 RESTful 服务,你可能需要为每种服务类型暴露不同的端口。
+
+[Sealos](https://cloud.sealos.io) 在使用「[应用管理](/guides/applaunchpad/applaunchpad.md)」部署应用时,可以轻松地选择暴露多个端口。在部署应用的过程中,用户只需要点击「网络配置」选项,然后选择「添加端口」,即可实现多端口的配置。
+
+
+
+此外,Sealos 平台还提供了端口的外网访问功能。一旦暴露到公网,每一个暴露的端口都会被分配一个独立的二级域名,使得用户可以更方便地进行远程访问和管理。
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/11.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/11.png
new file mode 100644
index 000000000000..a4ba01524001
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/11.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/aliyun-cname.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/aliyun-cname.png
new file mode 100644
index 000000000000..54651df59314
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/aliyun-cname.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain.png
new file mode 100644
index 000000000000..9e88bb76dc3d
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain2.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain2.png
new file mode 100644
index 000000000000..b5a9dd12dc40
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain3.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain3.png
new file mode 100644
index 000000000000..5eb0377da0e8
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad1.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad1.png
new file mode 100644
index 000000000000..0e2d87f96122
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad10.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad10.png
new file mode 100644
index 000000000000..37bbf7ab57d5
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad10.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad11.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad11.png
new file mode 100644
index 000000000000..ceeb631f3120
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad11.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad12.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad12.png
new file mode 100644
index 000000000000..a0189b569ab8
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad12.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad13.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad13.png
new file mode 100644
index 000000000000..9317aa9f5c5d
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad13.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad14.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad14.png
new file mode 100644
index 000000000000..a7551be28826
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad14.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad2.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad2.png
new file mode 100644
index 000000000000..016395059908
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad3.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad3.png
new file mode 100644
index 000000000000..fd28e1f9fb6b
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad4.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad4.png
new file mode 100644
index 000000000000..82b4603f4ca3
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad5.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad5.png
new file mode 100644
index 000000000000..c4f8f7c33d95
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad6.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad6.png
new file mode 100644
index 000000000000..6019b61ca10f
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad7.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad7.png
new file mode 100644
index 000000000000..73fcdcc39cf3
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad8.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad8.png
new file mode 100644
index 000000000000..227d5f1b8cff
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad8.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad9.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad9.png
new file mode 100644
index 000000000000..77eb0919f867
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad9.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/autoscale.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/autoscale.png
new file mode 100644
index 000000000000..e8b4907448f6
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/autoscale.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/multi-ports1.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/multi-ports1.png
new file mode 100644
index 000000000000..ab08b16299b9
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/multi-ports1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/persistent-volume.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/persistent-volume.png
new file mode 100644
index 000000000000..b904addd338a
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/persistent-volume.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert1.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert1.png
new file mode 100644
index 000000000000..373e56b2bee4
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert2.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert2.png
new file mode 100644
index 000000000000..b7ea59eed18a
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert3.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert3.png
new file mode 100644
index 000000000000..0fddf4574890
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert4.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert4.png
new file mode 100644
index 000000000000..aa4fc627bc11
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/persistent-volume.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/persistent-volume.md
new file mode 100644
index 000000000000..6c9377648120
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/persistent-volume.md
@@ -0,0 +1,13 @@
+---
+sidebar_position: 7
+---
+
+# 持久化存储
+
+在 [Sealos](https://cloud.sealos.io) 中,容器可以轻松地被创建和销毁。虽然这为部署和管理应用带来了灵活性,但它也带来了数据持久性的挑战。如果应用在容器内部存储数据,那么当容器被销毁时,所有存储在容器内的数据也会丢失。
+
+持久化存储可以确保数据存储在容器外部,即使在容器重启或重新部署的情况下也能保证数据不会丢失。**如果你的应用需要保留数据(例如数据库、文件存储系统或任何存储用户数据的服务),一定要使用持久化存储!**
+
+假设你要部署一个 Nextcloud,Nextcloud 容器的所有数据都存储在 `/var/www/html` 这个目录下,因此我们需要将该目录下的数据通过外挂存储进行持久化。
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/set-cert.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/set-cert.md
new file mode 100644
index 000000000000..f865fd7aa47e
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/set-cert.md
@@ -0,0 +1,51 @@
+---
+sidebar_position: 8
+---
+
+# 设置自定义域名证书
+
+在「应用管理」中成功设置自定义域名,但是无法访问域名并显示证书不安全。 这是因为 cert-manager 没有成功签发证书。为了解决这个问题,我们可以手动设置证书。
+
+首先,确认在云厂商中成功设置 CNAME,并且下载域名对应的证书。
+
+打开「应用管理」,设置自定义域名。
+
+
+
+打开「终端」,依次执行以下命令。
+
+```bash
+# 用证书文件信息创建 tls.crt(使用证书文件信息替换掉 xxxx)
+cat > tls.crt < tls.key <的最大连接数 |
+| max_wal_size | 设置WAL文件的最大大小 |
+| min_wal_size | 设置WAL文件的最小大小 |
+| max_worker_processes | 设置PostgreSQL可以启动 的最大后台进程数 |
+| shared_buffers | 设置PostgreSQL可以启动 的最大后台进程数 |
+
+**常见的mysql参数**:
+
+| 参数名 | 描述 |
+|-------------------------|-----------------------------------|
+| innodb_buffer_pool_size | 设置InnoDB缓冲池的大小 |
+| max_connections | 允许的最大并发连接数 |
+| query_cache_size | 查询缓存的大小 |
+| thread_cache_size | 线程缓存的大小 |
+| max_allowed_packet | 最大数据包大小 |
+| innodb_log_file_size | InnoDB日志文件的大小 |
+
+**常见的mongo参数**:
+
+| 参数名 | 描述 |
+|-------------------------|------------------------------------|
+| storage.dbPath | 数据文件存放路径 |
+| storage.journal.enabled | 启用日志 |
+| net.port | 服务器端口 |
+| net.bindIp | 绑定的IP地址 |
+
+**常见的redis参数**:
+
+| 参数名 | 描述 |
+|------------------------|-------------------------------------|
+| maxclients | 最大客户端连接数 |
+| maxmemory | 最大内存使用量 |
+| maxmemory-policy | 内存淘汰策略 |
+| appendonly | AOF持久化开关 |
+| appendfsync | AOF文件刷新频率 |
+
+
+4. **应用pg-config.yaml**:
+
+```bash
+$ kubectl apply -f pg-config.yaml
+```
+
+
+5. **检查pg-config.yaml是否应用成功**:
+```bash
+# OpsRequest中对应的状态为Succeed且pod对应的状态为Running则说明配置应用成功
+$ kubectl get OpsRequest
+$ kubectl get pod
+```
+
+
+6. **进入数据库查看配置是否生效**:
+```bash
+$ show max_connections;
+```
+
+
+
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_1.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_1.png
new file mode 100644
index 000000000000..d8c8c62be3a6
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_2.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_2.png
new file mode 100644
index 000000000000..4bd234485bf3
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_3.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_3.png
new file mode 100644
index 000000000000..1f6d2ff3383b
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_4.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_4.png
new file mode 100644
index 000000000000..02f9a61edddf
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_5.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_5.png
new file mode 100644
index 000000000000..412e0bbe7fc7
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_6.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_6.png
new file mode 100644
index 000000000000..5f49dd84f12e
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_7.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_7.png
new file mode 100644
index 000000000000..88f930a876f0
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/dbprovider.md b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/dbprovider.md
new file mode 100644
index 000000000000..16de7edf2d5e
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/dbprovider.md
@@ -0,0 +1,208 @@
+---
+sidebar_position: 2
+---
+
+# 数据库
+
+**数据库** 是一个应用的核心组成,Sealos 提供了一个数据库集群部署管理工具,可以帮助你快速部署数据库集群。目前支持 MySQL、PostgreSQL、MongoDB、Redis、Kafka等。目前**数据库** 支持如下特性。
+
++ 弹性伸缩
++ 支持多版本、多类型 数据库
++ 数据库 连接:一键连接、外网访问
++ 数据库 连接:手动备份、自动备份
++ 数据库 监控:资源、状态、性能监控
++ 数据库 迁移:公网迁移、文件迁移
++ 数据库 高可用:支持多个 数据库 实例
++ 可视化 数据库 管理:新建、删除、更新 、暂停、重启 数据库
+
+
+## 快速开始
+
+以部署一个 PostgreSQL 数据库为例,体验 **数据库** 的便捷性。
+
+只需 6 步即可完成部署和访问:
+
+1. 在 [Sealos](https://cloud.sealos.io) 桌面进入 **数据库** 应用:
+
+
+
+2. 点击新建集群:
+
+
+
+3. 填写数据库名并选择对应的数据库和参数:
+
+
+
+4. 点击部署:
+
+
+
+5. 查看数据库详情信息:
+
+
+
+6. 数据库详情界面:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## 数据库连接
+
+### 一键连接
+
+1. 进入 **数据库** ,点击连接:
+
+
+
+2. 在终端操作数据库:
+
+
+
+
+### 外网访问
+
+1. 进入 **数据库** ,开启外网访问:
+
+
+
+2. 确认开启:
+
+
+
+3. 复制数据库连接信息:
+
+
+
+4. 在数据库连接工具中连接数据库:
+
+
+
+
+
+
+## 数据库备份
+
+### 手动备份
+1. 进入数据库备份界面,点击备份:
+
+
+
+2. 填写备份信息,开始备份:
+
+
+
+3. 查看备份状态:
+
+
+
+
+
+
+### 自动备份
+1. 进入数据库备份界面,点击备份:
+
+
+
+2. 开启自动备份,填写备份信息:
+
+
+
+
+## 数据库迁移
+
+以 MySQL 数据库为例,介绍 数据库迁移 的过程。
+
+### 公网迁移
+
+公网迁移涉及两个数据库:源数据库、目标数据库。源数据库是迁移的数据来源,目标数据库是迁移的数据目的地。以下介绍中以本地数据库做为源数据库进行演示。
+
+1. 进入目标数据库,连接目标数据库:
+
+
+
+2. 在终端界面中创建对应的database(如果已经存在对应的database,则跳过这一步):
+
+
+```bash
+# 创建数据库sql语句,示例:
+$ create database testmysql;
+```
+
+3. 进入目标数据库公网迁移界面,点击迁移按钮:
+
+
+
+4. 查看迁移配置信息:
+
+
+
+复制迁移配置信息,如下:
+```bash
+# 设置配置信息sql语句,示例:
+$ set global binlog_format=ROW;
+$ set binlog_row_image ='FULL';
+```
+
+5. 在源数据库中执行设置配置信息(MySQL、Postgres需要手动配置信息,Mongo不需要进行配置):
+```bash
+# 设置配置信息sql语句,示例:
+$ set global binlog_format=ROW;
+$ set binlog_row_image ='FULL';
+```
+
+
+6. 进入目标数据库,填写源数据库的迁移信息,要迁移哪些表,并填写要迁移到目标数据库中哪个database:
+
+
+
+如果需要持续从源数据库迁移数据,可开启高级配置中的持续迁移,如下:
+
+
+
+7. 查看迁移任务信息:
+
+
+
+8. 进入目标数据库,连接目标数据库,检查迁移数据是否完整:
+
+
+
+### 文件迁移
+
+1. 进入目标数据库,连接目标数据库:
+
+
+
+2. 在终端界面中创建对应的database(如果已经存在对应的database,则跳过这一步):
+```bash
+# 创建数据库sql语句,示例:
+$ create database testmysql;
+```
+
+
+3. 上传迁移文件、填写数据库名,开始迁移:
+
+
+
+4. 文件迁移中,等待迁移结果:
+
+
+
+
+
+5. 连接数据库,检查迁移数据是否完整:
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_1.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_1.png
new file mode 100644
index 000000000000..8c9e8c50867d
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_2.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_2.png
new file mode 100644
index 000000000000..02f239da516c
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_3.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_3.png
new file mode 100644
index 000000000000..e0d5dca8c2ef
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_4.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_4.png
new file mode 100644
index 000000000000..58c37d7c5d2a
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_5.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_5.png
new file mode 100644
index 000000000000..8c9e8c50867d
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_6.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_6.png
new file mode 100644
index 000000000000..48b20cf2b8ca
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_1.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_1.png
new file mode 100644
index 000000000000..52c6879b4d29
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_2.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_2.png
new file mode 100644
index 000000000000..76215ce1c9ab
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_3.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_3.png
new file mode 100644
index 000000000000..0a205d4a3626
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_4.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_4.png
new file mode 100644
index 000000000000..a1a4366d15ce
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_5.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_5.png
new file mode 100644
index 000000000000..0b4a3b398f73
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_6.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_6.png
new file mode 100644
index 000000000000..3125cbb7bf12
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_7.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_7.png
new file mode 100644
index 000000000000..e8bce07c9cde
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_1.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_1.png
new file mode 100644
index 000000000000..d5527ccf5116
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_10.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_10.png
new file mode 100644
index 000000000000..d5527ccf5116
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_10.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_11.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_11.png
new file mode 100644
index 000000000000..895255296854
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_11.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_12.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_12.png
new file mode 100644
index 000000000000..12561fb4eba5
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_12.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_13.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_13.png
new file mode 100644
index 000000000000..074b4882b952
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_13.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_14.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_14.png
new file mode 100644
index 000000000000..409e77e907a6
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_14.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_15.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_15.png
new file mode 100644
index 000000000000..54589726b673
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_15.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_2.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_2.png
new file mode 100644
index 000000000000..895255296854
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_3.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_3.png
new file mode 100644
index 000000000000..2bc1d55549e8
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_4.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_4.png
new file mode 100644
index 000000000000..9a49e93f1b62
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_5.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_5.png
new file mode 100644
index 000000000000..e9b0d128758b
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_6.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_6.png
new file mode 100644
index 000000000000..271c5bafd8b5
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_7.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_7.png
new file mode 100644
index 000000000000..778411659966
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_8.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_8.png
new file mode 100644
index 000000000000..3202d5031fb1
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_8.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_9.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_9.png
new file mode 100644
index 000000000000..54589726b673
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_9.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_1.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_1.png
new file mode 100644
index 000000000000..2b87eb60a94f
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_10.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_10.png
new file mode 100644
index 000000000000..6e10d9cedd0e
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_10.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_11.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_11.png
new file mode 100644
index 000000000000..633705ae905c
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_11.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_12.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_12.png
new file mode 100644
index 000000000000..f08ae8c081df
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_12.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_2.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_2.png
new file mode 100644
index 000000000000..016fe962470c
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_3.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_3.png
new file mode 100644
index 000000000000..f687bb3cda4f
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_4.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_4.png
new file mode 100644
index 000000000000..175419decad9
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_5.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_5.png
new file mode 100644
index 000000000000..da9edd9ca8cc
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_6.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_6.png
new file mode 100644
index 000000000000..e61a8d5befba
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_7.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_7.png
new file mode 100644
index 000000000000..63aeada4ce95
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_8.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_8.png
new file mode 100644
index 000000000000..8e2d489183a4
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_8.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_9.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_9.png
new file mode 100644
index 000000000000..cbb8d4a36ce9
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_9.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/1.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/1.png
new file mode 100644
index 000000000000..3ff6dfa9b024
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/10.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/10.png
new file mode 100644
index 000000000000..e636144a8eeb
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/10.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/11.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/11.png
new file mode 100644
index 000000000000..12624610f67d
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/11.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/12.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/12.png
new file mode 100644
index 000000000000..4ae45dedc78c
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/12.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/13.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/13.png
new file mode 100644
index 000000000000..e3cc905cd04f
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/13.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/14.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/14.png
new file mode 100644
index 000000000000..66dff9b0eecd
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/14.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/15.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/15.png
new file mode 100644
index 000000000000..0022dce6edd9
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/15.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/16.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/16.png
new file mode 100644
index 000000000000..442bdb7e067a
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/16.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/17.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/17.png
new file mode 100644
index 000000000000..0e768c7adef3
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/17.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/2.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/2.png
new file mode 100644
index 000000000000..386fb07680d8
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/2.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/3.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/3.png
new file mode 100644
index 000000000000..3e17d0f68633
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/3.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/4.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/4.png
new file mode 100644
index 000000000000..17ffbdd8d582
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/4.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/5.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/5.png
new file mode 100644
index 000000000000..61c1098e18af
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/5.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/6.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/6.png
new file mode 100644
index 000000000000..4f8e59ec7250
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/6.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/7.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/7.png
new file mode 100644
index 000000000000..65a4e96dbcaa
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/7.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/8.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/8.png
new file mode 100644
index 000000000000..827eb00383d8
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/8.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/9.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/9.png
new file mode 100644
index 000000000000..6ba91e30a63b
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/9.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/objectstorage.md b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/objectstorage.md
new file mode 100644
index 000000000000..7409d90e89db
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/objectstorage.md
@@ -0,0 +1,188 @@
+---
+sidebar_position: 0
+---
+
+# 对象存储
+
+「对象存储」是 Sealos 内置的对象存储服务,主要用于存储和管理非结构化数据。
+
+目前「对象存储」具备以下功能:
+
+- 上传文件到「存储桶」
+- 从「存储桶」下载文件
+- 公开「存储桶」的访问权限
+- 使用 SDK 访问「存储桶」
+- 监控「存储桶」资源指标
+- 静态托管
+
+## 快速开始
+
+### 上传文件
+
+进入「对象存储」
+
+
+创建「存储桶」
+
+
+设置「存储桶」名字为 test,权限为 private
+
+
+「存储桶」创建成功
+
+
+上传文件
+
+
+上传文件成功
+
+
+### 公开「存储桶」的访问权限
+
+点击「编辑」
+
+
+设置「存储桶权限」为 publicRead,点击「应用」
+
+
+复制文件链接
+
+
+粘贴到浏览器地址栏访问文件
+
+
+### 查看访问密钥配置
+
+对象存储用户由唯一的 Access Key(用户名)和对应的 Secret Key(密码)组成。Internal 为对象存储的内部访问地址,External
+为对象存储的外部访问地址。
+
+
+### 使用 SDK 访问「存储桶」
+
+SDK 访问「存储桶」需要三个参数:AccessKey、SecretKey、Endpoint。参数都在访问密钥中,Internal 是内网地址 Endpoint,External 是外网地址
+Endpoint。如果需要使用 Region 参数,默认使用 us-east-1。
+
+#### Go Client SDK
+
+详细文档参考:https://min.io/docs/minio/linux/developers/go/API.html
+
+例子:使用 Go Client SDK 上传 style.css 文件到 sv3dd7u4-test 存储桶,将 Endpoint 设置为外网地址 External。如果服务部署在当前
+K8s 集群内,可以将 Endpoint 改为内网地址 Internal。
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+)
+import "github.com/minio/minio-go/v7"
+import "github.com/minio/minio-go/v7/pkg/credentials"
+
+func main() {
+ endpoint := "objectstorageapi.xxx.xxx.xxx"
+ accessKey := "xxxxxxxx"
+ secretKey := "xxxxxxxxxxxxxxxx"
+ // init minio client
+ minioClient, err := minio.New(endpoint, &minio.Options{
+ Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
+ })
+ if err != nil {
+ log.Fatalln(err)
+ }
+ // get local file
+ file, err := os.Open("./style.css")
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ defer file.Close()
+
+ fileStat, err := file.Stat()
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ // put object
+ uploadInfo, err := minioClient.PutObject(context.Background(), "sv3dd7u4-test", "style.css", file, fileStat.Size(), minio.PutObjectOptions{ContentType: "text/css"})
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Println("Successfully uploaded bytes: ", uploadInfo)
+}
+```
+
+文件上传成功
+
+
+#### Java Client SDK
+
+详细文档参考:https://min.io/docs/minio/linux/developers/java/API.html
+
+例子:使用 Java Client SDK 上传 style1.css 文件到 sv3dd7u4-test 存储桶,将 Endpoint 设置为外网地址 External。如果服务部署在当前
+K8s 集群内,可以将 Endpoint 改为内网地址 Internal。
+
+```xml
+
+
+ io.minio
+ minio
+ 8.5.9
+
+```
+
+```javascript
+package org.example;
+
+import io.minio.MinioClient;
+import io.minio.UploadObjectArgs;
+
+public class FileUploader {
+ public static void main(String[] args) throws Exception {
+
+ MinioClient minioClient =
+ MinioClient.builder()
+ .endpoint("https://objectstorageapi.xxx.xxx.xxx")
+ .credentials("xxxxxxxx", "xxxxxxxxxxxxxxxx")
+ .build();
+
+
+ minioClient.uploadObject(
+ UploadObjectArgs.builder()
+ .bucket("sv3dd7u4-test")
+ .object("style1.css")
+ .filename("src/main/java/org/example/style1.css")
+ .build());
+
+ System.out.println("Successfully uploaded bytes.");
+ }
+}
+```
+
+文件上传成功
+
+
+#### 其他语言 SDK 略
+
+详细文档参考:https://min.io/docs/minio/linux/developers/minio-drivers.html
+
+### 静态托管
+
+创建一个权限为 publicRead/publicReadwrite 的「存储桶」,点击「打开托管」
+
+
+点击地址跳转访问
+
+
+点击「自定义域名」
+
+
+跳转到「应用管理」的「变更」中,可以自定义域名
+
+
+
+
+
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/templates/images/templates.png b/docs/archived/4.0/i18n/zh-Hans/guides/templates/images/templates.png
new file mode 100644
index 000000000000..4e7734a645f3
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/templates/images/templates.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/templates/images/wechat-qr-code.jpg b/docs/archived/4.0/i18n/zh-Hans/guides/templates/images/wechat-qr-code.jpg
new file mode 100644
index 000000000000..c842edc94e1e
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/templates/images/wechat-qr-code.jpg differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/templates/submit-template.md b/docs/archived/4.0/i18n/zh-Hans/guides/templates/submit-template.md
new file mode 100644
index 000000000000..cbf9222823f9
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/templates/submit-template.md
@@ -0,0 +1,37 @@
+---
+sidebar_position: 2
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import wechat from './images/wechat-qr-code.jpg';
+
+# 提交模板
+
+:::tip
+
+提交模板是可以拿奖金💰的!奖励规则如下:
+
+| 类型 | 奖金 |
+| ------------------------------------------------------------ | ---- |
+| 提交模板到 Sealos 模板市场 | 50¥ |
+| 同时将模板的一键部署 PR 提交到模板应用的官方文档或者 README 中 | 150¥ |
+
+:::
+
+## 模板提交流程
+
+Sealos 模板市场的所有模板都是实时从 [Sealos 模板仓库](https://github.com/labring-actions/templates) 同步过来的。如果您想要为这个仓库贡献新的模板,可以通过提交 PR(Pull Request)的方式来实现。
+
+要创建一个新模板,您可以参考这里的 [template.yaml](https://github.com/labring-actions/templates/blob/main/template.yaml) 文件。系统已经内置了许多通用的环境变量和函数,这些都可以在编写模板时使用。这些内置的功能允许您使用类似于 `GitHub Actions` 的语法来编写模板,例如,您可以使用 `${{ SEALOS_NAMESPACE }}` 这样的环境变量来设置模板参数。关于这些内置环境变量的具体信息,可以参阅[模板说明文档](https://github.com/labring-actions/templates/blob/main/example_zh.md)。
+
+## 领取奖金
+
+首先填写并提交以下表单:
+
+
+
+然后扫码添加 Sealos 小助理微信领取奖金:
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/templates/templates.md b/docs/archived/4.0/i18n/zh-Hans/guides/templates/templates.md
new file mode 100644
index 000000000000..f5dff84a1cca
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/templates/templates.md
@@ -0,0 +1,17 @@
+---
+sidebar_position: 0
+---
+
+# 模板市场
+
+import Highlight from '@site/src/components/Highlight'
+
+
+
+Sealos 的[模板市场](https://template.cloud.sealos.io/)提供了一系列预制的模板,这些模板可用于快速创建和部署网站和各种应用程序。你可以在模板市场中找到各种类型的模板,比如博客、AI 应用、低代码应用、网盘、IM 应用、中间件等等。这些模板旨在简化开发过程,使开发者能够快速启动和部署项目,而无需从零开始构建整个网站,也无需关心应用之间的各种依赖关系。
+
+与 Vercel 的模板市场相比,Sealos 的模板市场在应用范围和功能上有所不同。Vercel 主要专注于前端项目的部署,它的模板主要是为了支持网站的界面和用户交互部分,但它不支持数据的持久化存储,所有的数据都是临时的,一旦应用重启,数据就会丢失。
+
+Sealos 应用模板不仅支持前端项目,还支持后端和其他各类应用的部署。更重要的是,Sealos 支持挂载持久化存储,这对于需要存储大量数据或者保持数据持久化的应用来说至关重要。例如,对于电商网站、大型社交媒体应用或企业级应用来说,数据的持久化是必不可少的功能。
+
+
\ No newline at end of file
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/database-terminal-1.png b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/database-terminal-1.png
new file mode 100644
index 000000000000..f1fcc577c7f2
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/database-terminal-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/database-terminal.png b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/database-terminal.png
new file mode 100644
index 000000000000..01dba426f6b0
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/database-terminal.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/nginx-terminal-1.png b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/nginx-terminal-1.png
new file mode 100644
index 000000000000..ecb9443c9621
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/nginx-terminal-1.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/nginx-terminal.png b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/nginx-terminal.png
new file mode 100644
index 000000000000..c8fa6b4ae9f9
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/nginx-terminal.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/terminal-pod.png b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/terminal-pod.png
new file mode 100644
index 000000000000..9e649dac3a11
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/terminal-pod.png differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/terminal.webp b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/terminal.webp
new file mode 100644
index 000000000000..d1daf72539d7
Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/terminal.webp differ
diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/terminal.md b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/terminal.md
new file mode 100644
index 000000000000..7a622ffe046c
--- /dev/null
+++ b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/terminal.md
@@ -0,0 +1,86 @@
+---
+sidebar_position: 4
+---
+
+# 终端
+
+终端是 [Sealos](https://cloud.sealos.io) 云操作系统内置的应用,它是一款非常强大的应用,与单机操作系统的终端一样,允许用户与操作系统进行直接交互,用户可以通过键入命令来执行各种操作。
+
+
+
+终端的价值在于其高效,灵活和强大的特性。对于许多任务来说,使用终端比使用图形用户界面(GUI)更快更简洁。对于开发者而言,终端是必不可少的工具。使用终端,可以让用户深入理解和控制 Sealos 云操作系统。
+
+**每一个用户的终端实际上都运行在 Kubernetes 的一个 Pod 中**,可以在终端中通过以下命令查看:
+
+
+
+## 进入应用容器的终端
+
+也可以直接通过终端 App 进入每个应用所在容器的终端。假设你在应用管理中部署了一个应用 Nginx,可以直接进入 Nginx 应用的详情页面,依次点击详情右侧的三个点,再点击「终端」,便进入了 Nginx 应用的终端。
+
+
+
+
+
+## 终端一键直连数据库
+
+还可以在终端中一键直连[数据库 App](../dbprovider/dbprovider.md) 中创建的数据库。
+
+进入数据库详情页面,点击左侧的「一键连接」:
+
+
+
+就会直接跳转到终端 App 并连接数据库:
+
+
+
+## 终端直接操作 Kubernetes 资源
+
+:::danger
+
+以下操作需要一定的 Kubernetes 和容器相关知识基础,否则不要轻易操作。
+
+:::
+
+资深 Kubernetes 用户可以直接使用终端来操作 Kubernetes 资源,对于资深用户而言,终端的优势有很多:
+
++ 可以批量操作和自动化
++ 高度灵活性
++ 可以进行深入的故障排查
+
+以下是一些常见的终端中操作 Kubernetes 资源的示例:
+
+### 查看用户的 Pod 列表
+
+```bash
+kubectl get pods
+```
+
+### 创建和管理资源对象
+
+```bash
+# 使用YAML或JSON文件创建资源
+$ kubectl create -f
+
+# 使用YAML或JSON文件创建或更新资源
+$ kubectl apply -f
+
+# 删除指定类型和名称的资源
+$ kubectl delete
+
+# 查看指定 Deployment 的滚动更新状态
+$ kubectl rollout status deployment/
+```
+
+### 故障排查
+
+```bash
+# 获取指定资源的详细信息,包括事件和状态
+$ kubectl describe
+
+# 获取指定 Pod 中指定容器的日志
+$ kubectl logs -c | |