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
34 changes: 28 additions & 6 deletions internal/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,12 @@ func (m *Manager) Delete(ctx context.Context) error {
}

// DetectNixMounts detects available nix mounts on the host system
func DetectNixMounts() (*types.Mount, *types.Mount) {
var nixStore, nixSocket *types.Mount
func DetectNixMounts() (*types.ClusterMount, *types.ClusterMount) {
var nixStore, nixSocket *types.ClusterMount

// Check for /nix/store
if _, err := os.Stat("/nix/store"); err == nil {
nixStore = &types.Mount{
nixStore = &types.ClusterMount{
HostPath: "/nix/store",
ContainerPath: "/nix/store",
}
Expand All @@ -108,7 +108,7 @@ func DetectNixMounts() (*types.Mount, *types.Mount) {

for _, socketPath := range nixSocketPaths {
if _, err := os.Stat(socketPath); err == nil {
nixSocket = &types.Mount{
nixSocket = &types.ClusterMount{
HostPath: socketPath,
ContainerPath: "/nix/var/nix/daemon-socket/socket",
}
Expand All @@ -120,7 +120,7 @@ func DetectNixMounts() (*types.Mount, *types.Mount) {
}

// DetectDeskrunCache detects the host deskrun cache directory and creates mount config
func DetectDeskrunCache() *types.Mount {
func DetectDeskrunCache() *types.ClusterMount {
homeDir, err := os.UserHomeDir()
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to get home directory for deskrun cache: %v\n", err)
Expand All @@ -135,12 +135,27 @@ func DetectDeskrunCache() *types.Mount {
return nil
}

return &types.Mount{
return &types.ClusterMount{
HostPath: deskrunCachePath,
ContainerPath: "/host-cache/deskrun",
}
}

// DetectDockerSocket detects if Docker socket is available on the host system
func DetectDockerSocket() *types.ClusterMount {
dockerSocketPath := "/var/run/docker.sock"

// Check if Docker socket exists
if _, err := os.Stat(dockerSocketPath); err == nil {
return &types.ClusterMount{
HostPath: dockerSocketPath,
ContainerPath: dockerSocketPath,
}
}

return nil
}

// buildKindConfig creates a kind cluster configuration with nix and cache mounts
func (m *Manager) buildKindConfig() *v1alpha4.Cluster {
config := &v1alpha4.Cluster{
Expand Down Expand Up @@ -183,6 +198,13 @@ func (m *Manager) buildKindConfig() *v1alpha4.Cluster {
})
}

if m.config.DockerSocket != nil {
extraMounts = append(extraMounts, v1alpha4.Mount{
HostPath: m.config.DockerSocket.HostPath,
ContainerPath: m.config.DockerSocket.ContainerPath,
})
}

if len(extraMounts) > 0 {
node.ExtraMounts = extraMounts
}
Expand Down
140 changes: 123 additions & 17 deletions internal/cmd/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ var (
addInstances int
addAuthType string
addAuthValue string
addCachePaths []string
addCachePaths []string // Deprecated: kept for backward compatibility
addMounts []string
)

var addCmd = &cobra.Command{
Expand All @@ -44,29 +45,34 @@ Examples:
# Add a standard runner (single instance, scales 1-5)
deskrun add my-runner --repository https://github.com/owner/repo --auth-type pat --auth-value ghp_xxx

# Add a privileged runner with Nix cache (single instance, scales 1-5)
deskrun add nix-runner \
# Add a privileged runner with Docker cache (single instance, scales 1-5)
deskrun add docker-runner \
--repository https://github.com/owner/repo \
--mode cached-privileged-kubernetes \
--cache /nix/store \
--mount /var/lib/docker \
--max-runners 5 \
--auth-type pat --auth-value ghp_xxx

# Add a privileged runner with custom source and target cache paths
# Add a privileged runner with custom source and target mount paths
deskrun add custom-runner \
--repository https://github.com/owner/repo \
--mode cached-privileged-kubernetes \
--cache /host/cache/npm:/root/.npm \
--cache /host/cache/cargo:/usr/local/cargo/registry \
--mount /host/cache/npm:/root/.npm \
--mount /host/cache/cargo:/usr/local/cargo/registry \
--auth-type pat --auth-value ghp_xxx

# Add a runner with Docker socket mount for host Docker access
deskrun add docker-runner \
--repository https://github.com/owner/repo \
--mount /var/run/docker.sock:/var/run/docker.sock:Socket \
--auth-type pat --auth-value ghp_xxx

# Add a privileged runner with 3 instances for cache isolation
# Each instance runs exactly 1 runner with dedicated cache paths
deskrun add nix-runner \
deskrun add multi-runner \
--repository https://github.com/owner/repo \
--mode cached-privileged-kubernetes \
--cache /nix/store \
--cache /var/lib/docker \
--mount /var/lib/docker \
--instances 3 \
--auth-type pat --auth-value ghp_xxx

Expand All @@ -85,7 +91,8 @@ func init() {
addCmd.Flags().IntVar(&addInstances, "instances", 1, "Number of separate runner scale set instances (each will have min=1, max=1 for cache isolation)")
addCmd.Flags().StringVar(&addAuthType, "auth-type", "pat", "Authentication type (pat, github-app)")
addCmd.Flags().StringVar(&addAuthValue, "auth-value", "", "Authentication value (PAT token or GitHub App private key)")
addCmd.Flags().StringSliceVar(&addCachePaths, "cache", []string{}, "Cache paths to mount. Format: target or src:target (can be specified multiple times)")
addCmd.Flags().StringSliceVar(&addMounts, "mount", []string{}, "Mount paths. Format: target, src:target, or src:target:type (can be specified multiple times)")
addCmd.Flags().StringSliceVar(&addCachePaths, "cache", []string{}, "Deprecated: use --mount instead. Cache paths to mount. Format: target or src:target")

if err := addCmd.MarkFlagRequired("repository"); err != nil {
panic(err)
Expand Down Expand Up @@ -127,7 +134,7 @@ func runAdd(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid auth type: %s", addAuthType)
}

// Create cache paths
// Create cache paths from --cache flag (deprecated, for backward compatibility)
cachePaths := []types.CachePath{}
for _, path := range addCachePaths {
// Parse src:target notation
Expand Down Expand Up @@ -156,8 +163,82 @@ func runAdd(cmd *cobra.Command, args []string) error {
})
}

// Validate parameters including cache paths
if err := validateAddParams(addInstances, addMaxRunners, containerMode, cachePaths); err != nil {
// Create mounts from --mount flag (new format)
mounts := []types.Mount{}
for _, path := range addMounts {
// Parse src:target:type notation
// Supported formats:
// - target (auto-generated source, DirectoryOrCreate type)
// - src:target (explicit source, DirectoryOrCreate type)
// - src:target:type (explicit source and type)
var source, target string
mountType := types.MountTypeDirectoryOrCreate

parts := strings.Split(path, ":")
switch len(parts) {
case 1:
// Just target path, auto-generate source
target = parts[0]
safePath := strings.TrimPrefix(path, "/")
safePath = strings.ReplaceAll(safePath, "/", "-")
source = fmt.Sprintf("/tmp/deskrun-cache/%s", safePath)
case 2:
// src:target
source = parts[0]
target = parts[1]
case 3:
// src:target:type
source = parts[0]
target = parts[1]
typeStr := parts[2]
switch typeStr {
case "DirectoryOrCreate":
mountType = types.MountTypeDirectoryOrCreate
case "Directory":
mountType = types.MountTypeDirectory
case "Socket":
mountType = types.MountTypeSocket
default:
return fmt.Errorf("invalid mount type '%s', must be one of: DirectoryOrCreate, Directory, Socket", typeStr)
}
default:
return fmt.Errorf("invalid mount format '%s', expected target, src:target, or src:target:type", path)
}
Comment on lines +177 to +206
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mount path parsing uses strings.Split with ":" as the delimiter, which could cause issues if file paths contain colons. While colons are rare in Unix paths, they are valid characters. For example, a path like "/tmp/my:weird:path" would be incorrectly split into 4 parts.

Windows paths with drive letters (e.g., "C:/path") would also be problematic, though this seems to be a Linux/Unix-focused tool.

Consider documenting this limitation or using a more robust parsing approach that handles escaped colons or uses a different delimiter for the type field (e.g., "@" or using flags like --mount-source, --mount-target, --mount-type).

Copilot uses AI. Check for mistakes.

mounts = append(mounts, types.Mount{
Source: source,
Target: target,
Type: mountType,
})
}

// Check for duplicates within mounts
mountTargets := make(map[string]struct{}, len(mounts))
for _, m := range mounts {
if _, exists := mountTargets[m.Target]; exists {
return fmt.Errorf("duplicate mount target '%s' specified multiple times", m.Target)
}
mountTargets[m.Target] = struct{}{}
}

// Validate that there are no duplicate target paths between deprecated --cache
// paths (cachePaths) and new --mount targets. Using the same target path with
// both flags would result in duplicate volume mounts and cause pod creation
// to fail with duplicate mountPath errors.
if len(cachePaths) > 0 && len(mounts) > 0 {
cacheTargets := make(map[string]struct{}, len(cachePaths))
for _, p := range cachePaths {
cacheTargets[p.Target] = struct{}{}
}
for _, m := range mounts {
if _, exists := cacheTargets[m.Target]; exists {
return fmt.Errorf("duplicate mount target '%s' specified via both --cache and --mount; use only one of these flags for this path", m.Target)
}
}
}

// Validate parameters including mounts
if err := validateAddParams(addInstances, addMaxRunners, containerMode, cachePaths, mounts); err != nil {
return err
}

Expand All @@ -178,7 +259,8 @@ func runAdd(cmd *cobra.Command, args []string) error {
MinRunners: minRunners,
MaxRunners: maxRunners,
Instances: addInstances,
CachePaths: cachePaths,
Mounts: mounts,
CachePaths: cachePaths, // Keep for backward compatibility
AuthType: authType,
AuthValue: addAuthValue,
}
Expand All @@ -200,8 +282,8 @@ func runAdd(cmd *cobra.Command, args []string) error {
return nil
}

// validateAddParams validates the instances, max-runners, and cache paths
func validateAddParams(instances, maxRunners int, containerMode types.ContainerMode, cachePaths []types.CachePath) error {
// validateAddParams validates the instances, max-runners, cache paths, and mounts
func validateAddParams(instances, maxRunners int, containerMode types.ContainerMode, cachePaths []types.CachePath, mounts []types.Mount) error {
// Validate instances
if instances < 1 {
return fmt.Errorf("instances must be at least 1")
Expand Down Expand Up @@ -231,6 +313,30 @@ func validateAddParams(instances, maxRunners int, containerMode types.ContainerM
}
}

// Validate mounts - provide helpful guidance for /nix/store
for _, mount := range mounts {
if mount.Target == "/nix/store" {
return fmt.Errorf(
"mount target /nix/store is not supported in deskrun: " +
"mounting host paths directly to /nix/store breaks NixOS containers by overwriting essential NixOS binaries and libraries.\n\n" +
"To cache Nix packages, consider:\n" +
"1. Use the opencode-workspace-action with overlayfs support for /nix/store caching\n" +
"2. Mount alternative paths like /root/.cache/nix for user-level Nix cache\n" +
"3. Mount /var/lib/docker for Docker layer caching (unaffected by this limitation)\n\n" +
"See: https://github.com/rkoster/opencode-workspace-action/issues (overlay filesystem support for /nix/store)")
}

// Validate that target path is absolute
if !strings.HasPrefix(mount.Target, "/") {
return fmt.Errorf("mount target path '%s' must be an absolute path", mount.Target)
}

// Validate that source path is absolute when provided
if mount.Source != "" && !strings.HasPrefix(mount.Source, "/") {
return fmt.Errorf("mount source path '%s' must be an absolute path", mount.Source)
}
}

return nil
}

Expand Down
18 changes: 18 additions & 0 deletions internal/cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ func runList(cmd *cobra.Command, args []string) error {

fmt.Printf("Auth Type: %s\n", installation.AuthType)

if len(installation.Mounts) > 0 {
fmt.Printf("Mounts: ")
for i, mount := range installation.Mounts {
if i > 0 {
fmt.Printf(" ")
}
if mount.Source != "" {
if mount.Type != "" && mount.Type != types.MountTypeDirectoryOrCreate {
fmt.Printf("%s:%s:%s\n", mount.Source, mount.Target, mount.Type)
} else {
fmt.Printf("%s:%s\n", mount.Source, mount.Target)
}
} else {
fmt.Printf("%s\n", mount.Target)
}
}
}

if len(installation.CachePaths) > 0 {
fmt.Printf("Cache Paths: ")
for i, path := range installation.CachePaths {
Expand Down
10 changes: 7 additions & 3 deletions internal/cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,15 @@ func runUp(cmd *cobra.Command, args []string) error {
// Detect available nix mounts
nixStore, nixSocket := cluster.DetectNixMounts()

// Detect docker socket if available
dockerSocket := cluster.DetectDockerSocket()

// Setup cluster manager
clusterConfig := &types.ClusterConfig{
Name: configMgr.GetConfig().ClusterName,
NixStore: nixStore,
NixSocket: nixSocket,
Name: configMgr.GetConfig().ClusterName,
NixStore: nixStore,
NixSocket: nixSocket,
DockerSocket: dockerSocket,
}
clusterMgr := cluster.NewManager(clusterConfig)

Expand Down
12 changes: 6 additions & 6 deletions internal/runner/overlay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,9 @@ var _ = Describe("ytt Overlay Processing", func() {
volumeMounts := runnerContainer["volumeMounts"].([]interface{})
expectedMounts := map[string]string{
"hook-extension": "/etc/hooks",
"cache-0": "/nix/store-host",
"cache-1": "/nix/var/nix/daemon-socket-host",
"cache-2": "/var/lib/docker",
"mount-0": "/nix/store-host",
"mount-1": "/nix/var/nix/daemon-socket-host",
"mount-2": "/var/lib/docker",
}

for expectedName, expectedPath := range expectedMounts {
Expand All @@ -318,9 +318,9 @@ var _ = Describe("ytt Overlay Processing", func() {
volumes := podSpec["volumes"].([]interface{})
expectedVolumes := map[string]string{
"hook-extension": "configMap",
"cache-0": "hostPath",
"cache-1": "hostPath",
"cache-2": "emptyDir",
"mount-0": "hostPath",
"mount-1": "hostPath",
"mount-2": "emptyDir",
}

for expectedName, expectedType := range expectedVolumes {
Expand Down
Loading
Loading