@@ -5,11 +5,78 @@ import (
55 "fmt"
66 "os"
77 "path/filepath"
8+ "regexp"
9+ "strconv"
10+ "strings"
811
912 devboxerr "github.com/junixlabs/devbox/internal/errors"
1013 "gopkg.in/yaml.v3"
1114)
1215
16+ // memPattern matches Docker-compatible memory strings like "512m", "4g".
17+ var memPattern = regexp .MustCompile (`^[0-9]+[gGmM]$` )
18+
19+ // Resources defines CPU and memory limits for a workspace container.
20+ type Resources struct {
21+ CPUs float64 `yaml:"cpus,omitempty"`
22+ Memory string `yaml:"memory,omitempty"`
23+ }
24+
25+ // IsZero returns true if no resource limits are configured.
26+ func (r Resources ) IsZero () bool {
27+ return r .CPUs == 0 && r .Memory == ""
28+ }
29+
30+ // Validate checks that resource values are sensible.
31+ func (r Resources ) Validate () error {
32+ if r .CPUs < 0 {
33+ return fmt .Errorf ("resources.cpus must be positive, got %g" , r .CPUs )
34+ }
35+ if r .Memory != "" && ! memPattern .MatchString (r .Memory ) {
36+ return fmt .Errorf ("resources.memory must match <number>(g|m), got %q" , r .Memory )
37+ }
38+ return nil
39+ }
40+
41+ // ParseMemoryBytes converts a Docker memory string like "4g" or "512m" to bytes.
42+ func ParseMemoryBytes (mem string ) (int64 , error ) {
43+ if mem == "" {
44+ return 0 , nil
45+ }
46+ mem = strings .ToLower (mem )
47+ suffix := mem [len (mem )- 1 ]
48+ num , err := strconv .ParseInt (mem [:len (mem )- 1 ], 10 , 64 )
49+ if err != nil {
50+ return 0 , fmt .Errorf ("invalid memory value %q: %w" , mem , err )
51+ }
52+ switch suffix {
53+ case 'g' :
54+ return num * 1024 * 1024 * 1024 , nil
55+ case 'm' :
56+ return num * 1024 * 1024 , nil
57+ default :
58+ return 0 , fmt .Errorf ("unsupported memory suffix %q in %q" , string (suffix ), mem )
59+ }
60+ }
61+
62+ // MergeResources returns a Resources where workspace overrides take precedence
63+ // over server defaults for each field that is set.
64+ func MergeResources (serverDefaults , workspaceOverride * Resources ) Resources {
65+ result := Resources {}
66+ if serverDefaults != nil {
67+ result = * serverDefaults
68+ }
69+ if workspaceOverride != nil {
70+ if workspaceOverride .CPUs > 0 {
71+ result .CPUs = workspaceOverride .CPUs
72+ }
73+ if workspaceOverride .Memory != "" {
74+ result .Memory = workspaceOverride .Memory
75+ }
76+ }
77+ return result
78+ }
79+
1380// DevboxConfig represents the per-project devbox.yaml configuration.
1481type DevboxConfig struct {
1582 Name string `yaml:"name"`
@@ -18,7 +85,8 @@ type DevboxConfig struct {
1885 Branch string `yaml:"branch,omitempty"`
1986 Services []string `yaml:"services,omitempty"`
2087 Ports map [string ]int `yaml:"ports,omitempty"`
21- Env map [string ]string `yaml:"env,omitempty"`
88+ Env map [string ]string `yaml:"env,omitempty"`
89+ Resources * Resources `yaml:"resources,omitempty"`
2290}
2391
2492// DefaultConfigFile is the default config filename looked up in the project root.
@@ -60,9 +128,64 @@ func Load(path string) (*DevboxConfig, error) {
60128 )
61129 }
62130
131+ if cfg .Resources != nil {
132+ if err := cfg .Resources .Validate (); err != nil {
133+ return nil , devboxerr .NewConfigError (
134+ fmt .Sprintf ("config file %s: %v" , path , err ),
135+ "Example: resources: {cpus: 2, memory: 4g}" ,
136+ nil ,
137+ )
138+ }
139+ }
140+
63141 return & cfg , nil
64142}
65143
144+ // ServerDefaults holds per-server default settings.
145+ type ServerDefaults struct {
146+ Resources Resources `yaml:"resources"`
147+ }
148+
149+ // GlobalConfig represents the user-level ~/.devbox/config.yaml.
150+ type GlobalConfig struct {
151+ Servers map [string ]ServerDefaults `yaml:"servers"`
152+ }
153+
154+ // LoadGlobal reads the global config from ~/.devbox/config.yaml.
155+ // Returns an empty config (not an error) if the file doesn't exist.
156+ func LoadGlobal () (* GlobalConfig , error ) {
157+ home , err := os .UserHomeDir ()
158+ if err != nil {
159+ return & GlobalConfig {}, nil
160+ }
161+ path := filepath .Join (home , ".devbox" , "config.yaml" )
162+ data , err := os .ReadFile (path )
163+ if err != nil {
164+ return & GlobalConfig {}, nil
165+ }
166+ var gc GlobalConfig
167+ if err := yaml .Unmarshal (data , & gc ); err != nil {
168+ return nil , devboxerr .NewConfigError (
169+ fmt .Sprintf ("failed to parse global config %s" , path ),
170+ "Check YAML syntax in ~/.devbox/config.yaml" ,
171+ err ,
172+ )
173+ }
174+ return & gc , nil
175+ }
176+
177+ // ServerResourceDefaults returns the resource defaults for a given server,
178+ // or nil if no defaults are configured.
179+ func (gc * GlobalConfig ) ServerResourceDefaults (server string ) * Resources {
180+ if gc == nil || gc .Servers == nil {
181+ return nil
182+ }
183+ if sd , ok := gc .Servers [server ]; ok {
184+ return & sd .Resources
185+ }
186+ return nil
187+ }
188+
66189// LoadFromDir looks for devbox.yaml in the given directory and loads it.
67190// If devbox.yaml is not found, it falls back to .devcontainer/devcontainer.json.
68191func LoadFromDir (dir string ) (* DevboxConfig , error ) {
0 commit comments