Fix proper version handling for uploading a new config

This commit is contained in:
Ingo Oppermann
2022-10-10 16:19:45 +02:00
parent f896c1a9ac
commit 4d4e70571e
30 changed files with 2730 additions and 1420 deletions

View File

@@ -4,8 +4,16 @@ import (
"time"
"github.com/datarhei/core/v16/config"
v1config "github.com/datarhei/core/v16/config/v1"
v2config "github.com/datarhei/core/v16/config/v2"
)
// ConfigVersion is used to only unmarshal the version field in order
// find out which SetConfig should be used.
type ConfigVersion struct {
Version int64 `json:"version"`
}
// ConfigData embeds config.Data
type ConfigData struct {
config.Data
@@ -22,11 +30,68 @@ type Config struct {
Overrides []string `json:"overrides"`
}
type SetConfigV1 struct {
v1config.Data
}
// NewSetConfigV1 creates a new SetConfigV1 based on the current
// config with downgrading.
func NewSetConfigV1(cfg *config.Config) SetConfigV1 {
v2data, _ := config.DowngradeV3toV2(&cfg.Data)
v1data, _ := v2config.DowngradeV2toV1(v2data)
data := SetConfigV1{
Data: *v1data,
}
return data
}
// MergeTo merges the v1 config into the current config.
func (s *SetConfigV1) MergeTo(cfg *config.Config) {
v2data, _ := config.DowngradeV3toV2(&cfg.Data)
v2config.MergeV1ToV2(v2data, &s.Data)
config.MergeV2toV3(&cfg.Data, v2data)
}
type SetConfigV2 struct {
v2config.Data
}
// NewSetConfigV2 creates a new SetConfigV2 based on the current
// config with downgrading.
func NewSetConfigV2(cfg *config.Config) SetConfigV2 {
v2data, _ := config.DowngradeV3toV2(&cfg.Data)
data := SetConfigV2{
Data: *v2data,
}
return data
}
// MergeTo merges the v2 config into the current config.
func (s *SetConfigV2) MergeTo(cfg *config.Config) {
config.MergeV2toV3(&cfg.Data, &s.Data)
}
// SetConfig embeds config.Data. It is used to send a new config to the server.
type SetConfig struct {
config.Data
}
// NewSetConfig converts a config.Config into a SetConfig in order to prepopulate
// a SetConfig with the current values. The uploaded config can have missing fields that
// will be filled with the current values after unmarshalling the JSON.
func NewSetConfig(cfg *config.Config) SetConfig {
data := SetConfig{
cfg.Data,
}
return data
}
// MergeTo merges a sent config into a config.Config
func (rscfg *SetConfig) MergeTo(cfg *config.Config) {
cfg.ID = rscfg.ID
@@ -51,18 +116,7 @@ func (rscfg *SetConfig) MergeTo(cfg *config.Config) {
cfg.Router = rscfg.Router
}
// NewSetConfig converts a config.Config into a RestreamerSetConfig in order to prepopulate
// a RestreamerSetConfig with the current values. The uploaded config can have missing fields that
// will be filled with the current values after unmarshalling the JSON.
func NewSetConfig(cfg *config.Config) SetConfig {
data := SetConfig{
cfg.Data,
}
return data
}
// Unmarshal converts a config.Config to a RestreamerConfig.
// Unmarshal converts a config.Config to a Config.
func (c *Config) Unmarshal(cfg *config.Config) {
if cfg == nil {
return

View File

@@ -1,11 +1,13 @@
package api
import (
"io"
"net/http"
"github.com/datarhei/core/v16/config"
cfgstore "github.com/datarhei/core/v16/config/store"
cfgvars "github.com/datarhei/core/v16/config/vars"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/labstack/echo/v4"
)
@@ -13,11 +15,11 @@ import (
// The ConfigHandler type provides handler functions for reading and manipulating
// the current config.
type ConfigHandler struct {
store config.Store
store cfgstore.Store
}
// NewConfig return a new Config type. You have to provide a valid config store.
func NewConfig(store config.Store) *ConfigHandler {
func NewConfig(store cfgstore.Store) *ConfigHandler {
return &ConfigHandler{
store: store,
}
@@ -53,25 +55,73 @@ func (p *ConfigHandler) Get(c echo.Context) error {
// @Security ApiKeyAuth
// @Router /api/v3/config [put]
func (p *ConfigHandler) Set(c echo.Context) error {
cfg := p.store.Get()
version := api.ConfigVersion{}
// Set the current config as default config value. This will
// allow to set a partial config without destroying the other
// values.
setConfig := api.NewSetConfig(cfg)
req := c.Request()
if err := util.ShouldBindJSON(c, &setConfig); err != nil {
body, err := io.ReadAll(req.Body)
if err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
}
// Merge it into the current config
setConfig.MergeTo(cfg)
if err := json.Unmarshal(body, &version); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", json.FormatError(body, err))
}
cfg := p.store.Get()
// For each version, set the current config as default config value. This will
// allow to set a partial config without destroying the other values.
if version.Version == 1 {
// Downgrade to v1 in order to have a populated v1 config
v1SetConfig := api.NewSetConfigV1(cfg)
if err := json.Unmarshal(body, &v1SetConfig); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", json.FormatError(body, err))
}
if err := c.Validate(v1SetConfig); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
}
// Merge it into the current config
v1SetConfig.MergeTo(cfg)
} else if version.Version == 2 {
// Downgrade to v2 in order to have a populated v2 config
v2SetConfig := api.NewSetConfigV2(cfg)
if err := json.Unmarshal(body, &v2SetConfig); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", json.FormatError(body, err))
}
if err := c.Validate(v2SetConfig); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
}
// Merge it into the current config
v2SetConfig.MergeTo(cfg)
} else if version.Version == 3 {
v3SetConfig := api.NewSetConfig(cfg)
if err := json.Unmarshal(body, &v3SetConfig); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", json.FormatError(body, err))
}
if err := c.Validate(v3SetConfig); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
}
// Merge it into the current config
v3SetConfig.MergeTo(cfg)
} else {
return api.Err(http.StatusBadRequest, "Invalid config version", "version %d", version.Version)
}
// Now we make a copy from the config and merge it with the environment
// variables. If this configuration is valid, we will store the un-merged
// one to disk.
mergedConfig := config.NewConfigFrom(cfg)
mergedConfig := cfg.Clone()
mergedConfig.Merge()
// Validate the new merged config
@@ -79,7 +129,7 @@ func (p *ConfigHandler) Set(c echo.Context) error {
if mergedConfig.HasErrors() {
errors := make(map[string][]string)
mergedConfig.Messages(func(level string, v config.Variable, message string) {
mergedConfig.Messages(func(level string, v cfgvars.Variable, message string) {
if level != "error" {
return
}

View File

@@ -7,25 +7,28 @@ import (
"testing"
"github.com/datarhei/core/v16/config"
"github.com/datarhei/core/v16/config/store"
v1 "github.com/datarhei/core/v16/config/v1"
"github.com/datarhei/core/v16/http/mock"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/require"
)
func getDummyConfigRouter() *echo.Echo {
func getDummyConfigRouter() (*echo.Echo, store.Store) {
router := mock.DummyEcho()
config := config.NewDummyStore()
config := store.NewDummy()
handler := NewConfig(config)
router.Add("GET", "/", handler.Get)
router.Add("PUT", "/", handler.Set)
return router
return router, config
}
func TestConfigGet(t *testing.T) {
router := getDummyConfigRouter()
router, _ := getDummyConfigRouter()
mock.Request(t, http.StatusOK, router, "GET", "/", nil)
@@ -33,7 +36,7 @@ func TestConfigGet(t *testing.T) {
}
func TestConfigSetConflict(t *testing.T) {
router := getDummyConfigRouter()
router, _ := getDummyConfigRouter()
var data bytes.Buffer
@@ -44,18 +47,86 @@ func TestConfigSetConflict(t *testing.T) {
}
func TestConfigSet(t *testing.T) {
router := getDummyConfigRouter()
router, store := getDummyConfigRouter()
storedcfg := store.Get()
require.Equal(t, []string{}, storedcfg.Host.Name)
var data bytes.Buffer
encoder := json.NewEncoder(&data)
// Setting a new v3 config
cfg := config.New()
cfg.FFmpeg.Binary = "true"
cfg.DB.Dir = "."
cfg.Storage.Disk.Dir = "."
cfg.Storage.MimeTypes = ""
cfg.Storage.Disk.Cache.Types.Allow = []string{".aaa"}
cfg.Storage.Disk.Cache.Types.Block = []string{".zzz"}
cfg.Host.Name = []string{"foobar.com"}
encoder := json.NewEncoder(&data)
encoder.Encode(cfg)
mock.Request(t, http.StatusOK, router, "PUT", "/", &data)
storedcfg = store.Get()
require.Equal(t, []string{"foobar.com"}, storedcfg.Host.Name)
require.Equal(t, []string{".aaa"}, cfg.Storage.Disk.Cache.Types.Allow)
require.Equal(t, []string{".zzz"}, cfg.Storage.Disk.Cache.Types.Block)
require.Equal(t, "cert@datarhei.com", cfg.TLS.Email)
// Setting a complete v1 config
cfgv1 := v1.New()
cfgv1.FFmpeg.Binary = "true"
cfgv1.DB.Dir = "."
cfgv1.Storage.Disk.Dir = "."
cfgv1.Storage.MimeTypes = ""
cfgv1.Storage.Disk.Cache.Types = []string{".bbb"}
cfgv1.Host.Name = []string{"foobar.com"}
data.Reset()
encoder.Encode(cfgv1)
mock.Request(t, http.StatusOK, router, "PUT", "/", &data)
storedcfg = store.Get()
require.Equal(t, []string{"foobar.com"}, storedcfg.Host.Name)
require.Equal(t, []string{".bbb"}, storedcfg.Storage.Disk.Cache.Types.Allow)
require.Equal(t, []string{".zzz"}, storedcfg.Storage.Disk.Cache.Types.Block)
require.Equal(t, "cert@datarhei.com", cfg.TLS.Email)
// Setting a partial v1 config
type customconfig struct {
Version int `json:"version"`
Storage struct {
Disk struct {
Cache struct {
Types []string `json:"types"`
} `json:"cache"`
} `json:"disk"`
} `json:"storage"`
}
customcfg := customconfig{
Version: 1,
}
customcfg.Storage.Disk.Cache.Types = []string{".ccc"}
data.Reset()
encoder.Encode(customcfg)
mock.Request(t, http.StatusOK, router, "PUT", "/", &data)
storedcfg = store.Get()
require.Equal(t, []string{"foobar.com"}, storedcfg.Host.Name)
require.Equal(t, []string{".ccc"}, storedcfg.Storage.Disk.Cache.Types.Allow)
require.Equal(t, []string{".zzz"}, storedcfg.Storage.Disk.Cache.Types.Block)
require.Equal(t, "cert@datarhei.com", cfg.TLS.Email)
}

View File

@@ -32,7 +32,7 @@ import (
"net/http"
"strings"
"github.com/datarhei/core/v16/config"
cfgstore "github.com/datarhei/core/v16/config/store"
"github.com/datarhei/core/v16/http/cache"
"github.com/datarhei/core/v16/http/errorhandler"
"github.com/datarhei/core/v16/http/graph/resolver"
@@ -87,7 +87,7 @@ type Config struct {
RTMP rtmp.Server
SRT srt.Server
JWT jwt.JWT
Config config.Store
Config cfgstore.Store
Cache cache.Cacher
Sessions session.RegistryReader
Router router.Router