diff --git a/internal/cluster/cluster.go b/internal/cluster/cluster.go index 95ebcb7..984e795 100644 --- a/internal/cluster/cluster.go +++ b/internal/cluster/cluster.go @@ -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", } @@ -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", } @@ -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) @@ -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{ @@ -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 } diff --git a/internal/cmd/add.go b/internal/cmd/add.go index dc00184..37b0c2d 100644 --- a/internal/cmd/add.go +++ b/internal/cmd/add.go @@ -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{ @@ -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 @@ -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) @@ -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 @@ -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) + } + + 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 } @@ -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, } @@ -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") @@ -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 } diff --git a/internal/cmd/list.go b/internal/cmd/list.go index ef5fa35..5112082 100644 --- a/internal/cmd/list.go +++ b/internal/cmd/list.go @@ -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 { diff --git a/internal/cmd/up.go b/internal/cmd/up.go index 334bbca..a8382bd 100644 --- a/internal/cmd/up.go +++ b/internal/cmd/up.go @@ -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) diff --git a/internal/runner/overlay_test.go b/internal/runner/overlay_test.go index 6c94e9c..fa27ac5 100644 --- a/internal/runner/overlay_test.go +++ b/internal/runner/overlay_test.go @@ -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 { @@ -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 { diff --git a/internal/runner/template_spec/testdata/expected/privileged_multi_cache.yaml b/internal/runner/template_spec/testdata/expected/privileged_multi_cache.yaml index 744a16c..c65e351 100644 --- a/internal/runner/template_spec/testdata/expected/privileged_multi_cache.yaml +++ b/internal/runner/template_spec/testdata/expected/privileged_multi_cache.yaml @@ -64,9 +64,9 @@ data: mountPath: /lib64 - name: glibc-compat mountPath: /lib/x86_64-linux-gnu - - name: cache-0 + - name: mount-0 mountPath: /var/lib/docker - - name: cache-1 + - name: mount-1 mountPath: /nix/store volumes: - name: sys @@ -95,11 +95,11 @@ data: type: Directory - name: glibc-compat emptyDir: {} - - name: cache-0 + - name: mount-0 hostPath: path: /var/lib/docker type: DirectoryOrCreate - - name: cache-1 + - name: mount-1 hostPath: path: /nix/store type: DirectoryOrCreate @@ -384,9 +384,9 @@ spec: - name: hook-extension mountPath: /etc/hooks readOnly: true - - name: cache-0 + - name: mount-0 mountPath: /var/lib/docker - - name: cache-1 + - name: mount-1 mountPath: /nix/store securityContext: privileged: true @@ -398,11 +398,11 @@ spec: configMap: name: privileged-hook-extension-test-runner defaultMode: 493 - - name: cache-0 + - name: mount-0 hostPath: path: /var/lib/docker type: DirectoryOrCreate - - name: cache-1 + - name: mount-1 hostPath: path: /nix/store type: DirectoryOrCreate diff --git a/internal/runner/template_test.go b/internal/runner/template_test.go index d95c6d3..cba1ae5 100644 --- a/internal/runner/template_test.go +++ b/internal/runner/template_test.go @@ -129,17 +129,17 @@ func TestDockerCachePersistenceTemplates(t *testing.T) { Target: "/var/lib/docker", }, wantContains: []string{ - // Cache should be mounted in runner container + // Cache should be mounted "mountPath: /var/lib/docker", // Should use hostPath volume "path: /nvme/docker-images", "type: DirectoryOrCreate", - // Verify cache-0 specifically uses hostPath (not emptyDir) - "name: cache-0\n hostPath:", + // Verify mount-0 specifically uses hostPath (not emptyDir) + "name: mount-0\n hostPath:", }, wantNotContains: []string{ - // cache-0 specifically should NOT be emptyDir (GitHub workspace volumes will be emptyDir, which is correct) - "name: cache-0\n emptyDir:", + // mount-0 specifically should NOT be emptyDir (GitHub workspace volumes will be emptyDir, which is correct) + "name: mount-0\n emptyDir:", }, }, { diff --git a/pkg/templates/processor.go b/pkg/templates/processor.go index c5e74d5..5bdc480 100644 --- a/pkg/templates/processor.go +++ b/pkg/templates/processor.go @@ -177,7 +177,7 @@ func (p *Processor) transformTemplateForYtt(templateContent string) string { // buildDataValues creates the ytt data values YAML from the configuration func (p *Processor) buildDataValues(config Config) ([]byte, error) { - // Convert cache paths to simple map format for easier ytt access + // Convert cache paths to simple map format for easier ytt access (deprecated, for backward compatibility) var cachePaths []map[string]string for _, cp := range config.Installation.CachePaths { cachePaths = append(cachePaths, map[string]string{ @@ -191,6 +191,21 @@ func (p *Processor) buildDataValues(config Config) ([]byte, error) { cachePaths = []map[string]string{} } + // Convert mounts to map format for ytt access + var mounts []map[string]string + for _, m := range config.Installation.Mounts { + mounts = append(mounts, map[string]string{ + "target": m.Target, + "source": m.Source, + "type": string(m.Type), + }) + } + + // If no mounts, use empty array (not nil) for ytt + if mounts == nil { + mounts = []map[string]string{} + } + dataValues := map[string]any{ "installation": map[string]any{ "name": config.InstanceName, @@ -199,7 +214,8 @@ func (p *Processor) buildDataValues(config Config) ([]byte, error) { "containerMode": string(config.Installation.ContainerMode), "minRunners": config.Installation.MinRunners, "maxRunners": config.Installation.MaxRunners, - "cachePaths": cachePaths, + "cachePaths": cachePaths, // Deprecated, for backward compatibility + "mounts": mounts, "instanceNum": config.InstanceNum, }, } diff --git a/pkg/templates/templates/overlay.yaml b/pkg/templates/templates/overlay.yaml index d142ede..c6f67b0 100644 --- a/pkg/templates/templates/overlay.yaml +++ b/pkg/templates/templates/overlay.yaml @@ -70,12 +70,18 @@ #@ {"name": "glibc-compat", "mountPath": "/lib64"}, #@ {"name": "glibc-compat", "mountPath": "/lib/x86_64-linux-gnu"} #@ ] -#@ -#@ # Add cache path volume mounts -#@ for i, cachePath in enumerate(data.values.installation.cachePaths): -#@ volumeMounts.append({"name": "cache-" + str(i), "mountPath": cachePath.target}) -#@ end -#@ + #@ + #@ # Add cache path volume mounts (deprecated - for backward compatibility) + #@ for i, cachePath in enumerate(data.values.installation.cachePaths): + #@ volumeMounts.append({"name": "mount-" + str(i), "mountPath": cachePath.target}) + #@ end + #@ + #@ # Add mount volume mounts (new field) + #@ for i, mount in enumerate(data.values.installation.mounts): + #@ mount_index = i + len(data.values.installation.cachePaths) + #@ volumeMounts.append({"name": "mount-" + str(mount_index), "mountPath": mount.target}) + #@ end + #@ #@ # Note: externals (/__e), work (/__w), and github (/github) volumes are automatically #@ # added by the k8s-novolume hooks, so we don't include them here to avoid duplicates. #@ # The hooks handle all GitHub workspace paths including /github/workflow/event.json @@ -94,21 +100,38 @@ #@ {"name": "shm", "hostPath": {"path": "/dev/shm", "type": "Directory"}}, #@ {"name": "glibc-compat", "emptyDir": {}} #@ ] -#@ -#@ # Add cache path volumes -#@ for i, cachePath in enumerate(data.values.installation.cachePaths): -#@ cache_source = cachePath.source -#@ if cache_source == "": -#@ instance_num = data.values.installation.instanceNum if hasattr(data.values.installation, "instanceNum") else 0 -#@ if instance_num > 0: -#@ cache_source = "/tmp/github-runner-cache/" + data.values.installation.name + "-" + str(instance_num) + "/cache-" + str(i) -#@ else: -#@ cache_source = "/tmp/github-runner-cache/" + data.values.installation.name + "/cache-" + str(i) -#@ end -#@ end -#@ volumes.append({"name": "cache-" + str(i), "hostPath": {"path": cache_source, "type": "DirectoryOrCreate"}}) -#@ end -#@ + #@ + #@ # Add cache path volumes (deprecated - for backward compatibility) + #@ for i, cachePath in enumerate(data.values.installation.cachePaths): + #@ cache_source = cachePath.source + #@ if cache_source == "": + #@ instance_num = data.values.installation.instanceNum if hasattr(data.values.installation, "instanceNum") else 0 + #@ if instance_num > 0: + #@ cache_source = "/tmp/github-runner-cache/" + data.values.installation.name + "-" + str(instance_num) + "/mount-" + str(i) + #@ else: + #@ cache_source = "/tmp/github-runner-cache/" + data.values.installation.name + "/mount-" + str(i) + #@ end + #@ end + #@ volumes.append({"name": "mount-" + str(i), "hostPath": {"path": cache_source, "type": "DirectoryOrCreate"}}) + #@ end + #@ + #@ # Add mount volumes (new field) + #@ for i, mount in enumerate(data.values.installation.mounts): + #@ mount_index = i + len(data.values.installation.cachePaths) + #@ mount_source = mount.source + #@ if mount_source == "": + #@ instance_num = data.values.installation.instanceNum if hasattr(data.values.installation, "instanceNum") else 0 + #@ if instance_num > 0: + #@ mount_source = "/tmp/github-runner-cache/" + data.values.installation.name + "-" + str(instance_num) + "/mount-" + str(mount_index) + #@ else: + #@ mount_source = "/tmp/github-runner-cache/" + data.values.installation.name + "/mount-" + str(mount_index) + #@ end + #@ end + #@ # Determine hostPath type based on mount type + #@ mount_type = mount.type if hasattr(mount, "type") and mount.type != "" else "DirectoryOrCreate" + #@ volumes.append({"name": "mount-" + str(mount_index), "hostPath": {"path": mount_source, "type": mount_type}}) + #@ end + #@ #@ spec["containers"] = [container] #@ spec["volumes"] = volumes #@ @@ -379,9 +402,14 @@ spec: mountPath: /etc/hooks readOnly: true #@ for i, cachePath in enumerate(data.values.installation.cachePaths): - - name: #@ "cache-" + str(i) + - name: #@ "mount-" + str(i) mountPath: #@ cachePath.target #@ end + #@ for i, mount in enumerate(data.values.installation.mounts): + #@ mount_index = i + len(data.values.installation.cachePaths) + - name: #@ "mount-" + str(mount_index) + mountPath: #@ mount.target + #@ end #@overlay/replace volumes: - name: hook-extension @@ -389,7 +417,7 @@ spec: name: #@ "privileged-hook-extension-" + data.values.installation.name defaultMode: 0755 #@ for i, cachePath in enumerate(data.values.installation.cachePaths): - - name: #@ "cache-" + str(i) + - name: #@ "mount-" + str(i) #@ if cachePath.source == "": emptyDir: {} #@ else: @@ -398,6 +426,18 @@ spec: type: DirectoryOrCreate #@ end #@ end + #@ for i, mount in enumerate(data.values.installation.mounts): + #@ mount_index = i + len(data.values.installation.cachePaths) + - name: #@ "mount-" + str(mount_index) + #@ if mount.source == "": + emptyDir: {} + #@ else: + #@ mount_type = mount.type if hasattr(mount, "type") and mount.type != "" else "DirectoryOrCreate" + hostPath: + path: #@ mount.source + type: #@ mount_type + #@ end + #@ end #@ end #! ConfigMap overlay for privileged mode diff --git a/pkg/templates/templates/values/schema.yaml b/pkg/templates/templates/values/schema.yaml index e7c35b3..bdbafa5 100644 --- a/pkg/templates/templates/values/schema.yaml +++ b/pkg/templates/templates/values/schema.yaml @@ -23,7 +23,20 @@ installation: #@schema/validation min=1 maxRunners: 3 - #@schema/desc "Cache path configurations for privileged mode" + #@schema/desc "Mount configurations for volumes" + #@schema/nullable + mounts: + - #@schema/desc "Mount configuration" + #@schema/desc "Source path on host. Required and must be non-empty for Socket mount types." + #@schema/nullable + source: null + #@schema/desc "Target path in container" + target: "" + #@schema/desc "Mount type - must be one of: DirectoryOrCreate, Directory, Socket" + #@schema/validation one_of=["DirectoryOrCreate", "Directory", "Socket"] + type: "DirectoryOrCreate" + + #@schema/desc "Cache path configurations (deprecated, use mounts instead)" #@schema/nullable cachePaths: - #@schema/desc "Cache path mapping" diff --git a/pkg/templates/testdata/expected/privileged_emptydir_cache.yaml b/pkg/templates/testdata/expected/privileged_emptydir_cache.yaml index 408a8ab..3abbe69 100644 --- a/pkg/templates/testdata/expected/privileged_emptydir_cache.yaml +++ b/pkg/templates/testdata/expected/privileged_emptydir_cache.yaml @@ -64,7 +64,7 @@ data: mountPath: /lib64 - name: glibc-compat mountPath: /lib/x86_64-linux-gnu - - name: cache-0 + - name: mount-0 mountPath: /var/lib/docker volumes: - name: sys @@ -93,9 +93,9 @@ data: type: Directory - name: glibc-compat emptyDir: {} - - name: cache-0 + - name: mount-0 hostPath: - path: /tmp/github-runner-cache/test-runner-1/cache-0 + path: /tmp/github-runner-cache/test-runner-1/mount-0 type: DirectoryOrCreate --- apiVersion: v1 @@ -378,7 +378,7 @@ spec: - name: hook-extension mountPath: /etc/hooks readOnly: true - - name: cache-0 + - name: mount-0 mountPath: /var/lib/docker securityContext: privileged: true @@ -390,7 +390,7 @@ spec: configMap: name: privileged-hook-extension-test-runner defaultMode: 493 - - name: cache-0 + - name: mount-0 emptyDir: {} securityContext: fsGroup: 123 diff --git a/pkg/templates/testdata/expected/privileged_multi_cache.yaml b/pkg/templates/testdata/expected/privileged_multi_cache.yaml index 744a16c..c65e351 100644 --- a/pkg/templates/testdata/expected/privileged_multi_cache.yaml +++ b/pkg/templates/testdata/expected/privileged_multi_cache.yaml @@ -64,9 +64,9 @@ data: mountPath: /lib64 - name: glibc-compat mountPath: /lib/x86_64-linux-gnu - - name: cache-0 + - name: mount-0 mountPath: /var/lib/docker - - name: cache-1 + - name: mount-1 mountPath: /nix/store volumes: - name: sys @@ -95,11 +95,11 @@ data: type: Directory - name: glibc-compat emptyDir: {} - - name: cache-0 + - name: mount-0 hostPath: path: /var/lib/docker type: DirectoryOrCreate - - name: cache-1 + - name: mount-1 hostPath: path: /nix/store type: DirectoryOrCreate @@ -384,9 +384,9 @@ spec: - name: hook-extension mountPath: /etc/hooks readOnly: true - - name: cache-0 + - name: mount-0 mountPath: /var/lib/docker - - name: cache-1 + - name: mount-1 mountPath: /nix/store securityContext: privileged: true @@ -398,11 +398,11 @@ spec: configMap: name: privileged-hook-extension-test-runner defaultMode: 493 - - name: cache-0 + - name: mount-0 hostPath: path: /var/lib/docker type: DirectoryOrCreate - - name: cache-1 + - name: mount-1 hostPath: path: /nix/store type: DirectoryOrCreate diff --git a/pkg/templates/testdata/expected/privileged_single_cache.yaml b/pkg/templates/testdata/expected/privileged_single_cache.yaml index 2884dfa..3daacea 100644 --- a/pkg/templates/testdata/expected/privileged_single_cache.yaml +++ b/pkg/templates/testdata/expected/privileged_single_cache.yaml @@ -64,7 +64,7 @@ data: mountPath: /lib64 - name: glibc-compat mountPath: /lib/x86_64-linux-gnu - - name: cache-0 + - name: mount-0 mountPath: /var/lib/docker volumes: - name: sys @@ -93,7 +93,7 @@ data: type: Directory - name: glibc-compat emptyDir: {} - - name: cache-0 + - name: mount-0 hostPath: path: /var/lib/docker type: DirectoryOrCreate @@ -378,7 +378,7 @@ spec: - name: hook-extension mountPath: /etc/hooks readOnly: true - - name: cache-0 + - name: mount-0 mountPath: /var/lib/docker securityContext: privileged: true @@ -390,7 +390,7 @@ spec: configMap: name: privileged-hook-extension-test-runner defaultMode: 493 - - name: cache-0 + - name: mount-0 hostPath: path: /var/lib/docker type: DirectoryOrCreate diff --git a/pkg/types/types.go b/pkg/types/types.go index b0e7f12..15a0e79 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -20,12 +20,36 @@ type RunnerInstallation struct { MinRunners int MaxRunners int Instances int // Number of separate runner scale set instances to create - CachePaths []CachePath + Mounts []Mount + CachePaths []CachePath // Deprecated: Use Mounts instead. Kept for backward compatibility. AuthType AuthType AuthValue string } +// MountType represents the type of host mount +type MountType string + +const ( + // MountTypeDirectoryOrCreate creates a directory if it doesn't exist + MountTypeDirectoryOrCreate MountType = "DirectoryOrCreate" + // MountTypeDirectory mounts an existing directory + MountTypeDirectory MountType = "Directory" + // MountTypeSocket mounts a Unix socket + MountTypeSocket MountType = "Socket" +) + +// Mount represents a host path to be mounted into pods. +type Mount struct { + // Source path on the host machine (can be empty for DirectoryOrCreate to auto-generate; must be provided for Socket types) + Source string + // Target path inside the container where the mount will be mounted + Target string + // Type specifies the hostPath volume type (defaults to DirectoryOrCreate) + Type MountType +} + // CachePath represents a path to be cached using hostPath volumes +// Deprecated: Use Mount instead. This type is kept for backward compatibility. type CachePath struct { // Target path inside the container where the cache will be mounted Target string @@ -45,17 +69,18 @@ const ( type ClusterConfig struct { Name string Network string - NixStore *Mount // Optional nix store mount - NixSocket *Mount // Optional nix socket mount - DeskrunCache *Mount // Optional deskrun cache mount + NixStore *ClusterMount // Optional nix store mount + NixSocket *ClusterMount // Optional nix socket mount + DeskrunCache *ClusterMount // Optional deskrun cache mount + DockerSocket *ClusterMount // Optional docker socket mount } -// Mount represents a host-to-container mount configuration -type Mount struct { +// ClusterMount represents a host-to-container mount configuration for cluster nodes +type ClusterMount struct { HostPath string // Host path to mount from ContainerPath string // Container path to mount to } -// NixMount is deprecated: use Mount instead +// NixMount is deprecated: use ClusterMount instead // Kept for backward compatibility -type NixMount = Mount +type NixMount = ClusterMount diff --git a/pkg/types/types_test.go b/pkg/types/types_test.go index 54449e6..1011592 100644 --- a/pkg/types/types_test.go +++ b/pkg/types/types_test.go @@ -130,3 +130,142 @@ func TestClusterConfig(t *testing.T) { t.Errorf("Network = %v, want test-network", config.Network) } } + +func TestMountTypeConstants(t *testing.T) { + tests := []struct { + name string + mountType MountType + want string + }{ + { + name: "directory or create", + mountType: MountTypeDirectoryOrCreate, + want: "DirectoryOrCreate", + }, + { + name: "directory", + mountType: MountTypeDirectory, + want: "Directory", + }, + { + name: "socket", + mountType: MountTypeSocket, + want: "Socket", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if string(tt.mountType) != tt.want { + t.Errorf("MountType = %v, want %v", tt.mountType, tt.want) + } + }) + } +} + +func TestMount(t *testing.T) { + tests := []struct { + name string + mount Mount + verify func(*testing.T, Mount) + }{ + { + name: "directory or create mount with auto-generated source", + mount: Mount{ + Source: "", + Target: "/nix/store", + Type: MountTypeDirectoryOrCreate, + }, + verify: func(t *testing.T, m Mount) { + if m.Target != "/nix/store" { + t.Errorf("Target = %v, want /nix/store", m.Target) + } + if m.Type != MountTypeDirectoryOrCreate { + t.Errorf("Type = %v, want DirectoryOrCreate", m.Type) + } + }, + }, + { + name: "socket mount", + mount: Mount{ + Source: "/var/run/docker.sock", + Target: "/var/run/docker.sock", + Type: MountTypeSocket, + }, + verify: func(t *testing.T, m Mount) { + if m.Source != "/var/run/docker.sock" { + t.Errorf("Source = %v, want /var/run/docker.sock", m.Source) + } + if m.Target != "/var/run/docker.sock" { + t.Errorf("Target = %v, want /var/run/docker.sock", m.Target) + } + if m.Type != MountTypeSocket { + t.Errorf("Type = %v, want Socket", m.Type) + } + }, + }, + { + name: "directory mount with explicit source", + mount: Mount{ + Source: "/host/path", + Target: "/container/path", + Type: MountTypeDirectory, + }, + verify: func(t *testing.T, m Mount) { + if m.Source != "/host/path" { + t.Errorf("Source = %v, want /host/path", m.Source) + } + if m.Target != "/container/path" { + t.Errorf("Target = %v, want /container/path", m.Target) + } + if m.Type != MountTypeDirectory { + t.Errorf("Type = %v, want Directory", m.Type) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.verify(t, tt.mount) + }) + } +} + +func TestRunnerInstallationWithMounts(t *testing.T) { + installation := &RunnerInstallation{ + Name: "test-runner", + Repository: "https://github.com/owner/repo", + ContainerMode: ContainerModeKubernetes, + MinRunners: 1, + MaxRunners: 5, + Instances: 1, + Mounts: []Mount{ + { + Source: "/var/run/docker.sock", + Target: "/var/run/docker.sock", + Type: MountTypeSocket, + }, + { + Source: "/tmp/cache", + Target: "/cache", + Type: MountTypeDirectoryOrCreate, + }, + }, + AuthType: AuthTypePAT, + AuthValue: "ghp_test", + } + + if installation.Name != "test-runner" { + t.Errorf("Name = %v, want test-runner", installation.Name) + } + if len(installation.Mounts) != 2 { + t.Errorf("Mounts length = %v, want 2", len(installation.Mounts)) + } + if installation.Mounts[0].Type != MountTypeSocket { + t.Errorf("First mount Type = %v, want Socket", installation.Mounts[0].Type) + } + if installation.Mounts[1].Type != MountTypeDirectoryOrCreate { + t.Errorf("Second mount Type = %v, want DirectoryOrCreate", installation.Mounts[1].Type) + } +}