Skip to content
Closed
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
30 changes: 30 additions & 0 deletions api/v1alpha1/workspace_types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package v1alpha1

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -28,6 +29,27 @@ type WorkspaceFile struct {
Content string `json:"content"`
}

// WorkspaceVolume defines an additional volume to mount into the agent
// container (and setup containers, once supported).
type WorkspaceVolume struct {
// Name is the volume name (must be unique across workspace volumes).
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`

// MountPath is the absolute path where the volume is mounted.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:Pattern="^/"
MountPath string `json:"mountPath"`

// ReadOnly mounts the volume as read-only when true.
// +optional
ReadOnly bool `json:"readOnly,omitempty"`

// Source is the Kubernetes volume source (e.g. PersistentVolumeClaim,
// ConfigMap, Secret, EmptyDir).
Source corev1.VolumeSource `json:"source"`
}

// WorkspaceSpec defines the desired state of Workspace.
type WorkspaceSpec struct {
// Repo is the git repository URL to clone.
Expand Down Expand Up @@ -58,6 +80,14 @@ type WorkspaceSpec struct {
// like "CLAUDE.md" or "AGENTS.md".
// +optional
Files []WorkspaceFile `json:"files,omitempty"`

// Volumes are additional volumes mounted into the agent container.
// They do not replace the workspace volume — they are supplementary
// mounts (e.g. a PVC with pre-populated dependencies or shared data).
// +optional
// +kubebuilder:validation:XValidation:rule="self.map(v, v.name).size() == self.size()",message="volume names must be unique"
// +kubebuilder:validation:XValidation:rule="self.all(v, v.name != 'workspace' && v.name != 'kelos-plugin')",message="volume names 'workspace' and 'kelos-plugin' are reserved"
Volumes []WorkspaceVolume `json:"volumes,omitempty"`
}

// +genclient
Expand Down
23 changes: 23 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.

13 changes: 13 additions & 0 deletions examples/12-workspace-with-volumes/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: kelos.dev/v1alpha1
kind: Task
metadata:
name: fix-frontend-bug
spec:
type: claude-code
prompt: "Fix the CSS regression in the header component"
credentials:
type: apiKey
secretRef:
name: anthropic-key
workspaceRef:
name: my-app-cached
17 changes: 17 additions & 0 deletions examples/12-workspace-with-volumes/workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: kelos.dev/v1alpha1
kind: Workspace
metadata:
name: my-app-cached
spec:
repo: https://github.com/your-org/your-repo.git
ref: main
secretRef:
name: github-token
volumes:
- name: npm-cache
mountPath: /workspace/repo/node_modules
readOnly: true
source:
persistentVolumeClaim:
claimName: shared-npm-cache
readOnly: true
13 changes: 13 additions & 0 deletions examples/15-workspace-with-setup/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: kelos.dev/v1alpha1
kind: Task
metadata:
name: fix-frontend-bug
spec:
type: claude-code
prompt: "Fix the CSS regression in the header component"
credentials:
type: apiKey
secretRef:
name: anthropic-key
workspaceRef:
name: my-app-with-setup
22 changes: 22 additions & 0 deletions examples/15-workspace-with-setup/workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: kelos.dev/v1alpha1
kind: Workspace
metadata:
name: my-app-with-setup
spec:
repo: https://github.com/your-org/your-repo.git
ref: main
secretRef:
name: github-token
volumes:
- name: npm-cache
mountPath: /workspace/repo/node_modules
source:
persistentVolumeClaim:
claimName: shared-npm-cache
setup:
- name: install-deps
image: node:22-alpine
command: ["sh", "-c", "npm ci --prefer-offline"]
- name: generate-protos
image: bufbuild/buf:latest
command: ["sh", "-c", "buf generate"]
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 imageLatestRe.MatchString(output) {
t.Errorf("expected all :latest image tags 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
14 changes: 10 additions & 4 deletions internal/cli/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"bytes"
"context"
"regexp"
"os"
"path/filepath"
"strings"
Expand All @@ -18,6 +19,11 @@ import (
"github.com/kelos-dev/kelos/internal/manifests"
)

// imageLatestRe matches actual image references with :latest tag (e.g.,
// "ghcr.io/kelos-dev/kelos-controller:latest") while ignoring occurrences
// of ":latest" inside CRD description strings.
var imageLatestRe = regexp.MustCompile(`ghcr\.io/[^:\s]+:latest`)

func TestParseManifests_SingleDocument(t *testing.T) {
data := []byte(`apiVersion: v1
kind: Namespace
Expand Down Expand Up @@ -228,8 +234,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 imageLatestRe.Match(data) {
t.Error("expected all :latest image tags 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 +507,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 imageLatestRe.MatchString(output) {
t.Errorf("expected all :latest image tags 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
14 changes: 14 additions & 0 deletions internal/controller/job_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,20 @@ func (b *JobBuilder) buildAgentJob(task *kelosv1alpha1.Task, workspace *kelosv1a
}

mainContainer.VolumeMounts = []corev1.VolumeMount{volumeMount}

// Append user-defined workspace volumes to the pod and agent container.
for _, wv := range workspace.Volumes {
volumes = append(volumes, corev1.Volume{
Name: wv.Name,
VolumeSource: wv.Source,
})
mainContainer.VolumeMounts = append(mainContainer.VolumeMounts, corev1.VolumeMount{
Name: wv.Name,
MountPath: wv.MountPath,
ReadOnly: wv.ReadOnly,
})
}

mainContainer.WorkingDir = WorkspaceMountPath + "/repo"
}

Expand Down
Loading
Loading