Files
devstar-create-from-template/modules/k8s/application/application.go
2025-08-25 15:46:12 +08:00

353 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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",
}
}