Files
devstar-create-from-template/services/appstore/k8s_application_mapper.go
2025-08-25 15:46:12 +08:00

307 lines
8.3 KiB
Go

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 ""
}