From 5ca500504064fe389eeec02106ec4d31a50765fb Mon Sep 17 00:00:00 2001 From: aler9 <46489434+aler9@users.noreply.github.com> Date: Tue, 13 Oct 2020 23:36:27 +0200 Subject: [PATCH] allow overriding configuration with environment variables (#98) (#101) --- README.md | 7 ++- conf.go | 51 +++++++++++-------- confenv/confenv.go | 124 +++++++++++++++++++++++++++++++++++++++++++++ main_test.go | 66 ++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 23 deletions(-) create mode 100644 confenv/confenv.go diff --git a/README.md b/README.md index bcdff742..61a13ed5 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,12 @@ docker run --rm -it -v $PWD/rtsp-simple-server.yml:/rtsp-simple-server.yml -p 85 ### Configuration -To see or change the configuration, edit the `rtsp-simple-server.yml` file, provided with the executable. The default configuration is [available here](rtsp-simple-server.yml). +To see or change the configuration, edit the `rtsp-simple-server.yml` file, provided with the executable, and also [available here](rtsp-simple-server.yml). + +The configuration can be overridden with environment variables in the format `RTSP_PARAMNAME`, where `PARAMNAME` is the name of a parameter, in uppercase. For instance, the `rtspPort` parameter can be overridden in the following way: +``` +RTSP_RTSPPORT=8555 ./rtsp-simple-server +``` ### RTSP proxy mode diff --git a/conf.go b/conf.go index e8b17da3..6897857b 100644 --- a/conf.go +++ b/conf.go @@ -12,6 +12,8 @@ import ( "github.com/aler9/gortsplib" "github.com/aler9/gortsplib/headers" "gopkg.in/yaml.v2" + + "github.com/aler9/rtsp-simple-server/confenv" ) type pathConf struct { @@ -57,41 +59,46 @@ type conf struct { func loadConf(fpath string, stdin io.Reader) (*conf, error) { conf := &conf{} + // read from file or stdin err := func() error { if fpath == "stdin" { err := yaml.NewDecoder(stdin).Decode(conf) if err != nil { return err } - - return nil - - } else { - // rtsp-simple-server.yml is optional - if fpath == "rtsp-simple-server.yml" { - if _, err := os.Stat(fpath); err != nil { - return nil - } - } - - f, err := os.Open(fpath) - if err != nil { - return err - } - defer f.Close() - - err = yaml.NewDecoder(f).Decode(conf) - if err != nil { - return err - } - return nil } + + // rtsp-simple-server.yml is optional + if fpath == "rtsp-simple-server.yml" { + if _, err := os.Stat(fpath); err != nil { + return nil + } + } + + f, err := os.Open(fpath) + if err != nil { + return err + } + defer f.Close() + + err = yaml.NewDecoder(f).Decode(conf) + if err != nil { + return err + } + + return nil }() if err != nil { return nil, err } + // read from environment + err = confenv.Process("RTSP", conf) + if err != nil { + return nil, err + } + if len(conf.Protocols) == 0 { conf.Protocols = []string{"udp", "tcp"} } diff --git a/confenv/confenv.go b/confenv/confenv.go new file mode 100644 index 00000000..93e7441e --- /dev/null +++ b/confenv/confenv.go @@ -0,0 +1,124 @@ +package confenv + +import ( + "fmt" + "os" + "reflect" + "strconv" + "strings" + "time" +) + +func process(env map[string]string, envKey string, rv reflect.Value) error { + rt := rv.Type() + + switch rt { + case reflect.TypeOf(time.Duration(0)): + if ev, ok := env[envKey]; ok { + d, err := time.ParseDuration(ev) + if err != nil { + return fmt.Errorf("%s: %s", envKey, err) + } + rv.Set(reflect.ValueOf(d)) + } + return nil + } + + switch rt.Kind() { + case reflect.String: + if ev, ok := env[envKey]; ok { + rv.SetString(ev) + } + return nil + + case reflect.Int: + if ev, ok := env[envKey]; ok { + iv, err := strconv.ParseInt(ev, 10, 64) + if err != nil { + return fmt.Errorf("%s: %s", envKey, err) + } + rv.SetInt(iv) + } + return nil + + case reflect.Bool: + if ev, ok := env[envKey]; ok { + switch strings.ToLower(ev) { + case "yes", "true": + rv.SetBool(true) + + case "no", "false": + rv.SetBool(false) + + default: + return fmt.Errorf("%s: invalid value '%s'", envKey, ev) + } + } + return nil + + case reflect.Slice: + if rt.Elem().Kind() == reflect.String { + if ev, ok := env[envKey]; ok { + nv := reflect.Zero(rt) + for _, sv := range strings.Split(ev, ",") { + nv = reflect.Append(nv, reflect.ValueOf(sv)) + } + rv.Set(nv) + } + return nil + } + + case reflect.Map: + for k := range env { + if !strings.HasPrefix(k, envKey) { + continue + } + + tmp := strings.Split(strings.TrimPrefix(k[len(envKey):], "_"), "_") + mapKey := strings.ToLower(tmp[0]) + + nv := rv.MapIndex(reflect.ValueOf(mapKey)) + zero := reflect.Value{} + if nv == zero { + nv = reflect.New(rt.Elem().Elem()) + rv.SetMapIndex(reflect.ValueOf(mapKey), nv) + } + + err := process(env, envKey+"_"+strings.ToUpper(mapKey), nv.Elem()) + if err != nil { + return err + } + } + return nil + + case reflect.Struct: + flen := rt.NumField() + for i := 0; i < flen; i++ { + fieldName := rt.Field(i).Name + + // process only public fields + if fieldName[0] < 'A' || fieldName[0] > 'Z' { + continue + } + + fieldEnvKey := envKey + "_" + strings.ToUpper(fieldName) + err := process(env, fieldEnvKey, rv.Field(i)) + if err != nil { + return err + } + } + return nil + } + + return fmt.Errorf("unsupported type: %v", rt) +} + +func Process(envKey string, v interface{}) error { + env := make(map[string]string) + for _, kv := range os.Environ() { + tmp := strings.Split(kv, "=") + env[tmp[0]] = tmp[1] + } + + return process(env, envKey, reflect.ValueOf(v).Elem()) +} diff --git a/main_test.go b/main_test.go index 84fe3857..682a5f5d 100644 --- a/main_test.go +++ b/main_test.go @@ -3,12 +3,14 @@ package main import ( "bytes" "net" + "net/url" "os" "os/exec" "strconv" "testing" "time" + "github.com/aler9/gortsplib" "github.com/stretchr/testify/require" ) @@ -97,6 +99,70 @@ func (c *container) ip() string { return string(out[:len(out)-1]) } +func TestEnvironment(t *testing.T) { + // string + os.Setenv("RTSP_RUNONCONNECT", "testcmd") + defer os.Unsetenv("RTSP_RUNONCONNECT") + + // int + os.Setenv("RTSP_RTSPPORT", "8555") + defer os.Unsetenv("RTSP_RTSPPORT") + + // bool + os.Setenv("RTSP_METRICS", "yes") + defer os.Unsetenv("RTSP_METRICS") + + // duration + os.Setenv("RTSP_READTIMEOUT", "22s") + defer os.Unsetenv("RTSP_READTIMEOUT") + + // slice + os.Setenv("RTSP_LOGDESTINATIONS", "stdout,file") + defer os.Unsetenv("RTSP_LOGDESTINATIONS") + + // map key + os.Setenv("RTSP_PATHS_TEST2", "") + defer os.Unsetenv("RTSP_PATHS_TEST2") + + // map value + os.Setenv("RTSP_PATHS_TEST_SOURCE", "rtsp://testing") + defer os.Unsetenv("RTSP_PATHS_TEST_SOURCE") + os.Setenv("RTSP_PATHS_TEST_SOURCEPROTOCOL", "tcp") + defer os.Unsetenv("RTSP_PATHS_TEST_SOURCEPROTOCOL") + + p, err := newProgram([]string{}, bytes.NewBuffer(nil)) + require.NoError(t, err) + defer p.close() + + require.Equal(t, "testcmd", p.conf.RunOnConnect) + + require.Equal(t, 8555, p.conf.RtspPort) + + require.Equal(t, true, p.conf.Metrics) + + require.Equal(t, 22*time.Second, p.conf.ReadTimeout) + + require.Equal(t, []string{"stdout", "file"}, p.conf.LogDestinations) + + pa, ok := p.conf.Paths["test2"] + require.Equal(t, true, ok) + require.Equal(t, &pathConf{ + Source: "record", + }, pa) + + pa, ok = p.conf.Paths["test"] + require.Equal(t, true, ok) + require.Equal(t, &pathConf{ + Source: "rtsp://testing", + sourceUrl: func() *url.URL { + u, _ := url.Parse("rtsp://testing:554") + return u + }(), + SourceProtocol: "tcp", + sourceProtocolParsed: gortsplib.StreamProtocolTCP, + }, pa) +} + func TestPublish(t *testing.T) { for _, conf := range []struct { publishSoft string