Files
kandev/apps/backend/internal/agent/kubernetes/template.go
T

304 lines
9.9 KiB
Go

package kubernetes
import (
"fmt"
"io"
pathpkg "path"
"strings"
yamlv3 "gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
sigsyaml "sigs.k8s.io/yaml"
)
const (
MaxPodTemplateBytes = 256 * 1024
DefaultAgentctlPort = int32(8765)
RuntimeVolumeName = "kandev-runtime"
AuthVolumeName = "kandev-auth"
WorkspaceVolumeName = "kandev-workspace"
RuntimeMountPath = "/opt/kandev"
AuthMountPath = "/run/kandev"
WorkspaceMountPath = "/workspace"
AgentctlContainerPort = "kandev-agentctl"
podTemplateFieldPrefix = "config.pod_template_yaml"
)
var reservedLabelKeys = map[string]bool{
"app.kubernetes.io/component": true,
"app.kubernetes.io/instance": true,
"app.kubernetes.io/managed-by": true,
"app.kubernetes.io/name": true,
}
// ParsePodTemplate decodes exactly one strict core/v1 PodTemplate document.
func ParsePodTemplate(raw string) (*corev1.PodTemplate, error) {
if len(raw) > MaxPodTemplateBytes {
return nil, fieldError(podTemplateFieldPrefix, "must not exceed 256 KiB")
}
if strings.TrimSpace(raw) == "" {
return nil, fieldError(podTemplateFieldPrefix, "is required")
}
if err := requireSingleYAMLDocument(raw); err != nil {
return nil, err
}
var template corev1.PodTemplate
if err := sigsyaml.UnmarshalStrict([]byte(raw), &template); err != nil {
return nil, fieldError(podTemplateFieldPrefix, "must be strict PodTemplate YAML")
}
if template.APIVersion != "v1" || template.Kind != "PodTemplate" {
return nil, fieldError(podTemplateFieldPrefix, "must have apiVersion v1 and kind PodTemplate")
}
return &template, nil
}
// ValidatePodTemplate rejects fields owned by Kandev before any API write.
func ValidatePodTemplate(template *corev1.PodTemplate, mainContainer string) error {
if template == nil {
return fieldError(podTemplateFieldPrefix, "is required")
}
if err := validateTemplateMetadata(template); err != nil {
return err
}
if err := validatePodSpecFields(&template.Template.Spec); err != nil {
return err
}
return validateTemplateContainers(&template.Template.Spec, mainContainer)
}
func requireSingleYAMLDocument(raw string) error {
decoder := yamlv3.NewDecoder(strings.NewReader(raw))
var document yamlv3.Node
if err := decoder.Decode(&document); err != nil {
return fieldError(podTemplateFieldPrefix, "must be valid YAML")
}
var extra yamlv3.Node
err := decoder.Decode(&extra)
if err == nil {
return fieldError(podTemplateFieldPrefix, "must contain exactly one YAML document")
}
if err != io.EOF {
return fieldError(podTemplateFieldPrefix, "must be valid YAML")
}
return nil
}
func validateTemplateMetadata(template *corev1.PodTemplate) error {
meta := &template.Template.ObjectMeta
checks := []struct {
set bool
path string
}{
{meta.Name != "", ".template.metadata.name"},
{meta.GenerateName != "", ".template.metadata.generateName"},
{meta.Namespace != "", ".template.metadata.namespace"},
{meta.UID != "", ".template.metadata.uid"},
{meta.ResourceVersion != "", ".template.metadata.resourceVersion"},
{meta.Generation != 0, ".template.metadata.generation"},
{!meta.CreationTimestamp.IsZero(), ".template.metadata.creationTimestamp"},
{meta.DeletionTimestamp != nil, ".template.metadata.deletionTimestamp"},
{len(meta.OwnerReferences) > 0, ".template.metadata.ownerReferences"},
{len(meta.Finalizers) > 0, ".template.metadata.finalizers"},
{len(meta.ManagedFields) > 0, ".template.metadata.managedFields"},
}
for _, check := range checks {
if check.set {
return reservedField(check.path)
}
}
for key := range meta.Labels {
if reservedLabelKeys[key] || strings.HasPrefix(key, "kandev.ai/") {
return reservedField(".template.metadata.labels." + key)
}
}
for key := range meta.Annotations {
if strings.HasPrefix(key, "kandev.ai/") {
return reservedField(".template.metadata.annotations." + key)
}
}
return nil
}
func validatePodSpecFields(spec *corev1.PodSpec) error {
if spec.RestartPolicy != "" {
return reservedField(".template.spec.restartPolicy")
}
if spec.NodeName != "" {
return reservedField(".template.spec.nodeName")
}
if spec.OS != nil {
return reservedField(".template.spec.os")
}
for _, key := range []string{corev1.LabelOSStable, corev1.LabelArchStable} {
if _, ok := spec.NodeSelector[key]; ok {
return reservedField(".template.spec.nodeSelector." + key)
}
}
if err := validateNodeAffinity(spec.Affinity); err != nil {
return err
}
for i, volume := range spec.Volumes {
if isReservedVolumeName(volume.Name) {
return reservedField(fmt.Sprintf(".template.spec.volumes[%d].name", i))
}
}
return nil
}
func validateTemplateContainers(spec *corev1.PodSpec, mainContainer string) error {
mainIndex := -1
names := make(map[string]bool)
for i := range spec.Containers {
container := &spec.Containers[i]
path := fmt.Sprintf(".template.spec.containers[%d]", i)
if names[container.Name] {
return fieldError(podTemplateFieldPrefix+path+".name", "duplicates a container name")
}
names[container.Name] = true
if container.Image == "" {
return fieldError(podTemplateFieldPrefix+path+".image", "is required")
}
if container.Name == mainContainer {
if mainIndex >= 0 {
return fieldError(podTemplateFieldPrefix+path+".name", "duplicates the main container")
}
mainIndex = i
if err := validateMainContainerFields(container, path); err != nil {
return err
}
}
if container.Name != mainContainer {
checked, _, err := companionWithoutWorkspaceGrant(container)
if err != nil {
return fieldError(podTemplateFieldPrefix+path+".volumeMounts", err.Error())
}
container = checked
}
if err := validateContainerReservedFields(container, path); err != nil {
return err
}
}
if mainIndex < 0 {
return fieldError(podTemplateFieldPrefix+".template.spec.containers", "must contain the configured main container")
}
return validateAuxiliaryContainers(spec)
}
func validateMainContainerFields(container *corev1.Container, path string) error {
switch {
case len(container.Command) > 0:
return reservedField(path + ".command")
case len(container.Args) > 0:
return reservedField(path + ".args")
case container.WorkingDir != "":
return reservedField(path + ".workingDir")
default:
return nil
}
}
func validateContainerReservedFields(container *corev1.Container, path string) error {
if len(container.EnvFrom) > 0 {
return reservedField(path + ".envFrom")
}
for i, env := range container.Env {
if isReservedEnvironmentKey(env.Name) {
return reservedField(fmt.Sprintf("%s.env[%d].name", path, i))
}
}
for i, port := range container.Ports {
if port.Name == AgentctlContainerPort || port.ContainerPort == DefaultAgentctlPort {
return reservedField(fmt.Sprintf("%s.ports[%d]", path, i))
}
}
for i, mount := range container.VolumeMounts {
if isReservedVolumeName(mount.Name) || isReservedMountPath(mount.MountPath) {
return reservedField(fmt.Sprintf("%s.volumeMounts[%d]", path, i))
}
}
for i, device := range container.VolumeDevices {
if isReservedVolumeName(device.Name) {
return reservedField(fmt.Sprintf("%s.volumeDevices[%d].name", path, i))
}
if isReservedMountPath(device.DevicePath) {
return reservedField(fmt.Sprintf("%s.volumeDevices[%d].devicePath", path, i))
}
}
return nil
}
func isReservedEnvironmentKey(name string) bool {
return name == "HOME" || strings.HasPrefix(name, "AGENTCTL_") || strings.HasPrefix(name, "KANDEV_")
}
func validateAuxiliaryContainers(spec *corev1.PodSpec) error {
for i := range spec.InitContainers {
path := fmt.Sprintf(".template.spec.initContainers[%d]", i)
if err := validateContainerReservedFields(&spec.InitContainers[i], path); err != nil {
return err
}
}
for i := range spec.EphemeralContainers {
path := fmt.Sprintf(".template.spec.ephemeralContainers[%d]", i)
container := corev1.Container(spec.EphemeralContainers[i].EphemeralContainerCommon)
if err := validateContainerReservedFields(&container, path); err != nil {
return err
}
}
return nil
}
func validateNodeAffinity(affinity *corev1.Affinity) error {
if affinity == nil || affinity.NodeAffinity == nil {
return nil
}
nodeAffinity := affinity.NodeAffinity
if nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution != nil {
for i, term := range nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms {
if path := reservedSelectorPath(term, fmt.Sprintf(".template.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[%d]", i)); path != "" {
return reservedField(path)
}
}
}
for i, term := range nodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution {
base := fmt.Sprintf(".template.spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution[%d].preference", i)
if path := reservedSelectorPath(term.Preference, base); path != "" {
return reservedField(path)
}
}
return nil
}
func reservedSelectorPath(term corev1.NodeSelectorTerm, base string) string {
for i, requirement := range term.MatchExpressions {
if requirement.Key == corev1.LabelOSStable || requirement.Key == corev1.LabelArchStable {
return fmt.Sprintf("%s.matchExpressions[%d].key", base, i)
}
}
for i, requirement := range term.MatchFields {
if requirement.Key == corev1.LabelOSStable || requirement.Key == corev1.LabelArchStable {
return fmt.Sprintf("%s.matchFields[%d].key", base, i)
}
}
return ""
}
func isReservedVolumeName(name string) bool {
return name == RuntimeVolumeName || name == AuthVolumeName || name == WorkspaceVolumeName
}
func isReservedMountPath(path string) bool {
cleaned := pathpkg.Clean(path)
return isPathAtOrBelow(cleaned, RuntimeMountPath) ||
isPathAtOrBelow(cleaned, AuthMountPath) ||
isPathAtOrBelow(cleaned, WorkspaceMountPath)
}
func isPathAtOrBelow(candidate, root string) bool {
return candidate == root || strings.HasPrefix(candidate, root+"/")
}
func reservedField(suffix string) error {
return fieldError(podTemplateFieldPrefix+suffix, "is reserved for Kandev")
}