Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions api/v1alpha1/task_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,33 @@ type PodOverrides struct {
// Workload Identity, or Azure Workload Identity.
// +optional
ServiceAccountName string `json:"serviceAccountName,omitempty"`

// Volumes is a list of additional volumes to attach to the agent pod.
// User-supplied volume names must not collide with Kelos-reserved
// names ("workspace", "kelos-plugin").
// +optional
Volumes []corev1.Volume `json:"volumes,omitempty"`

// VolumeMounts is a list of additional volume mounts to add to the
// agent container. Names must reference either a user-supplied volume
// from Volumes or a Kelos-managed volume ("workspace", "kelos-plugin").
// Init containers are not exposed via this field.
// +optional
VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"`

// PodSecurityContext is applied to the agent pod. Fields set here
// override Kelos defaults; fields left unset retain Kelos defaults
// (in particular, FSGroup is retained when a workspace is mounted so
// the agent user keeps read/write access to the workspace volume).
// +optional
PodSecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"`

// ContainerSecurityContext is applied to the agent container. Use
// this to declare allowPrivilegeEscalation=false, capabilities.drop=[ALL],
// readOnlyRootFilesystem=true, etc., so the spawned pod can land in a
// PSS restricted namespace.
// +optional
ContainerSecurityContext *corev1.SecurityContext `json:"containerSecurityContext,omitempty"`
}

// TaskSpec defines the desired state of Task.
Expand Down
24 changes: 24 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
| `spec.podOverrides.env` | Additional environment variables (built-in vars take precedence on conflict) | No |
| `spec.podOverrides.nodeSelector` | Node selection labels to constrain which nodes run agent pods | No |
| `spec.podOverrides.serviceAccountName` | Service account name for the agent pod; use with workload identity systems (IRSA, GKE Workload Identity, Azure) | No |
| `spec.podOverrides.volumes` | Additional volumes to attach to the agent pod. Names must not collide with Kelos-reserved names (`workspace`, `kelos-plugin`) | No |
| `spec.podOverrides.volumeMounts` | Additional volume mounts on the agent container; names must reference either a user-supplied volume from `volumes` or a Kelos-managed volume (`workspace`, `kelos-plugin`) | No |
| `spec.podOverrides.podSecurityContext` | Pod-level security context applied to the agent pod. Fields set here override Kelos defaults; `fsGroup` retains the Kelos default when unset so the agent user keeps workspace access | No |
| `spec.podOverrides.containerSecurityContext` | Security context applied to the agent container. Use to declare `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `readOnlyRootFilesystem: true`, etc., for PSS-restricted namespaces | No |

### Dependency Result Passing

Expand Down Expand Up @@ -181,7 +185,7 @@ GitHub Apps are preferred over PATs for production use because they offer fine-g
| `spec.taskTemplate.dependsOn` | Task names that spawned Tasks depend on | No |
| `spec.taskTemplate.branch` | Git branch template for spawned Tasks (supports Go template variables, e.g., `kelos-task-{{.Number}}`) | No |
| `spec.taskTemplate.ttlSecondsAfterFinished` | Auto-delete spawned tasks after N seconds | No |
| `spec.taskTemplate.podOverrides` | Pod customization for spawned Tasks (resources, timeout, env, nodeSelector, serviceAccountName) | No |
| `spec.taskTemplate.podOverrides` | Pod customization for spawned Tasks (resources, timeout, env, nodeSelector, serviceAccountName, volumes, volumeMounts, podSecurityContext, containerSecurityContext) | No |
| `spec.pollInterval` | How often to poll the source (default: `5m`). Deprecated: use per-source `pollInterval` instead | No |
| `spec.maxConcurrency` | Limit max concurrent running tasks (important for cost control) | No |
| `spec.maxTotalTasks` | Lifetime limit on total tasks created by this spawner | No |
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/dryrun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -709,8 +709,8 @@ func TestInstallCommand_DryRun_Version(t *testing.T) {
}
})

if strings.Contains(output, ":latest") {
t.Errorf("expected all :latest tags to be replaced, got:\n%s", output[:min(len(output), 500)])
if imageLatestRefRE.MatchString(output) {
t.Errorf("expected all :latest image refs to be replaced, got:\n%s", output[:min(len(output), 500)])
}
if !strings.Contains(output, ":v0.5.0") {
t.Errorf("expected :v0.5.0 tags in dry-run output, got:\n%s", output[:min(len(output), 500)])
Expand Down
15 changes: 11 additions & 4 deletions internal/cli/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"os"
"path/filepath"
"regexp"
"strings"
"testing"

Expand All @@ -18,6 +19,12 @@ import (
"github.com/kelos-dev/kelos/internal/manifests"
)

// imageLatestRefRE matches actual image references ending in ":latest" while
// ignoring narrative occurrences in CRD descriptions like "Defaults to Always
// if :latest tag is specified" — the leading non-whitespace requirement
// distinguishes "registry/name:latest" from " :latest" prose.
var imageLatestRefRE = regexp.MustCompile(`\S:latest`)

func TestParseManifests_SingleDocument(t *testing.T) {
data := []byte(`apiVersion: v1
kind: Namespace
Expand Down Expand Up @@ -228,8 +235,8 @@ func TestRenderChart_VersionSubstitution(t *testing.T) {
if err != nil {
t.Fatalf("rendering chart: %v", err)
}
if bytes.Contains(data, []byte(":latest")) {
t.Error("expected all :latest tags to be replaced")
if imageLatestRefRE.Match(data) {
t.Error("expected all :latest image refs to be replaced")
}
if !bytes.Contains(data, []byte(":v0.5.0")) {
t.Error("expected :v0.5.0 tags in rendered output")
Expand Down Expand Up @@ -501,8 +508,8 @@ func TestInstallCommand_VersionFlag(t *testing.T) {
}
})

if strings.Contains(output, ":latest") {
t.Errorf("expected all :latest tags to be replaced, got:\n%s", output[:min(len(output), 500)])
if imageLatestRefRE.MatchString(output) {
t.Errorf("expected all :latest image refs to be replaced, got:\n%s", output[:min(len(output), 500)])
}
if !strings.Contains(output, ":v0.5.0") {
t.Errorf("expected :v0.5.0 tags in output, got:\n%s", output[:min(len(output), 500)])
Expand Down
49 changes: 49 additions & 0 deletions internal/controller/job_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,32 @@ func (b *JobBuilder) buildAgentJob(task *kelosv1alpha1.Task, workspace *kelosv1a
if po.ServiceAccountName != "" {
serviceAccountName = po.ServiceAccountName
}

if len(po.Volumes) > 0 {
if err := validateUserVolumes(po.Volumes); err != nil {
return nil, err
}
volumes = append(volumes, po.Volumes...)
}

if len(po.VolumeMounts) > 0 {
mainContainer.VolumeMounts = append(mainContainer.VolumeMounts, po.VolumeMounts...)
}

if po.PodSecurityContext != nil {
merged := po.PodSecurityContext.DeepCopy()
// Retain Kelos's default FSGroup so the agent user keeps
// access to the workspace volume unless the user opts in
// to a different value explicitly.
if merged.FSGroup == nil && podSecurityContext != nil && podSecurityContext.FSGroup != nil {
merged.FSGroup = podSecurityContext.FSGroup
}
podSecurityContext = merged
}

if po.ContainerSecurityContext != nil {
mainContainer.SecurityContext = po.ContainerSecurityContext.DeepCopy()
}
}

// PodFailurePolicy ensures only pod disruptions (e.g. node scale-down,
Expand Down Expand Up @@ -727,6 +753,29 @@ func shellQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
}

// reservedVolumeNames is the set of volume names that Kelos manages
// internally. PodOverrides.Volumes entries must not use these names.
var reservedVolumeNames = map[string]struct{}{
WorkspaceVolumeName: {},
PluginVolumeName: {},
}

// validateUserVolumes ensures no user-supplied volume name collides with
// a Kelos-reserved name or duplicates another user-supplied name.
func validateUserVolumes(volumes []corev1.Volume) error {
seen := make(map[string]struct{}, len(volumes))
for _, v := range volumes {
if _, reserved := reservedVolumeNames[v.Name]; reserved {
return fmt.Errorf("podOverrides.volumes: %q is a Kelos-reserved volume name", v.Name)
}
if _, dup := seen[v.Name]; dup {
return fmt.Errorf("podOverrides.volumes: duplicate volume name %q", v.Name)
}
seen[v.Name] = struct{}{}
}
return nil
}

// sanitizeComponentName validates that a plugin, skill, or agent name is safe
// for use as a path component. It rejects empty names, path separators, and
// traversal attempts.
Expand Down
Loading
Loading