allow overriding configuration with environment variables (#98) (#101)

This commit is contained in:
aler9 2020-10-13 23:36:27 +02:00
parent 810643ab30
commit 5ca5005040
4 changed files with 225 additions and 23 deletions

View File

@ -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

13
conf.go
View File

@ -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,16 +59,16 @@ 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 {
@ -86,12 +88,17 @@ func loadConf(fpath string, stdin io.Reader) (*conf, error) {
}
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"}
}

124
confenv/confenv.go Normal file
View File

@ -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())
}

View File

@ -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