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

260 lines
9.1 KiB
Go

package kubernetes
import (
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation"
)
// Runtime inventory metadata keys are shared by lifecycle persistence and
// higher-level fail-closed inventory checks.
const (
MetadataKeyResourceExecutorID = "kubernetes_resource_executor_id"
MetadataKeyResourceProfileID = "kubernetes_resource_profile_id"
MetadataKeyResourceInstanceID = "kubernetes_resource_instance_id"
MetadataKeyResourceTaskID = "kubernetes_resource_task_id"
MetadataKeyResourceSessionID = "kubernetes_resource_session_id"
MetadataKeyResourceEnvironmentID = "kubernetes_resource_environment_id"
)
type ResourceIdentity struct {
ExecutorID string
ProfileID string
InstanceID string
TaskID string
SessionID string
EnvironmentID string
}
type PodOptions struct {
Name string
Namespace string
Identity ResourceIdentity
Command []string
Args []string
WorkingDir string
AgentctlPort int32
ManagedPVCName string
}
type Warning struct {
Path string `json:"path"`
Message string `json:"message"`
}
func ComposePod(template *corev1.PodTemplate, profile ProfileConfig, options PodOptions) (*corev1.Pod, []Warning, error) {
if err := validateComposition(template, profile, options); err != nil {
return nil, nil, err
}
labels, err := OwnershipLabels(options.Identity)
if err != nil {
return nil, nil, err
}
pod := &corev1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: *template.Template.ObjectMeta.DeepCopy(),
Spec: *template.Template.Spec.DeepCopy(),
}
pod.Name = options.Name
pod.Namespace = options.Namespace
if pod.Labels == nil {
pod.Labels = make(map[string]string, len(labels))
}
for key, value := range labels {
pod.Labels[key] = value
}
applyPodInvariants(pod, profile.Platform)
applyMainContainer(pod, profile.MainContainer, options)
workspace, err := workspaceVolume(profile.Workspace, options.ManagedPVCName)
if err != nil {
return nil, nil, err
}
pod.Spec.Volumes = append(pod.Spec.Volumes, managedVolumes(workspace)...)
if err := validateWorkspaceClaimAliases(pod.Spec.Volumes, pod.Spec.Volumes); err != nil {
return nil, nil, err
}
return pod, collectWarnings(pod), nil
}
func OwnershipLabels(identity ResourceIdentity) (map[string]string, error) {
values := []struct {
path string
key string
value string
}{
{"identity.executor_id", "kandev.ai/executor-id", identity.ExecutorID},
{"identity.profile_id", "kandev.ai/profile-id", identity.ProfileID},
{"identity.instance_id", "kandev.ai/instance-id", identity.InstanceID},
{"identity.task_id", "kandev.ai/task-id", identity.TaskID},
{"identity.session_id", "kandev.ai/session-id", identity.SessionID},
{"identity.environment_id", "kandev.ai/environment-id", identity.EnvironmentID},
}
labels := map[string]string{
"app.kubernetes.io/name": "kandev-agent",
"app.kubernetes.io/component": "agent-session",
"app.kubernetes.io/managed-by": "kandev",
"app.kubernetes.io/instance": identity.InstanceID,
}
for _, item := range values {
if item.value == "" || len(validation.IsValidLabelValue(item.value)) > 0 {
return nil, fieldError(item.path, "must be a non-empty Kubernetes label value")
}
labels[item.key] = item.value
}
return labels, nil
}
func validateComposition(template *corev1.PodTemplate, profile ProfileConfig, options PodOptions) error {
if len(validation.IsDNS1123Subdomain(options.Name)) > 0 {
return fieldError("pod.metadata.name", "must be a valid Pod name")
}
if len(validation.IsDNS1123Label(options.Namespace)) > 0 {
return fieldError("pod.metadata.namespace", "must be a valid namespace")
}
if options.AgentctlPort == 0 {
options.AgentctlPort = DefaultAgentctlPort
}
if options.AgentctlPort < 1 || options.AgentctlPort > 65535 {
return fieldError("pod.agentctl_port", "must be between 1 and 65535")
}
if len(options.Command) == 0 {
return fieldError("pod.command", "is required")
}
if !strings.HasPrefix(options.WorkingDir, "/") {
return fieldError("pod.working_dir", "must be absolute")
}
if profile.Platform != PlatformLinuxAMD64 && profile.Platform != PlatformLinuxARM64 {
return fieldError("config.platform", "must be linux/amd64 or linux/arm64")
}
if errs := validation.IsDNS1123Label(profile.MainContainer); len(errs) > 0 {
return fieldError("config.main_container", "must be a valid container name")
}
if err := profile.Workspace.validate(); err != nil {
return err
}
return ValidatePodTemplate(template, profile.MainContainer)
}
func applyPodInvariants(pod *corev1.Pod, platform Platform) {
pod.Spec.RestartPolicy = corev1.RestartPolicyAlways
pod.Spec.OS = &corev1.PodOS{Name: corev1.Linux}
if pod.Spec.AutomountServiceAccountToken == nil {
value := false
pod.Spec.AutomountServiceAccountToken = &value
}
if pod.Spec.NodeSelector == nil {
pod.Spec.NodeSelector = make(map[string]string, 2)
}
architecture := strings.TrimPrefix(string(platform), "linux/")
pod.Spec.NodeSelector[corev1.LabelOSStable] = "linux"
pod.Spec.NodeSelector[corev1.LabelArchStable] = architecture
}
func applyMainContainer(pod *corev1.Pod, mainContainer string, options PodOptions) {
port := options.AgentctlPort
if port == 0 {
port = DefaultAgentctlPort
}
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].Name != mainContainer {
continue
}
main := &pod.Spec.Containers[i]
main.Command = append([]string(nil), options.Command...)
main.Args = append([]string(nil), options.Args...)
main.WorkingDir = options.WorkingDir
main.Ports = append(main.Ports, corev1.ContainerPort{
Name: AgentctlContainerPort, ContainerPort: port, Protocol: corev1.ProtocolTCP,
})
main.VolumeMounts = append(main.VolumeMounts, managedVolumeMounts()...)
return
}
}
func managedVolumeMounts() []corev1.VolumeMount {
return []corev1.VolumeMount{
{Name: RuntimeVolumeName, MountPath: RuntimeMountPath},
{Name: AuthVolumeName, MountPath: AuthMountPath},
{Name: WorkspaceVolumeName, MountPath: WorkspaceMountPath},
}
}
func managedVolumes(workspace corev1.VolumeSource) []corev1.Volume {
return []corev1.Volume{
{Name: RuntimeVolumeName, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}},
{Name: AuthVolumeName, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}},
{Name: WorkspaceVolumeName, VolumeSource: workspace},
}
}
func workspaceVolume(workspace WorkspaceConfig, managedPVCName string) (corev1.VolumeSource, error) {
switch workspace.Mode {
case WorkspaceModeEmptyDir:
return corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, nil
case WorkspaceModeExistingClaim:
return claimVolume(workspace.ClaimName), nil
case WorkspaceModeManagedPVC:
if len(validation.IsDNS1123Subdomain(managedPVCName)) > 0 {
return corev1.VolumeSource{}, fieldError("pod.managed_pvc_name", "must be a valid claim name")
}
return claimVolume(managedPVCName), nil
default:
return corev1.VolumeSource{}, fieldError("config.workspace.mode", "is unsupported")
}
}
func claimVolume(name string) corev1.VolumeSource {
return corev1.VolumeSource{PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: name}}
}
func collectWarnings(pod *corev1.Pod) []Warning {
warnings := make([]Warning, 0)
if pod.Spec.AutomountServiceAccountToken != nil && *pod.Spec.AutomountServiceAccountToken {
warnings = append(warnings, warning(
".template.spec.automountServiceAccountToken",
"service account token automount is enabled",
))
}
if pod.Spec.HostNetwork {
warnings = append(warnings, warning(".template.spec.hostNetwork", "host networking is enabled"))
}
if pod.Spec.HostPID {
warnings = append(warnings, warning(".template.spec.hostPID", "host PID namespace is enabled"))
}
if pod.Spec.HostIPC {
warnings = append(warnings, warning(".template.spec.hostIPC", "host IPC namespace is enabled"))
}
for i, volume := range pod.Spec.Volumes {
if volume.HostPath != nil {
warnings = append(warnings, warning(fmt.Sprintf(".template.spec.volumes[%d].hostPath", i), "host filesystem access is enabled"))
}
}
for i := range pod.Spec.Containers {
warnings = append(warnings, containerWarnings(&pod.Spec.Containers[i], fmt.Sprintf(".template.spec.containers[%d]", i))...)
}
for i := range pod.Spec.InitContainers {
warnings = append(warnings, containerWarnings(&pod.Spec.InitContainers[i], fmt.Sprintf(".template.spec.initContainers[%d]", i))...)
}
return warnings
}
func containerWarnings(container *corev1.Container, path string) []Warning {
warnings := make([]Warning, 0)
if container.SecurityContext != nil && container.SecurityContext.Privileged != nil && *container.SecurityContext.Privileged {
warnings = append(warnings, warning(path+".securityContext.privileged", "privileged container is enabled"))
}
for i, port := range container.Ports {
if port.HostPort != 0 {
warnings = append(warnings, warning(fmt.Sprintf("%s.ports[%d].hostPort", path, i), "host port is enabled"))
}
}
return warnings
}
func warning(suffix, message string) Warning {
return Warning{Path: podTemplateFieldPrefix + suffix, Message: message}
}