package application import ( "context" "encoding/json" "fmt" applicationv1 "code.gitea.io/gitea/modules/k8s/api/application/v1" "code.gitea.io/gitea/modules/log" application_errors "code.gitea.io/gitea/modules/k8s/application/errors" apimachinery_apis_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" apimachinery_apis_v1_unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apimachinery_runtime_utils "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" apimachinery_watch "k8s.io/apimachinery/pkg/watch" dynamic_client "k8s.io/client-go/dynamic" ) // Application 资源定义 var applicationGroupVersionResource = schema.GroupVersionResource{ Group: "application.devstar.cn", Version: "v1", Resource: "applications", } // IsApplicationStatusReady 判断 Application 是否就绪 func IsApplicationStatusReady(applicationStatus *applicationv1.ApplicationStatus) bool { if applicationStatus == nil { return false } // 如果不是 Running 状态,则未就绪 if applicationStatus.Phase != "Running" { return false } // 零副本应用被认为是就绪的(暂停状态) if applicationStatus.Replicas == 0 && applicationStatus.ReadyReplicas == 0 { return true } // 有副本的应用需要至少有一个就绪副本 return applicationStatus.ReadyReplicas > 0 } // GetApplication 获取 Application func GetApplication(ctx context.Context, client dynamic_client.Interface, opts *GetApplicationOptions) (*applicationv1.Application, error) { // 参数检查 if ctx == nil || opts == nil || len(opts.Namespace) == 0 || len(opts.Name) == 0 { return nil, application_errors.ErrIllegalApplicationParameters{ FieldList: []string{"ctx", "opts", "opts.Name", "opts.Namespace"}, Message: "cannot be nil", } } // 获取 Application 资源 applicationUnstructured, err := client.Resource(applicationGroupVersionResource). Namespace(opts.Namespace).Get(ctx, opts.Name, opts.GetOptions) if err != nil { return nil, application_errors.ErrOperateApplication{ Action: "Get Application through k8s API Server", Message: err.Error(), } } // 转换为 Application 对象 application := &applicationv1.Application{} err = apimachinery_runtime_utils.DefaultUnstructuredConverter. FromUnstructured(applicationUnstructured.Object, application) if err != nil { return nil, application_errors.ErrOperateApplication{ Action: "Convert k8s API Server unstructured response into Application", Message: err.Error(), } } // 检查是否需要等待就绪 if !IsApplicationStatusReady(&application.Status) && opts.Wait { applicationStatusVO, err := waitUntilApplicationReadyWithTimeout(ctx, client, opts) if err != nil { return nil, application_errors.ErrOperateApplication{ Action: "wait for k8s Application to be ready", Message: err.Error(), } } application.Status = *applicationStatusVO } return application, nil } // CreateApplication 创建 Application func CreateApplication(ctx context.Context, client dynamic_client.Interface, opts *CreateApplicationOptions) (*applicationv1.Application, error) { log.Info("Creating Application with options: name=%s, namespace=%s, image=%s", opts.Name, opts.Namespace, opts.Template.Image) // 处理默认的 Resources var resources applicationv1.ResourceRequirements if opts.Resources != nil { resources = *opts.Resources } // 如果 Resources 为 nil,使用空的 ResourceRequirements(所有字段都是零值) // 创建 Application 资源定义 application := &applicationv1.Application{ TypeMeta: apimachinery_apis_v1.TypeMeta{ Kind: "Application", APIVersion: "application.devstar.cn/v1", }, ObjectMeta: apimachinery_apis_v1.ObjectMeta{ Name: opts.Name, Namespace: opts.Namespace, Labels: map[string]string{ "app.kubernetes.io/name": opts.Name, "app.kubernetes.io/component": opts.Component, "app.kubernetes.io/managed-by": "devstar", }, }, Spec: applicationv1.ApplicationSpec{ Template: opts.Template, Replicas: opts.Replicas, Environment: opts.Environment, Resources: resources, Expose: opts.Expose, Service: opts.Service, NetworkPolicy: opts.NetworkPolicy, TrafficPolicy: opts.TrafficPolicy, }, } // 转换为 JSON jsonData, err := json.Marshal(application) if err != nil { log.Error("Failed to marshal Application to JSON: %v", err) return nil, application_errors.ErrOperateApplication{ Action: "Marshal JSON", Message: err.Error(), } } log.Debug("Generated JSON for Application:\n%s", string(jsonData)) // 转换为 Unstructured 对象 unstructuredObj := &apimachinery_apis_v1_unstructured.Unstructured{} err = unstructuredObj.UnmarshalJSON(jsonData) if err != nil { log.Error("Failed to unmarshal JSON to Unstructured: %v", err) return nil, application_errors.ErrOperateApplication{ Action: "Unmarshal JSON to Unstructured", Message: err.Error(), } } // 创建资源 log.Info("Creating Application resource in namespace %s", opts.Namespace) result, err := client.Resource(applicationGroupVersionResource). Namespace(opts.Namespace).Create(ctx, unstructuredObj, opts.CreateOptions) if err != nil { log.Error("Failed to create Application: %v", err) return nil, application_errors.ErrOperateApplication{ Action: "create Application via Dynamic Client", Message: err.Error(), } } log.Info("Application resource created successfully") // 转换结果 resultJSON, err := result.MarshalJSON() if err != nil { return nil, application_errors.ErrOperateApplication{ Action: "Marshal result JSON", Message: err.Error(), } } createdApplication := &applicationv1.Application{} if err := json.Unmarshal(resultJSON, createdApplication); err != nil { return nil, application_errors.ErrOperateApplication{ Action: "Unmarshal result JSON", Message: err.Error(), } } return createdApplication, nil } // UpdateApplication 更新 Application func UpdateApplication(ctx context.Context, client dynamic_client.Interface, opts *UpdateApplicationOptions) (*applicationv1.Application, error) { log.Info("Updating Application: name=%s, namespace=%s", opts.Name, opts.Namespace) // 转换为 JSON jsonData, err := json.Marshal(opts.Application) if err != nil { return nil, application_errors.ErrOperateApplication{ Action: "Marshal Application to JSON", Message: err.Error(), } } // 转换为 Unstructured unstructuredObj := &apimachinery_apis_v1_unstructured.Unstructured{} err = unstructuredObj.UnmarshalJSON(jsonData) if err != nil { return nil, application_errors.ErrOperateApplication{ Action: "Unmarshal JSON to Unstructured", Message: err.Error(), } } // 更新资源 result, err := client.Resource(applicationGroupVersionResource). Namespace(opts.Namespace).Update(ctx, unstructuredObj, opts.UpdateOptions) if err != nil { return nil, application_errors.ErrOperateApplication{ Action: "update Application via Dynamic Client", Message: err.Error(), } } // 转换结果 resultJSON, err := result.MarshalJSON() if err != nil { return nil, application_errors.ErrOperateApplication{ Action: "Marshal result JSON", Message: err.Error(), } } updatedApplication := &applicationv1.Application{} if err := json.Unmarshal(resultJSON, updatedApplication); err != nil { return nil, application_errors.ErrOperateApplication{ Action: "Unmarshal result JSON", Message: err.Error(), } } return updatedApplication, nil } // DeleteApplication 删除 Application func DeleteApplication(ctx context.Context, client dynamic_client.Interface, opts *DeleteApplicationOptions) error { if ctx == nil || opts == nil || len(opts.Namespace) == 0 || len(opts.Name) == 0 { return application_errors.ErrIllegalApplicationParameters{ FieldList: []string{"ctx", "opts", "opts.Name", "opts.Namespace"}, Message: "cannot be nil", } } err := client.Resource(applicationGroupVersionResource). Namespace(opts.Namespace).Delete(ctx, opts.Name, opts.DeleteOptions) if err != nil { log.Warn("Failed to delete Application '%s' in namespace '%s': %s", opts.Name, opts.Namespace, err.Error()) return application_errors.ErrOperateApplication{ Action: fmt.Sprintf("delete application '%s' in namespace '%s'", opts.Name, opts.Namespace), Message: err.Error(), } } return nil } // ListApplications 列出 Applications func ListApplications(ctx context.Context, client dynamic_client.Interface, opts *ListApplicationsOptions) (*applicationv1.ApplicationList, error) { if ctx == nil || opts == nil || len(opts.Namespace) == 0 { return nil, application_errors.ErrIllegalApplicationParameters{ FieldList: []string{"ctx", "namespace"}, Message: "cannot be empty", } } list, err := client.Resource(applicationGroupVersionResource). Namespace(opts.Namespace).List(ctx, opts.ListOptions) if err != nil { return nil, application_errors.ErrOperateApplication{ Action: fmt.Sprintf("List Application in namespace '%s'", opts.Namespace), Message: err.Error(), } } // 转换为 ApplicationList jsonData, err := list.MarshalJSON() if err != nil { return nil, application_errors.ErrOperateApplication{ Action: "verify JSON data of Application List", Message: err.Error(), } } applicationList := &applicationv1.ApplicationList{} if err := json.Unmarshal(jsonData, applicationList); err != nil { return nil, application_errors.ErrOperateApplication{ Action: "deserialize Application List data", Message: err.Error(), } } return applicationList, nil } // 等待 Application 就绪的辅助函数 func waitUntilApplicationReadyWithTimeout(ctx context.Context, client dynamic_client.Interface, opts *GetApplicationOptions) (*applicationv1.ApplicationStatus, error) { // 监听 Application 状态变化,等待就绪 watcherTimeoutSeconds := int64(300) // 5分钟超时 watcher, err := client.Resource(applicationGroupVersionResource). Namespace(opts.Namespace).Watch(ctx, apimachinery_apis_v1.ListOptions{ FieldSelector: fmt.Sprintf("metadata.name=%s", opts.Name), Watch: true, TimeoutSeconds: &watcherTimeoutSeconds, }) if err != nil { return nil, application_errors.ErrOperateApplication{ Action: "register watcher of Application Readiness", Message: err.Error(), } } defer watcher.Stop() for event := range watcher.ResultChan() { switch event.Type { case apimachinery_watch.Added, apimachinery_watch.Modified: if applicationUnstructured, ok := event.Object.(*apimachinery_apis_v1_unstructured.Unstructured); ok { application := &applicationv1.Application{} err = apimachinery_runtime_utils.DefaultUnstructuredConverter. FromUnstructured(applicationUnstructured.Object, application) if err == nil && IsApplicationStatusReady(&application.Status) { return &application.Status, nil } } case apimachinery_watch.Error: apimachineryApiMetav1Status, ok := event.Object.(*apimachinery_apis_v1.Status) if ok { return nil, application_errors.ErrOperateApplication{ Action: fmt.Sprintf("wait for Application '%s' in namespace '%s' to be ready", opts.Name, opts.Namespace), Message: fmt.Sprintf("Watcher error: %v", apimachineryApiMetav1Status.Message), } } case apimachinery_watch.Deleted: return nil, application_errors.ErrOperateApplication{ Action: fmt.Sprintf("wait for Application '%s' in namespace '%s'", opts.Name, opts.Namespace), Message: fmt.Sprintf("Application '%s' has been deleted", opts.Name), } } } return nil, application_errors.ErrOperateApplication{ Action: "wait for Application to be ready", Message: "timeout waiting for Application to be ready", } }