diff --git a/cmd/accumulated/run/marshal.go b/cmd/accumulated/run/config.go similarity index 70% rename from cmd/accumulated/run/marshal.go rename to cmd/accumulated/run/config.go index a9d4c8fb9..7d34703c9 100644 --- a/cmd/accumulated/run/marshal.go +++ b/cmd/accumulated/run/config.go @@ -9,6 +9,8 @@ package run import ( "bytes" "encoding/json" + "fmt" + "io/fs" "os" "path/filepath" "reflect" @@ -16,6 +18,7 @@ import ( "strings" "github.com/BurntSushi/toml" + "github.com/joho/godotenv" "gitlab.com/accumulatenetwork/accumulate/pkg/errors" "gopkg.in/yaml.v3" ) @@ -58,7 +61,31 @@ func (c *Config) Load(b []byte, format func([]byte, any) error) error { return err } - return json.Unmarshal(b, c) + err = json.Unmarshal(b, c) + if err != nil { + return err + } + + return c.applyDotEnv() +} + +func (c *Config) applyDotEnv() error { + if !setDefaultPtr(&c.DotEnv, false) { + return nil + } + + file := ".env" + if c.file != "" { + dir := filepath.Dir(c.file) + file = filepath.Join(dir, file) + } + + env, err := godotenv.Read(file) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return err + } + + return errors.Join(expandEnv(reflect.ValueOf(c), env)...) } func (c *Config) Save() error { @@ -165,3 +192,43 @@ func float2int(v reflect.Value) any { return v.Interface() } } + +func expandEnv(v reflect.Value, env map[string]string) (errs []error) { + switch v.Kind() { + case reflect.String: + s := v.String() + s = os.Expand(s, func(name string) string { + value, ok := env[name] + if ok { + return value + } + errs = append(errs, fmt.Errorf("%q is not defined", name)) + return fmt.Sprintf("#!MISSING(%q)", name) + }) + v.SetString(s) + + case reflect.Pointer, reflect.Interface: + errs = append(errs, expandEnv(v.Elem(), env)...) + + case reflect.Slice, reflect.Array: + for i, n := 0, v.Len(); i < n; i++ { + errs = append(errs, expandEnv(v.Index(i), env)...) + } + + case reflect.Map: + it := v.MapRange() + for it.Next() { + errs = append(errs, expandEnv(it.Key(), env)...) + errs = append(errs, expandEnv(it.Value(), env)...) + } + + case reflect.Struct: + typ := v.Type() + for i, n := 0, typ.NumField(); i < n; i++ { + if typ.Field(i).IsExported() { + errs = append(errs, expandEnv(v.Field(i), env)...) + } + } + } + return errs +} diff --git a/cmd/accumulated/run/config.yml b/cmd/accumulated/run/config.yml index 6a84f7dcd..f7b9ddc6e 100644 --- a/cmd/accumulated/run/config.yml +++ b/cmd/accumulated/run/config.yml @@ -4,6 +4,9 @@ Config: - name: file type: string marshal-as: none + - name: DotEnv + type: bool + pointer: true - name: Network type: string - name: Logging diff --git a/cmd/accumulated/run/types_gen.go b/cmd/accumulated/run/types_gen.go index d55d1c592..4543b2d5d 100644 --- a/cmd/accumulated/run/types_gen.go +++ b/cmd/accumulated/run/types_gen.go @@ -50,6 +50,7 @@ type CometPrivValFile struct { type Config struct { file string + DotEnv *bool `json:"dotEnv,omitempty" form:"dotEnv" query:"dotEnv" validate:"required"` Network string `json:"network,omitempty" form:"network" query:"network" validate:"required"` Logging *Logging `json:"logging,omitempty" form:"logging" query:"logging" validate:"required"` Instrumentation *Instrumentation `json:"instrumentation,omitempty" form:"instrumentation" query:"instrumentation" validate:"required"` @@ -359,6 +360,10 @@ func (v *CometPrivValFile) CopyAsInterface() interface{} { return v.Copy() } func (v *Config) Copy() *Config { u := new(Config) + if v.DotEnv != nil { + u.DotEnv = new(bool) + *u.DotEnv = *v.DotEnv + } u.Network = v.Network if v.Logging != nil { u.Logging = (v.Logging).Copy() @@ -976,6 +981,14 @@ func (v *CometPrivValFile) Equal(u *CometPrivValFile) bool { } func (v *Config) Equal(u *Config) bool { + switch { + case v.DotEnv == u.DotEnv: + // equal + case v.DotEnv == nil || u.DotEnv == nil: + return false + case !(*v.DotEnv == *u.DotEnv): + return false + } if !(v.Network == u.Network) { return false } @@ -1948,6 +1961,7 @@ func (v *CometPrivValFile) MarshalJSON() ([]byte, error) { func (v *Config) MarshalJSON() ([]byte, error) { u := struct { + DotEnv *bool `json:"dotEnv,omitempty"` Network string `json:"network,omitempty"` Logging *Logging `json:"logging,omitempty"` Instrumentation *Instrumentation `json:"instrumentation,omitempty"` @@ -1955,6 +1969,9 @@ func (v *Config) MarshalJSON() ([]byte, error) { Configurations *encoding.JsonUnmarshalListWith[Configuration] `json:"configurations,omitempty"` Services *encoding.JsonUnmarshalListWith[Service] `json:"services,omitempty"` }{} + if !(v.DotEnv == nil) { + u.DotEnv = v.DotEnv + } if !(len(v.Network) == 0) { u.Network = v.Network } @@ -2667,6 +2684,7 @@ func (v *CometPrivValFile) UnmarshalJSON(data []byte) error { func (v *Config) UnmarshalJSON(data []byte) error { u := struct { + DotEnv *bool `json:"dotEnv,omitempty"` Network string `json:"network,omitempty"` Logging *Logging `json:"logging,omitempty"` Instrumentation *Instrumentation `json:"instrumentation,omitempty"` @@ -2675,6 +2693,7 @@ func (v *Config) UnmarshalJSON(data []byte) error { Services *encoding.JsonUnmarshalListWith[Service] `json:"services,omitempty"` }{} + u.DotEnv = v.DotEnv u.Network = v.Network u.Logging = v.Logging u.Instrumentation = v.Instrumentation @@ -2685,6 +2704,7 @@ func (v *Config) UnmarshalJSON(data []byte) error { if err != nil { return err } + v.DotEnv = u.DotEnv v.Network = u.Network v.Logging = u.Logging v.Instrumentation = u.Instrumentation diff --git a/cmd/accumulated/run/utils.go b/cmd/accumulated/run/utils.go index 0c35bd4d0..ac062329c 100644 --- a/cmd/accumulated/run/utils.go +++ b/cmd/accumulated/run/utils.go @@ -38,11 +38,11 @@ var ( func Ptr[T any](v T) *T { return &v } -func setDefaultPtr[V any](ptr **V, def V) *V { +func setDefaultPtr[V any](ptr **V, def V) V { if *ptr == nil { *ptr = &def } - return *ptr + return **ptr } func setDefaultVal[V any](ptr *V, def V) V { diff --git a/go.mod b/go.mod index 0e82a12f1..b49ee4c60 100644 --- a/go.mod +++ b/go.mod @@ -297,6 +297,7 @@ require ( github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect github.com/jmhodges/levigo v1.0.0 // indirect + github.com/joho/godotenv v1.5.1 github.com/jonboulle/clockwork v0.3.0 // indirect github.com/julz/importas v0.1.0 // indirect github.com/kisielk/errcheck v1.7.0 // indirect diff --git a/go.sum b/go.sum index d43775785..1f9ae0157 100644 --- a/go.sum +++ b/go.sum @@ -645,6 +645,8 @@ github.com/jjti/go-spancheck v0.5.3 h1:vfq4s2IB8T3HvbpiwDTYgVPj1Ze/ZSXrTtaZRTc7C github.com/jjti/go-spancheck v0.5.3/go.mod h1:eQdOX1k3T+nAKvZDyLC3Eby0La4dZ+I19iOl5NzSPFE= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=