11package config
22
33import (
4+ "context"
5+ "errors"
6+ "io/fs"
47 "sync"
8+ "time"
59
10+ "github.com/fsnotify/fsnotify"
611 "github.com/spf13/pflag"
712
813 "github.com/superfly/fly-go/tokens"
914 "github.com/superfly/flyctl/internal/env"
15+ "github.com/superfly/flyctl/internal/flag/flagctx"
1016 "github.com/superfly/flyctl/internal/flag/flagnames"
17+ "github.com/superfly/flyctl/internal/task"
1118)
1219
1320const (
@@ -47,7 +54,12 @@ const (
4754//
4855// Instances of Config are safe for concurrent use.
4956type Config struct {
50- mu sync.RWMutex
57+ mu sync.RWMutex
58+ path string
59+
60+ watchOnce sync.Once
61+ watchErr error
62+ subs map [chan * Config ]struct {}
5163
5264 // APIBaseURL denotes the base URL of the API.
5365 APIBaseURL string
@@ -93,22 +105,34 @@ type Config struct {
93105 MetricsToken string
94106}
95107
96- // New returns a new instance of Config populated with default values.
97- func New () * Config {
98- return & Config {
108+ func Load (ctx context.Context , path string ) (* Config , error ) {
109+ cfg := & Config {
99110 APIBaseURL : defaultAPIBaseURL ,
100111 FlapsBaseURL : defaultFlapsBaseURL ,
101112 RegistryHost : defaultRegistryHost ,
102113 MetricsBaseURL : defaultMetricsBaseURL ,
103114 Tokens : new (tokens.Tokens ),
104115 }
116+
117+ // Apply config from the config file, if it exists
118+ if err := cfg .applyFile (path ); err != nil && ! errors .Is (err , fs .ErrNotExist ) {
119+ return nil , err
120+ }
121+
122+ // Apply config from the environment, overriding anything from the file
123+ cfg .applyEnv ()
124+
125+ // Finally, apply command line options, overriding any previous setting
126+ cfg .applyFlags (flagctx .FromContext (ctx ))
127+
128+ return cfg , nil
105129}
106130
107- // ApplyEnv sets the properties of cfg which may be set via environment
131+ // applyEnv sets the properties of cfg which may be set via environment
108132// variables to the values these variables contain.
109133//
110- // ApplyEnv does not change the dirty state of config.
111- func (cfg * Config ) ApplyEnv () {
134+ // applyEnv does not change the dirty state of config.
135+ func (cfg * Config ) applyEnv () {
112136 cfg .mu .Lock ()
113137 defer cfg .mu .Unlock ()
114138
@@ -131,12 +155,14 @@ func (cfg *Config) ApplyEnv() {
131155 cfg .SendMetrics = env .IsTruthy (SendMetricsEnvKey ) || cfg .SendMetrics
132156}
133157
134- // ApplyFile sets the properties of cfg which may be set via configuration file
158+ // applyFile sets the properties of cfg which may be set via configuration file
135159// to the values the file at the given path contains.
136- func (cfg * Config ) ApplyFile (path string ) (err error ) {
160+ func (cfg * Config ) applyFile (path string ) (err error ) {
137161 cfg .mu .Lock ()
138162 defer cfg .mu .Unlock ()
139163
164+ cfg .path = path
165+
140166 var w struct {
141167 AccessToken string `yaml:"access_token"`
142168 MetricsToken string `yaml:"metrics_token"`
@@ -158,9 +184,9 @@ func (cfg *Config) ApplyFile(path string) (err error) {
158184 return
159185}
160186
161- // ApplyFlags sets the properties of cfg which may be set via command line flags
187+ // applyFlags sets the properties of cfg which may be set via command line flags
162188// to the values the flags of the given FlagSet may contain.
163- func (cfg * Config ) ApplyFlags (fs * pflag.FlagSet ) {
189+ func (cfg * Config ) applyFlags (fs * pflag.FlagSet ) {
164190 cfg .mu .Lock ()
165191 defer cfg .mu .Unlock ()
166192
@@ -188,6 +214,117 @@ func (cfg *Config) MetricsBaseURLIsProduction() bool {
188214 return cfg .MetricsBaseURL == defaultMetricsBaseURL
189215}
190216
217+ func (cfg * Config ) Watch (ctx context.Context ) (chan * Config , error ) {
218+ cfg .watchOnce .Do (func () {
219+ watch , err := fsnotify .NewWatcher ()
220+ if err != nil {
221+ cfg .watchErr = err
222+ return
223+ }
224+
225+ if err := watch .Add (cfg .path ); err != nil {
226+ cfg .watchErr = err
227+ return
228+ }
229+
230+ cfg .subs = make (map [chan * Config ]struct {})
231+
232+ task .FromContext (ctx ).Run (func (ctx context.Context ) {
233+ ctx , cancel := context .WithCancel (ctx )
234+ defer cancel ()
235+
236+ cleanupDone := make (chan struct {})
237+ defer func () { <- cleanupDone }()
238+
239+ go func () {
240+ defer close (cleanupDone )
241+
242+ <- ctx .Done ()
243+
244+ cfg .mu .Lock ()
245+ defer cfg .mu .Unlock ()
246+
247+ cfg .watchErr = errors .Join (cfg .watchErr , ctx .Err (), watch .Close ())
248+
249+ for sub := range cfg .subs {
250+ close (sub )
251+ }
252+ cfg .subs = nil
253+ }()
254+
255+ for {
256+ select {
257+ case e , open := <- watch .Events :
258+ if ! open {
259+ return
260+ }
261+
262+ if ! e .Has (fsnotify .Write ) {
263+ continue
264+ }
265+
266+ go cfg .notifySubs (ctx )
267+ case err := <- watch .Errors :
268+ cfg .mu .Lock ()
269+ defer cfg .mu .Unlock ()
270+
271+ cfg .watchErr = errors .Join (cfg .watchErr , err )
272+
273+ return
274+ case <- ctx .Done ():
275+ return
276+ }
277+ }
278+ })
279+ })
280+
281+ cfg .mu .Lock ()
282+ defer cfg .mu .Unlock ()
283+
284+ if cfg .watchErr != nil {
285+ return nil , cfg .watchErr
286+ }
287+
288+ sub := make (chan * Config )
289+ cfg .subs [sub ] = struct {}{}
290+
291+ return sub , nil
292+ }
293+
294+ func (cfg * Config ) Unwatch (sub chan * Config ) {
295+ cfg .mu .Lock ()
296+ defer cfg .mu .Unlock ()
297+
298+ if cfg .subs != nil {
299+ delete (cfg .subs , sub )
300+ close (sub )
301+ }
302+ }
303+
304+ func (cfg * Config ) notifySubs (ctx context.Context ) {
305+ newCfg , err := Load (ctx , cfg .path )
306+ if err != nil {
307+ return
308+ }
309+
310+ cfg .mu .RLock ()
311+ defer cfg .mu .RUnlock ()
312+
313+ // just in case we have a slow subscriber
314+ timer := time .NewTimer (100 * time .Millisecond )
315+ defer timer .Stop ()
316+
317+ for sub := range cfg .subs {
318+ select {
319+ case sub <- newCfg :
320+ case <- timer .C :
321+ return
322+ case <- ctx .Done ():
323+ return
324+ }
325+ }
326+ }
327+
191328func applyStringFlags (fs * pflag.FlagSet , flags map [string ]* string ) {
192329 for name , dst := range flags {
193330 if ! fs .Changed (name ) {
0 commit comments