[MM-59503] export: enable exporting configuration with mmctl (#28412)

Этот коммит содержится в:
Ibrahim Serdar Acikgoz
2024-12-17 10:23:52 +01:00
коммит произвёл GitHub
родитель ca72cdb445
Коммит 424ce2b8db
15 изменённых файлов: 577 добавлений и 11 удалений

Просмотреть файл

@@ -4886,6 +4886,30 @@ func (c *Client4) GetConfig(ctx context.Context) (*Config, *Response, error) {
return cfg, BuildResponse(r), d.Decode(&cfg)
}
// GetConfig will retrieve the server config with some sanitized items.
func (c *Client4) GetConfigWithOptions(ctx context.Context, options GetConfigOptions) (map[string]any, *Response, error) {
v := url.Values{}
if options.RemoveDefaults {
v.Set("remove_defaults", "true")
}
if options.RemoveMasked {
v.Set("remove_masked", "true")
}
url := c.configRoute()
if len(v) > 0 {
url += "?" + v.Encode()
}
r, err := c.DoAPIGet(ctx, url, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var cfg map[string]any
return cfg, BuildResponse(r), json.NewDecoder(r.Body).Decode(&cfg)
}
// ReloadConfig will reload the server configuration.
func (c *Client4) ReloadConfig(ctx context.Context) (*Response, error) {
r, err := c.DoAPIPost(ctx, c.configRoute()+"/reload", "")

Просмотреть файл

@@ -4576,6 +4576,66 @@ func (o *Config) Sanitize(pluginManifests []*Manifest) {
o.PluginSettings.Sanitize(pluginManifests)
}
type FilterTag struct {
TagType string
TagName string
}
type ConfigFilterOptions struct {
GetConfigOptions
TagFilters []FilterTag
}
type GetConfigOptions struct {
RemoveMasked bool
RemoveDefaults bool
}
// FilterConfig returns a map[string]any representation of the configuration.
// Also, the function can filter the configuration by the options passed
// in the argument. The options are used to remove the default values, the masked
// values and to filter the configuration by the tags passed in the TagFilters.
func FilterConfig(cfg *Config, opts ConfigFilterOptions) (map[string]any, error) {
if cfg == nil {
return nil, nil
}
defaultCfg := &Config{}
defaultCfg.SetDefaults()
filteredCfg, err := cfg.StringMap()
if err != nil {
return nil, err
}
filteredDefaultCfg, err := defaultCfg.StringMap()
if err != nil {
return nil, err
}
for i := range opts.TagFilters {
filteredCfg = structToMapFilteredByTag(filteredCfg, opts.TagFilters[i].TagType, opts.TagFilters[i].TagName)
filteredDefaultCfg = structToMapFilteredByTag(defaultCfg, opts.TagFilters[i].TagType, opts.TagFilters[i].TagName)
}
if opts.RemoveDefaults {
filteredCfg = stringMapDiff(filteredCfg, filteredDefaultCfg)
}
if opts.RemoveMasked {
removeFakeSettings(filteredCfg)
}
// only apply this if we applied some filters
// the alternative is to remove empty maps and slices during the filters
// but having this in a separate step makes it easier to understand
if opts.RemoveDefaults || opts.RemoveMasked || len(opts.TagFilters) > 0 {
removeEmptyMapsAndSlices(filteredCfg)
}
return filteredCfg, nil
}
// structToMapFilteredByTag converts a struct into a map removing those fields that has the tag passed
// as argument
func structToMapFilteredByTag(t any, typeOfTag, filterTag string) map[string]any {
@@ -4625,6 +4685,90 @@ func structToMapFilteredByTag(t any, typeOfTag, filterTag string) map[string]any
return out
}
// removeEmptyMapsAndSlices removes all the empty maps and slices from a map
func removeEmptyMapsAndSlices(m map[string]any) {
for k, v := range m {
switch vt := v.(type) {
case map[string]any:
removeEmptyMapsAndSlices(vt)
if len(vt) == 0 {
delete(m, k)
}
case []any:
if len(vt) == 0 {
delete(m, k)
}
}
}
}
// StringMap returns a map[string]any representation of the Config struct
func (o *Config) StringMap() (map[string]any, error) {
b, err := json.Marshal(o)
if err != nil {
return nil, err
}
var result map[string]any
err = json.Unmarshal(b, &result)
if err != nil {
return nil, err
}
return result, nil
}
// stringMapDiff returns the difference between two maps with string keys
func stringMapDiff(m1, m2 map[string]any) map[string]any {
result := make(map[string]any)
for k, v := range m1 {
if _, ok := m2[k]; !ok {
result[k] = v // ideally this should be never reached
}
if reflect.DeepEqual(v, m2[k]) {
continue
}
switch v.(type) {
case map[string]any:
// this happens during the serialization of the struct to map
// so we can safely assume that the type is not matching, there
// is a difference in the values
casted, ok := m2[k].(map[string]any)
if !ok {
result[k] = v
continue
}
res := stringMapDiff(v.(map[string]any), casted)
if len(res) > 0 {
result[k] = res
}
default:
result[k] = v
}
}
return result
}
// removeFakeSettings removes all the fields that have the value of FakeSetting
// it's necessary to remove the fields that have been masked to be able to
// export the configuration (and make it importable)
func removeFakeSettings(m map[string]any) {
for k, v := range m {
switch vt := v.(type) {
case map[string]any:
removeFakeSettings(vt)
case string:
if v == FakeSetting {
delete(m, k)
}
}
}
}
func isTagPresent(tag string, tags []string) bool {
for _, val := range tags {
tagValue := strings.TrimSpace(val)

Просмотреть файл

@@ -1968,3 +1968,124 @@ func TestConfigDefaultConnectedWorkspacesSettings(t *testing.T) {
require.True(t, *c.ConnectedWorkspacesSettings.EnableRemoteClusterService)
})
}
func TestFilterConfig(t *testing.T) {
t.Run("should clear default values", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
m, err := FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.Empty(t, m)
cfg.ServiceSettings = ServiceSettings{
EnableLocalMode: NewPointer(true),
}
m, err = FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.NotEmpty(t, m)
require.Equal(t, true, m["ServiceSettings"].(map[string]any)["EnableLocalMode"])
})
t.Run("should clear masked config values", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
dsn := "somedb://user:password@localhost:5432/mattermost"
cfg.SqlSettings.DataSource = NewPointer(dsn)
m, err := FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.NotEmpty(t, m)
require.Equal(t, dsn, m["SqlSettings"].(map[string]any)["DataSource"])
cfg.Sanitize(nil)
m, err = FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.NotEmpty(t, m)
require.Equal(t, FakeSetting, m["SqlSettings"].(map[string]any)["DataSource"])
cfg.Sanitize(nil)
m, err = FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
RemoveMasked: true,
},
})
require.NoError(t, err)
require.Empty(t, m)
cfg.SqlSettings.DriverName = NewPointer("mysql")
m, err = FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
RemoveMasked: true,
},
})
require.NoError(t, err)
require.NotEmpty(t, m)
require.Equal(t, "mysql", m["SqlSettings"].(map[string]any)["DriverName"])
})
t.Run("should not clear non primitive types", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
cfg.TeamSettings.ExperimentalDefaultChannels = []string{"ch-a", "ch-b"}
m, err := FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.NotEmpty(t, m)
require.ElementsMatch(t, []string{"ch-a", "ch-b"}, m["TeamSettings"].(map[string]any)["ExperimentalDefaultChannels"])
})
t.Run("should be able to handle nil values", func(t *testing.T) {
var cfg *Config
m, err := FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.Empty(t, m)
})
t.Run("should be able to handle float64 values", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
cfg.PluginSettings.Plugins = map[string]map[string]any{
"com.mattermost.plugin-a": {
"setting": 1.0,
},
}
m, err := FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.Equal(t, 1.0, m["PluginSettings"].(map[string]any)["Plugins"].(map[string]any)["com.mattermost.plugin-a"].(map[string]any)["setting"])
})
}