[MM-59503] export: enable exporting configuration with mmctl (#28412)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ca72cdb445
Коммит
424ce2b8db
@@ -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"])
|
||||
})
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user