burrow/services/forgejo-nsc/internal/config/config.go
Conrad Kramer c47f0e6bea
Some checks failed
Build Rust / Cargo Test (push) Failing after 9s
Build Site / Next.js Build (push) Failing after 8s
Build Apple / Build App (iOS Simulator) (push) Has been cancelled
Build Apple / Build App (macOS) (push) Has been cancelled
Enable Nix and refresh linux cache volumes
2026-03-19 04:18:38 -07:00

245 lines
7.1 KiB
Go

package config
import (
"errors"
"fmt"
"os"
"strings"
"time"
"gopkg.in/yaml.v3"
"github.com/burrow/forgejo-nsc/internal/forgejo"
)
// Duration wraps time.Duration to support YAML unmarshalling from strings.
type Duration struct {
time.Duration
}
// UnmarshalYAML implements yaml.v3 unmarshalling for Duration.
func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
switch value.Tag {
case "!!int":
var seconds int64
if err := value.Decode(&seconds); err != nil {
return err
}
d.Duration = time.Duration(seconds) * time.Second
return nil
default:
parsed, err := time.ParseDuration(value.Value)
if err != nil {
return err
}
d.Duration = parsed
return nil
}
}
// MarshalYAML implements yaml.v3 marshalling.
func (d Duration) MarshalYAML() (any, error) {
return d.Duration.String(), nil
}
type Config struct {
Listen string `yaml:"listen"`
Forgejo ForgejoConfig `yaml:"forgejo"`
Namespace NamespaceConfig `yaml:"namespace"`
Runner RunnerConfig `yaml:"runner"`
}
type CacheVolumeConfig struct {
Tag string `yaml:"tag"`
MountPoint string `yaml:"mount_point"`
SizeGb int64 `yaml:"size_gb"`
}
type ForgejoConfig struct {
BaseURL string `yaml:"base_url"`
// InstanceURL is the URL runners should use when registering with Forgejo.
// This must be reachable from the spawned runner (e.g. the public URL like
// https://git.burrow.net), and may differ from BaseURL (which can be a local
// loopback URL on the forge host).
InstanceURL string `yaml:"instance_url"`
Token string `yaml:"token"`
DefaultScope ScopeConfig `yaml:"default_scope"`
DefaultLabels []string `yaml:"default_labels"`
Timeout Duration `yaml:"timeout"`
ExtraHeaders yaml.Node `yaml:"extra_headers"`
}
type ScopeConfig struct {
Level string `yaml:"level"`
Owner string `yaml:"owner,omitempty"`
Name string `yaml:"name,omitempty"`
}
type NamespaceConfig struct {
NSCBinary string `yaml:"nsc_binary"`
// ComputeBaseURL is the Namespace Cloud Compute API endpoint (Connect RPC base URL).
// This is used for macOS runners, since NSC "run" is container-based (Linux-only).
// Example: "https://ord4.compute.namespaceapis.com"
ComputeBaseURL string `yaml:"compute_base_url"`
Image string `yaml:"image"`
MachineType string `yaml:"machine_type"`
// MacosBaseImageID selects which macOS base image to use (e.g. "tahoe").
MacosBaseImageID string `yaml:"macos_base_image_id"`
// MacosMachineArch is the architecture used for macOS instances (typically "arm64").
MacosMachineArch string `yaml:"macos_machine_arch"`
Duration Duration `yaml:"duration"`
WorkDir string `yaml:"workdir"`
MaxParallel int64 `yaml:"max_parallel"`
Environment []string `yaml:"environment"`
AllowLabels []string `yaml:"allow_labels"`
AllowScopes []string `yaml:"allow_scopes"`
Network string `yaml:"network"`
InstanceTags []string `yaml:"instance_tags"`
LinuxCachePath string `yaml:"linux_cache_path"`
LinuxCacheVolumes []CacheVolumeConfig `yaml:"linux_cache_volumes"`
MacosCachePath string `yaml:"macos_cache_path"`
MacosCacheVolumes []CacheVolumeConfig `yaml:"macos_cache_volumes"`
}
type RunnerConfig struct {
NamePrefix string `yaml:"name_prefix"`
Executor string `yaml:"executor"`
}
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
if err := cfg.Validate(); err != nil {
return nil, err
}
return &cfg, nil
}
func (c *Config) Validate() error {
if c.Listen == "" {
c.Listen = ":8080"
}
if c.Runner.NamePrefix == "" {
c.Runner.NamePrefix = "nscloud-"
}
if c.Runner.Executor == "" {
c.Runner.Executor = "shell"
}
if c.Forgejo.BaseURL == "" {
return errors.New("forgejo.base_url is required")
}
if c.Forgejo.InstanceURL == "" {
// Backwards-compatible default: assume runners can reach the same URL.
c.Forgejo.InstanceURL = c.Forgejo.BaseURL
}
if c.Forgejo.Token == "" {
return errors.New("forgejo.token is required")
}
if c.Forgejo.Timeout.Duration == 0 {
c.Forgejo.Timeout.Duration = 30 * time.Second
}
if _, err := c.Forgejo.DefaultScope.ToScope(); err != nil {
return err
}
if c.Namespace.NSCBinary == "" {
c.Namespace.NSCBinary = "nsc"
}
if c.Namespace.Image == "" {
c.Namespace.Image = "code.forgejo.org/forgejo/runner:11"
}
if c.Namespace.MacosBaseImageID == "" {
c.Namespace.MacosBaseImageID = "tahoe"
}
if c.Namespace.MacosMachineArch == "" {
c.Namespace.MacosMachineArch = "arm64"
}
if c.Namespace.Duration.Duration == 0 {
c.Namespace.Duration.Duration = 30 * time.Minute
}
if c.Namespace.MaxParallel <= 0 {
c.Namespace.MaxParallel = 4
}
if c.Namespace.LinuxCachePath == "" {
c.Namespace.LinuxCachePath = "/var/cache/burrow"
}
if len(c.Namespace.LinuxCacheVolumes) == 0 {
c.Namespace.LinuxCacheVolumes = []CacheVolumeConfig{
{
Tag: "burrow-forgejo-linux-nix-v2",
MountPoint: "/nix",
SizeGb: 80,
},
{
Tag: "burrow-forgejo-linux-cache-v2",
MountPoint: c.Namespace.LinuxCachePath,
SizeGb: 80,
},
}
}
if c.Namespace.MacosCachePath == "" {
c.Namespace.MacosCachePath = "/Users/runner/.cache/burrow"
}
if len(c.Namespace.MacosCacheVolumes) == 0 {
c.Namespace.MacosCacheVolumes = []CacheVolumeConfig{
{
Tag: "burrow-forgejo-macos-shared-v1",
MountPoint: c.Namespace.MacosCachePath + "/shared",
SizeGb: 80,
},
{
Tag: "burrow-forgejo-macos-macos-v1",
MountPoint: c.Namespace.MacosCachePath + "/lane/macos",
SizeGb: 80,
},
{
Tag: "burrow-forgejo-macos-ios-simulator-v1",
MountPoint: c.Namespace.MacosCachePath + "/lane/ios-simulator",
SizeGb: 80,
},
}
}
for _, volume := range append(append([]CacheVolumeConfig{}, c.Namespace.LinuxCacheVolumes...), c.Namespace.MacosCacheVolumes...) {
if strings.TrimSpace(volume.Tag) == "" {
return errors.New("namespace cache volume tag is required")
}
if strings.TrimSpace(volume.MountPoint) == "" {
return fmt.Errorf("namespace cache volume %q mount_point is required", volume.Tag)
}
if volume.SizeGb <= 0 {
return fmt.Errorf("namespace cache volume %q size_gb must be positive", volume.Tag)
}
}
return nil
}
func (s ScopeConfig) ToScope() (forgejo.Scope, error) {
level := forgejo.ScopeLevel(strings.ToLower(s.Level))
switch level {
case forgejo.ScopeInstance:
return forgejo.Scope{Level: level}, nil
case forgejo.ScopeOrganization:
if s.Owner == "" {
return forgejo.Scope{}, errors.New("forgejo default scope requires owner for organization level")
}
return forgejo.Scope{Level: level, Owner: s.Owner}, nil
case forgejo.ScopeRepository:
if s.Owner == "" || s.Name == "" {
return forgejo.Scope{}, errors.New("forgejo default scope requires owner and name for repository level")
}
return forgejo.Scope{Level: level, Owner: s.Owner, Name: s.Name}, nil
default:
return forgejo.Scope{}, fmt.Errorf("unknown scope level %q", s.Level)
}
}