307 lines
8.3 KiB
Go
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 ""
|
|
}
|