304 lines
9.9 KiB
Go
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")
|
|
}
|