package appstore import ( "fmt" "strings" applicationv1 "code.gitea.io/gitea/modules/k8s/api/application/v1" application "code.gitea.io/gitea/modules/k8s/application" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // BuildK8sCreateOptions converts an AppStore App into CreateApplicationOptions for the Application CRD. // Returns error if the app doesn't contain a valid Kubernetes deployment definition. func BuildK8sCreateOptions(app *App) (*application.CreateApplicationOptions, error) { if app == nil { return nil, fmt.Errorf("nil app") } // Require actual Deploy.Type to be kubernetes, parameters are in Deploy.Kubernetes if strings.ToLower(strings.TrimSpace(app.Deploy.Type)) != "kubernetes" { return nil, fmt.Errorf("deploy.type must be 'kubernetes'") } if app.Deploy.Kubernetes == nil { return nil, fmt.Errorf("deploy.kubernetes is required for kubernetes deployment") } k := app.Deploy.Kubernetes // Name & Namespace name := sanitizeName(app.ID) namespace := k.Namespace if namespace == "" { namespace = "default" } // Image with optional tag image := k.Image if k.Tag != "" { image = fmt.Sprintf("%s:%s", k.Image, k.Tag) } // Template Ports var tplPorts []applicationv1.Port for _, p := range k.Ports { portName := p.Name if portName == "" { portName = fmt.Sprintf("port-%d", p.ContainerPort) } proto := strings.ToUpper(p.Protocol) if proto == "" { proto = "TCP" } tplPorts = append(tplPorts, applicationv1.Port{ Name: portName, Port: int32(p.ContainerPort), Protocol: proto, }) } // Resources res := applicationv1.ResourceRequirements{} if k.Resources != nil { res.CPU = k.Resources.CPU res.Memory = k.Resources.Memory } // Service config (optional) var svc *applicationv1.ServiceConfig if k.Service != nil { svc = &applicationv1.ServiceConfig{ Enabled: true, Type: k.Service.Type, // ClusterIP/NodePort/LoadBalancer/ExternalName } if len(k.Service.Ports) > 0 { for _, sp := range k.Service.Ports { portName := sp.Name if portName == "" { portName = fmt.Sprintf("svc-%d", sp.ContainerPort) } proto := strings.ToUpper(sp.Protocol) if proto == "" { proto = "TCP" } svc.Ports = append(svc.Ports, applicationv1.ServicePort{ Name: portName, Port: int32(sp.ContainerPort), TargetPort: portName, // name-based targetPort for stability Protocol: proto, }) } } else if len(tplPorts) > 0 { // Fallback: expose template ports for _, p := range tplPorts { svc.Ports = append(svc.Ports, applicationv1.ServicePort{ Name: p.Name, Port: p.Port, TargetPort: p.Name, Protocol: p.Protocol, }) } } } // Replicas var replicasPtr *int32 if k.Replicas > 0 { r := int32(k.Replicas) replicasPtr = &r } opts := &application.CreateApplicationOptions{ Name: name, Namespace: namespace, Component: app.Category, Template: applicationv1.ApplicationTemplate{ Image: image, Type: "stateless", Ports: tplPorts, }, Replicas: replicasPtr, Environment: k.Environment, Resources: &res, Expose: svc != nil, // deprecated in CRD but kept for compatibility Service: svc, } return opts, nil } func sanitizeName(s string) string { s = strings.ToLower(strings.TrimSpace(s)) s = strings.ReplaceAll(s, "_", "-") s = strings.ReplaceAll(s, " ", "-") return s } // BuildK8sGetOptions builds GetApplicationOptions from an App for querying Application CRD func BuildK8sGetOptions(app *App, namespaceOverride string, wait bool) (*application.GetApplicationOptions, error) { if app == nil { return nil, fmt.Errorf("nil app") } if strings.ToLower(strings.TrimSpace(app.Deploy.Type)) != "kubernetes" { return nil, fmt.Errorf("deploy.type must be 'kubernetes'") } if app.Deploy.Kubernetes == nil { return nil, fmt.Errorf("deploy.kubernetes is required") } name := sanitizeName(app.ID) ns := firstNonEmpty(namespaceOverride, app.Deploy.Kubernetes.Namespace, "default") return &application.GetApplicationOptions{ Name: name, Namespace: ns, Wait: wait, GetOptions: metav1.GetOptions{}, }, nil } // BuildK8sDeleteOptions builds DeleteApplicationOptions to remove an Application CRD func BuildK8sDeleteOptions(app *App, namespaceOverride string) (*application.DeleteApplicationOptions, error) { if app == nil { return nil, fmt.Errorf("nil app") } if strings.ToLower(strings.TrimSpace(app.Deploy.Type)) != "kubernetes" { return nil, fmt.Errorf("deploy.type must be 'kubernetes'") } if app.Deploy.Kubernetes == nil { return nil, fmt.Errorf("deploy.kubernetes is required") } name := sanitizeName(app.ID) ns := firstNonEmpty(namespaceOverride, app.Deploy.Kubernetes.Namespace, "default") return &application.DeleteApplicationOptions{ Name: name, Namespace: ns, DeleteOptions: metav1.DeleteOptions{}, }, nil } // BuildK8sListOptions builds ListApplicationsOptions to list Applications in a namespace func BuildK8sListOptions(namespace string, listOpts metav1.ListOptions) (*application.ListApplicationsOptions, error) { ns := namespace if strings.TrimSpace(ns) == "" { ns = "default" } return &application.ListApplicationsOptions{ Namespace: ns, ListOptions: listOpts, }, nil } // BuildK8sUpdateOptions builds UpdateApplicationOptions from an App and optional existing Application object func BuildK8sUpdateOptions(app *App, namespaceOverride string, existing *applicationv1.Application) (*application.UpdateApplicationOptions, error) { if app == nil { return nil, fmt.Errorf("nil app") } if strings.ToLower(strings.TrimSpace(app.Deploy.Type)) != "kubernetes" { return nil, fmt.Errorf("deploy.type must be 'kubernetes'") } if app.Deploy.Kubernetes == nil { return nil, fmt.Errorf("deploy.kubernetes is required") } name := sanitizeName(app.ID) ns := firstNonEmpty(namespaceOverride, app.Deploy.Kubernetes.Namespace, "default") // Start from existing object when provided to preserve metadata/status var obj applicationv1.Application if existing != nil { obj = *existing.DeepCopy() } else { obj.TypeMeta = metav1.TypeMeta{Kind: "Application", APIVersion: "application.devstar.cn/v1"} obj.ObjectMeta = metav1.ObjectMeta{Name: name, Namespace: ns} } // Map spec similar to BuildK8sCreateOptions k := app.Deploy.Kubernetes image := k.Image if k.Tag != "" { image = fmt.Sprintf("%s:%s", k.Image, k.Tag) } var tplPorts []applicationv1.Port for _, p := range k.Ports { portName := p.Name if portName == "" { portName = fmt.Sprintf("port-%d", p.ContainerPort) } proto := strings.ToUpper(p.Protocol) if proto == "" { proto = "TCP" } tplPorts = append(tplPorts, applicationv1.Port{Name: portName, Port: int32(p.ContainerPort), Protocol: proto}) } res := applicationv1.ResourceRequirements{} if k.Resources != nil { res.CPU = k.Resources.CPU res.Memory = k.Resources.Memory } var svc *applicationv1.ServiceConfig if k.Service != nil { svc = &applicationv1.ServiceConfig{Enabled: true, Type: k.Service.Type} if len(k.Service.Ports) > 0 { for _, sp := range k.Service.Ports { portName := sp.Name if portName == "" { portName = fmt.Sprintf("svc-%d", sp.ContainerPort) } proto := strings.ToUpper(sp.Protocol) if proto == "" { proto = "TCP" } svc.Ports = append(svc.Ports, applicationv1.ServicePort{ Name: portName, Port: int32(sp.ContainerPort), TargetPort: portName, Protocol: proto, }) } } else if len(tplPorts) > 0 { for _, p := range tplPorts { svc.Ports = append(svc.Ports, applicationv1.ServicePort{ Name: p.Name, Port: p.Port, TargetPort: p.Name, Protocol: p.Protocol, }) } } } var replicasPtr *int32 if k.Replicas > 0 { r := int32(k.Replicas) replicasPtr = &r } obj.Spec = applicationv1.ApplicationSpec{ Template: applicationv1.ApplicationTemplate{ Image: image, Type: "stateless", Ports: tplPorts, }, Replicas: replicasPtr, Environment: k.Environment, Resources: res, Expose: svc != nil, Service: svc, } return &application.UpdateApplicationOptions{ Name: name, Namespace: ns, Application: &obj, UpdateOptions: metav1.UpdateOptions{}, }, nil } func firstNonEmpty(vals ...string) string { for _, v := range vals { if strings.TrimSpace(v) != "" { return v } } return "" }