rdev/internal/adapter/deployer/resources.go
jordan 812b8341be refactor: Split large files to comply with 500-line limit
- cmd/rdev-api/main.go: Extract OpenAPI spec to openapi.go (1073→386 lines)
- internal/adapter/deployer/deployer.go: Extract K8s resources to resources.go (502→264 lines)
- internal/handlers/infrastructure.go: Extract deploy handlers to infrastructure_deploy.go (592→342 lines)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 23:02:31 -07:00

261 lines
7.4 KiB
Go

package deployer
import (
"context"
"strings"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/orchard9/rdev/internal/domain"
)
// ensureNamespace creates the deployment namespace if it doesn't exist.
func (d *Deployer) ensureNamespace(ctx context.Context) error {
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: d.config.Namespace,
},
}
_, err := d.client.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
if err != nil && !errors.IsAlreadyExists(err) {
return err
}
return nil
}
// createOrUpdateSecret manages the secret for environment variables.
func (d *Deployer) createOrUpdateSecret(ctx context.Context, spec domain.DeploySpec) error {
secretName := spec.ProjectName + "-env"
ns := d.config.Namespace
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: ns,
Labels: map[string]string{
"app": spec.ProjectName,
"project": spec.ProjectName,
},
},
StringData: spec.Secrets,
}
_, err := d.client.CoreV1().Secrets(ns).Get(ctx, secretName, metav1.GetOptions{})
if errors.IsNotFound(err) {
_, err = d.client.CoreV1().Secrets(ns).Create(ctx, secret, metav1.CreateOptions{})
} else if err == nil {
_, err = d.client.CoreV1().Secrets(ns).Update(ctx, secret, metav1.UpdateOptions{})
}
return err
}
// createOrUpdateDeployment manages the Kubernetes Deployment resource.
func (d *Deployer) createOrUpdateDeployment(ctx context.Context, spec domain.DeploySpec) error {
ns := d.config.Namespace
replicas := int32(spec.Replicas)
// Build env vars
var envVars []corev1.EnvVar
for k, v := range spec.EnvVars {
envVars = append(envVars, corev1.EnvVar{Name: k, Value: v})
}
// Add secret env vars
var envFrom []corev1.EnvFromSource
if len(spec.Secrets) > 0 {
envFrom = append(envFrom, corev1.EnvFromSource{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: spec.ProjectName + "-env",
},
},
})
}
deployment := d.buildDeployment(spec, ns, replicas, envVars, envFrom)
_, err := d.client.AppsV1().Deployments(ns).Get(ctx, spec.ProjectName, metav1.GetOptions{})
if errors.IsNotFound(err) {
_, err = d.client.AppsV1().Deployments(ns).Create(ctx, deployment, metav1.CreateOptions{})
} else if err == nil {
_, err = d.client.AppsV1().Deployments(ns).Update(ctx, deployment, metav1.UpdateOptions{})
}
return err
}
func (d *Deployer) buildDeployment(spec domain.DeploySpec, ns string, replicas int32, envVars []corev1.EnvVar, envFrom []corev1.EnvFromSource) *appsv1.Deployment {
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: spec.ProjectName,
Namespace: ns,
Labels: map[string]string{
"app": spec.ProjectName,
"project": spec.ProjectName,
},
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": spec.ProjectName,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": spec.ProjectName,
"project": spec.ProjectName,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: spec.ProjectName,
Image: spec.Image,
Env: envVars,
EnvFrom: envFrom,
Ports: []corev1.ContainerPort{
{
ContainerPort: int32(spec.Port),
Protocol: corev1.ProtocolTCP,
},
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resourceQuantity("100m"),
corev1.ResourceMemory: resourceQuantity("128Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resourceQuantity("1000m"),
corev1.ResourceMemory: resourceQuantity("512Mi"),
},
},
},
},
},
},
},
}
}
// createOrUpdateService manages the Kubernetes Service resource.
func (d *Deployer) createOrUpdateService(ctx context.Context, spec domain.DeploySpec) error {
ns := d.config.Namespace
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: spec.ProjectName,
Namespace: ns,
Labels: map[string]string{
"app": spec.ProjectName,
"project": spec.ProjectName,
},
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{
"app": spec.ProjectName,
},
Ports: []corev1.ServicePort{
{
Port: int32(spec.Port),
TargetPort: intstr.FromInt(spec.Port),
Protocol: corev1.ProtocolTCP,
},
},
},
}
_, err := d.client.CoreV1().Services(ns).Get(ctx, spec.ProjectName, metav1.GetOptions{})
if errors.IsNotFound(err) {
_, err = d.client.CoreV1().Services(ns).Create(ctx, service, metav1.CreateOptions{})
} else if err == nil {
_, err = d.client.CoreV1().Services(ns).Update(ctx, service, metav1.UpdateOptions{})
}
return err
}
// createOrUpdateIngress manages the Kubernetes Ingress resource.
func (d *Deployer) createOrUpdateIngress(ctx context.Context, spec domain.DeploySpec) error {
ns := d.config.Namespace
pathType := networkingv1.PathTypePrefix
ingressClass := d.config.IngressClass
// Build TLS secret name from domain
tlsSecretName := strings.ReplaceAll(spec.Domain, ".", "-") + "-tls"
annotations := map[string]string{}
if d.config.TLSIssuer != "" {
annotations["cert-manager.io/issuer"] = d.config.TLSIssuer
}
ingress := d.buildIngress(spec, ns, pathType, ingressClass, tlsSecretName, annotations)
_, err := d.client.NetworkingV1().Ingresses(ns).Get(ctx, spec.ProjectName, metav1.GetOptions{})
if errors.IsNotFound(err) {
_, err = d.client.NetworkingV1().Ingresses(ns).Create(ctx, ingress, metav1.CreateOptions{})
} else if err == nil {
_, err = d.client.NetworkingV1().Ingresses(ns).Update(ctx, ingress, metav1.UpdateOptions{})
}
return err
}
func (d *Deployer) buildIngress(spec domain.DeploySpec, ns string, pathType networkingv1.PathType, ingressClass, tlsSecretName string, annotations map[string]string) *networkingv1.Ingress {
return &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: spec.ProjectName,
Namespace: ns,
Labels: map[string]string{
"app": spec.ProjectName,
"project": spec.ProjectName,
},
Annotations: annotations,
},
Spec: networkingv1.IngressSpec{
IngressClassName: &ingressClass,
TLS: []networkingv1.IngressTLS{
{
Hosts: []string{spec.Domain},
SecretName: tlsSecretName,
},
},
Rules: []networkingv1.IngressRule{
{
Host: spec.Domain,
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{
Path: "/",
PathType: &pathType,
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: spec.ProjectName,
Port: networkingv1.ServiceBackendPort{
Number: int32(spec.Port),
},
},
},
},
},
},
},
},
},
},
}
}
// resourceQuantity parses a resource quantity string.
// Returns the parsed quantity or a zero quantity on error.
func resourceQuantity(s string) resource.Quantity {
q, _ := resource.ParseQuantity(s)
return q
}