| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package main |
| 4 | |
| 5 | import ( |
| 6 | "os" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/spf13/cobra" |
| 10 | "github.com/spf13/pflag" |
| 11 | |
| 12 | infralog "github.com/tenseleyFlow/shithub/internal/infra/log" |
| 13 | runnerpkg "github.com/tenseleyFlow/shithub/internal/runner" |
| 14 | runnerapi "github.com/tenseleyFlow/shithub/internal/runner/api" |
| 15 | runnerconfig "github.com/tenseleyFlow/shithub/internal/runner/config" |
| 16 | "github.com/tenseleyFlow/shithub/internal/runner/engine" |
| 17 | "github.com/tenseleyFlow/shithub/internal/runner/workspace" |
| 18 | "github.com/tenseleyFlow/shithub/internal/version" |
| 19 | ) |
| 20 | |
| 21 | var runConfigPath string |
| 22 | |
| 23 | var runCmd = &cobra.Command{ |
| 24 | Use: "run", |
| 25 | Short: "Claim and execute shithub Actions jobs", |
| 26 | RunE: func(cmd *cobra.Command, _ []string) error { |
| 27 | cfg, err := runnerconfig.Load(runnerconfig.LoadOptions{ |
| 28 | ConfigPath: runConfigPath, |
| 29 | Overrides: flagOverrides(cmd), |
| 30 | }) |
| 31 | if err != nil { |
| 32 | return err |
| 33 | } |
| 34 | logger := infralog.New(infralog.Options{ |
| 35 | Level: cfg.Log.Level, |
| 36 | Format: cfg.Log.Format, |
| 37 | Writer: os.Stderr, |
| 38 | }).With("component", "runner") |
| 39 | |
| 40 | workspaces := workspace.New(cfg.Runner.WorkspaceRoot) |
| 41 | removed, err := workspaces.Sweep(cfg.Runner.WorkspaceTTL, time.Now().UTC()) |
| 42 | if err != nil { |
| 43 | return err |
| 44 | } |
| 45 | if removed > 0 { |
| 46 | logger.InfoContext(cmd.Context(), "swept stale workspaces", "count", removed) |
| 47 | } |
| 48 | |
| 49 | client, err := runnerapi.New(runnerapi.Config{ |
| 50 | BaseURL: cfg.Server.BaseURL, |
| 51 | RunnerToken: cfg.Runner.Token, |
| 52 | }) |
| 53 | if err != nil { |
| 54 | return err |
| 55 | } |
| 56 | hostName, err := os.Hostname() |
| 57 | if err != nil { |
| 58 | hostName = "" |
| 59 | } |
| 60 | execEngine := engine.NewDocker(engine.DockerConfig{ |
| 61 | Binary: cfg.Engine.Kind, |
| 62 | DefaultImage: cfg.Engine.DefaultImage, |
| 63 | Network: cfg.Engine.Network, |
| 64 | Memory: cfg.Engine.Memory, |
| 65 | CPUs: cfg.Engine.CPUs, |
| 66 | SeccompProfile: cfg.Engine.SeccompProfile, |
| 67 | User: cfg.Engine.User, |
| 68 | PidsLimit: cfg.Engine.PidsLimit, |
| 69 | DNSServers: cfg.Engine.DNSServers, |
| 70 | Stdout: os.Stdout, |
| 71 | Stderr: os.Stderr, |
| 72 | Logger: logger, |
| 73 | }) |
| 74 | r := runnerpkg.New(runnerpkg.Options{ |
| 75 | API: client, |
| 76 | Engine: execEngine, |
| 77 | Workspaces: workspaces, |
| 78 | Logger: logger, |
| 79 | Labels: cfg.Runner.Labels, |
| 80 | Capacity: cfg.Runner.Capacity, |
| 81 | HostName: hostName, |
| 82 | Version: version.String(), |
| 83 | PollInterval: cfg.Runner.PollInterval, |
| 84 | DefaultImage: cfg.Engine.DefaultImage, |
| 85 | Clock: func() time.Time { return time.Now().UTC() }, |
| 86 | }) |
| 87 | return r.Run(cmd.Context()) |
| 88 | }, |
| 89 | } |
| 90 | |
| 91 | func init() { |
| 92 | runCmd.Flags().StringVar(&runConfigPath, "config", "", "Path to runner config file") |
| 93 | runCmd.Flags().String("server-url", "", "shithub base URL") |
| 94 | runCmd.Flags().String("token", "", "Runner registration token") |
| 95 | runCmd.Flags().String("labels", "", "Comma-separated runner labels") |
| 96 | runCmd.Flags().Int("capacity", 0, "Maximum concurrent jobs this runner advertises") |
| 97 | runCmd.Flags().Duration("poll-interval", 0, "Idle heartbeat interval") |
| 98 | runCmd.Flags().String("workspace-root", "", "Workspace root directory") |
| 99 | runCmd.Flags().Duration("workspace-ttl", 0, "Startup sweep TTL for stale workspaces") |
| 100 | runCmd.Flags().String("engine", "", "Execution engine: docker or podman") |
| 101 | runCmd.Flags().String("image", "", "Default container image") |
| 102 | runCmd.Flags().String("network", "", "Container network") |
| 103 | runCmd.Flags().String("memory", "", "Container memory limit") |
| 104 | runCmd.Flags().String("cpus", "", "Container CPU limit") |
| 105 | runCmd.Flags().String("seccomp-profile", "", "Container seccomp profile path") |
| 106 | runCmd.Flags().String("container-user", "", "Default container user") |
| 107 | runCmd.Flags().Int("pids-limit", 0, "Container PID limit") |
| 108 | runCmd.Flags().String("network-allowlist", "", "Comma-separated host patterns allowed by the runner DNS policy") |
| 109 | runCmd.Flags().String("dns-servers", "", "Comma-separated DNS servers passed to step containers") |
| 110 | runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error") |
| 111 | runCmd.Flags().String("log-format", "", "Log format: text or json") |
| 112 | } |
| 113 | |
| 114 | func flagOverrides(cmd *cobra.Command) map[string]string { |
| 115 | keys := map[string]string{ |
| 116 | "server-url": "server.base_url", |
| 117 | "token": "runner.token", |
| 118 | "labels": "runner.labels", |
| 119 | "capacity": "runner.capacity", |
| 120 | "poll-interval": "runner.poll_interval", |
| 121 | "workspace-root": "runner.workspace_root", |
| 122 | "workspace-ttl": "runner.workspace_ttl", |
| 123 | "engine": "engine.kind", |
| 124 | "image": "engine.default_image", |
| 125 | "network": "engine.network", |
| 126 | "memory": "engine.memory", |
| 127 | "cpus": "engine.cpus", |
| 128 | "seccomp-profile": "engine.seccomp_profile", |
| 129 | "container-user": "engine.user", |
| 130 | "pids-limit": "engine.pids_limit", |
| 131 | "network-allowlist": "runner.network_allowlist", |
| 132 | "dns-servers": "engine.dns_servers", |
| 133 | "log-level": "log.level", |
| 134 | "log-format": "log.format", |
| 135 | } |
| 136 | out := make(map[string]string) |
| 137 | cmd.Flags().Visit(func(f *pflag.Flag) { |
| 138 | if key, ok := keys[f.Name]; ok { |
| 139 | out[key] = f.Value.String() |
| 140 | } |
| 141 | }) |
| 142 | return out |
| 143 | } |
| 144 |