mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
Merge 9a302b698f into 7e75035cb6
This commit is contained in:
commit
39ad886efa
17 changed files with 944 additions and 0 deletions
|
|
@ -1711,6 +1711,36 @@ components:
|
|||
description: Map of all available LDAP attributes
|
||||
additionalProperties:
|
||||
type: string
|
||||
ConfigChange:
|
||||
type: object
|
||||
description: A single setting change between two configuration versions.
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: The dot-separated path of the changed setting.
|
||||
old_value:
|
||||
description: The previous value of the setting. Only present when detailed diffs are requested.
|
||||
new_value:
|
||||
description: The new value of the setting. Only present when detailed diffs are requested.
|
||||
ConfigListItem:
|
||||
type: object
|
||||
description: Metadata about a stored configuration entry.
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: The unique identifier of the configuration.
|
||||
create_at:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The time in milliseconds the configuration was created.
|
||||
active:
|
||||
type: boolean
|
||||
description: Whether this is the currently active configuration.
|
||||
changes:
|
||||
type: array
|
||||
description: The list of setting changes from the previous configuration.
|
||||
items:
|
||||
$ref: "#/components/schemas/ConfigChange"
|
||||
Config:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
|||
|
|
@ -768,6 +768,87 @@
|
|||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
/api/v4/config/list:
|
||||
get:
|
||||
tags:
|
||||
- system
|
||||
summary: List configuration history
|
||||
description: |
|
||||
Retrieve a list of previous configurations with optional diffs between them.
|
||||
|
||||
##### Permissions
|
||||
Must have `manage_system` permission.
|
||||
operationId: ListConfigurations
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
description: The number of configuration history entries to return. Default is 5, maximum is 100.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 5
|
||||
- name: include_diffs
|
||||
in: query
|
||||
description: Whether to include diffs between configurations. Set to "true" to include.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Configuration history retrieval successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ConfigListItem"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalServerError"
|
||||
/api/v4/config/rollback:
|
||||
post:
|
||||
tags:
|
||||
- system
|
||||
summary: Rollback to a previous configuration
|
||||
description: |
|
||||
Rollback the server configuration to a previous version identified by its config ID.
|
||||
|
||||
##### Permissions
|
||||
Must have `manage_system` permission.
|
||||
operationId: RollbackConfig
|
||||
requestBody:
|
||||
description: The config ID to rollback to
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
config_id:
|
||||
type: string
|
||||
description: The ID of the configuration to rollback to
|
||||
required:
|
||||
- config_id
|
||||
responses:
|
||||
"200":
|
||||
description: Configuration rollback successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Config"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
/api/v4/config/patch:
|
||||
put:
|
||||
tags:
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ func (api *API) InitConfig() {
|
|||
api.BaseRoutes.APIRoot.Handle("/config/reload", api.APISessionRequired(configReload)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/client", api.APIHandler(getClientConfig)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/environment", api.APISessionRequired(getEnvironmentConfig)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/list", api.APISessionRequired(listConfigurations)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/rollback", api.APISessionRequired(rollbackConfig)).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -458,3 +460,85 @@ func makeFilterConfigByPermission(accessType filterType) func(c *Context, struct
|
|||
return c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
|
||||
}
|
||||
}
|
||||
|
||||
func listConfigurations(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventListConfigurations, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if limit <= 0 {
|
||||
limit = 5
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
includeDiffs := r.URL.Query().Get("include_diffs")
|
||||
|
||||
items, appErr := c.App.ListConfigurations(limit, includeDiffs)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if c.App.Channels().License().IsCloud() {
|
||||
model.FilterCloudRestrictedChanges(items)
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if err := json.NewEncoder(w).Encode(items); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func rollbackConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventRollbackConfig, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
var body struct {
|
||||
ConfigID string `json:"config_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.ConfigID == "" {
|
||||
c.SetInvalidParamWithErr("config_id", err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddMeta("config_id", body.ConfigID)
|
||||
|
||||
_, newCfg, appErr := c.App.RollbackConfig(body.ConfigID)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
c.App.SanitizedConfig(newCfg)
|
||||
|
||||
auditRec.Success()
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if c.App.Channels().License().IsCloud() {
|
||||
js, err := newCfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("rollbackConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(js); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(newCfg); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ func (api *API) InitConfigLocal() {
|
|||
api.BaseRoutes.APIRoot.Handle("/config/reload", api.APILocal(configReload)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/migrate", api.APILocal(localMigrateConfig)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/client", api.APILocal(localGetClientConfig)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/list", api.APILocal(localListConfigurations)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.APIRoot.Handle("/config/rollback", api.APILocal(localRollbackConfig)).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
func localGetConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -199,3 +201,57 @@ func localGetClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func localListConfigurations(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventListConfigurations, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if limit <= 0 {
|
||||
limit = 5
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
includeDiffs := r.URL.Query().Get("include_diffs")
|
||||
|
||||
items, appErr := c.App.ListConfigurations(limit, includeDiffs)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if err := json.NewEncoder(w).Encode(items); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func localRollbackConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventRollbackConfig, model.AuditStatusFail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
var body struct {
|
||||
ConfigID string `json:"config_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.ConfigID == "" {
|
||||
c.SetInvalidParamWithErr("config_id", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, newCfg, appErr := c.App.RollbackConfig(body.ConfigID)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
c.App.SanitizedConfig(newCfg)
|
||||
|
||||
auditRec.Success()
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if err := json.NewEncoder(w).Encode(newCfg); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ package app
|
|||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
stderrors "errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
|
@ -244,6 +247,27 @@ func (a *App) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bo
|
|||
return a.Srv().platform.SaveConfig(newCfg, sendConfigChangeClusterMessage)
|
||||
}
|
||||
|
||||
// ListConfigurations returns metadata for stored configuration entries with optional diffs.
|
||||
func (a *App) ListConfigurations(limit int, includeDiffs string) ([]*model.ConfigListItem, *model.AppError) {
|
||||
items, err := a.Srv().platform.ListConfigurations(limit, includeDiffs)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("ListConfigurations", "api.config.list_configurations.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// RollbackConfig restores a historical configuration identified by its ID.
|
||||
func (a *App) RollbackConfig(id string) (*model.Config, *model.Config, *model.AppError) {
|
||||
historicalCfg, err := a.Srv().platform.GetConfigByID(id)
|
||||
if err != nil {
|
||||
if stderrors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil, model.NewAppError("RollbackConfig", "api.config.rollback_config.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
}
|
||||
return nil, nil, model.NewAppError("RollbackConfig", "api.config.rollback_config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return a.SaveConfig(historicalCfg, true)
|
||||
}
|
||||
|
||||
func (a *App) HandleMessageExportConfig(cfg *model.Config, appCfg *model.Config) {
|
||||
// If the Message Export feature has been toggled in the System Console, rewrite the ExportFromTimestamp field to an
|
||||
// appropriate value. The rewriting occurs here to ensure it doesn't affect values written to the config file
|
||||
|
|
|
|||
|
|
@ -151,6 +151,16 @@ func (ps *PlatformService) CleanUpConfig() error {
|
|||
return ps.configStore.CleanUp()
|
||||
}
|
||||
|
||||
// ListConfigurations delegates to the config store to retrieve configuration history.
|
||||
func (ps *PlatformService) ListConfigurations(limit int, includeDiffs string) ([]*model.ConfigListItem, error) {
|
||||
return ps.configStore.ListConfigurations(limit, includeDiffs)
|
||||
}
|
||||
|
||||
// GetConfigByID delegates to the config store to retrieve a configuration by its ID.
|
||||
func (ps *PlatformService) GetConfigByID(id string) (*model.Config, error) {
|
||||
return ps.configStore.GetConfigByID(id)
|
||||
}
|
||||
|
||||
// ConfigureLogger applies the specified configuration to a logger.
|
||||
func (ps *PlatformService) ConfigureLogger(name string, logger *mlog.Logger, logSettings *model.LogSettings, getPath func(string) string) error {
|
||||
// Advanced logging is E20 only, however logging must be initialized before the license
|
||||
|
|
|
|||
|
|
@ -102,6 +102,8 @@ type Client interface {
|
|||
PatchConfig(context.Context, *model.Config) (*model.Config, *model.Response, error)
|
||||
ReloadConfig(ctx context.Context) (*model.Response, error)
|
||||
MigrateConfig(ctx context.Context, from, to string) (*model.Response, error)
|
||||
ListConfigurations(ctx context.Context, limit int, includeDiffs string) ([]*model.ConfigListItem, *model.Response, error)
|
||||
RollbackConfig(ctx context.Context, configID string) (*model.Config, *model.Response, error)
|
||||
SyncLdap(ctx context.Context) (*model.Response, error)
|
||||
MigrateIdLdap(ctx context.Context, toAttribute string) (*model.Response, error)
|
||||
GetUsers(ctx context.Context, page, perPage int, etag string) ([]*model.User, *model.Response, error)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||
|
|
@ -129,6 +130,24 @@ var ConfigExportCmd = &cobra.Command{
|
|||
RunE: withClient(configExportCmdF),
|
||||
}
|
||||
|
||||
var ConfigListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List stored configurations",
|
||||
Long: "Lists recent configuration entries from the database-backed config store, showing Id, creation time, and active status.",
|
||||
Example: " config list\n config list --limit 10",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: withClient(configListCmdF),
|
||||
}
|
||||
|
||||
var ConfigRollbackCmd = &cobra.Command{
|
||||
Use: "rollback [config_id]",
|
||||
Short: "Rollback to a previous configuration",
|
||||
Long: "Restores a previous configuration by ID. Use 'config list' to find available config IDs. This creates a new config entry preserving the full audit trail.",
|
||||
Example: " config rollback abc123def456ghi789jkl0mn",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: withClient(configRollbackCmdF),
|
||||
}
|
||||
|
||||
func init() {
|
||||
ConfigResetCmd.Flags().Bool("confirm", false, "confirm you really want to reset all configuration settings to its default value")
|
||||
|
||||
|
|
@ -140,6 +159,10 @@ func init() {
|
|||
ConfigExportCmd.Flags().Bool("remove-masked", true, "remove masked values from the exported configuration")
|
||||
ConfigExportCmd.Flags().Bool("remove-defaults", false, "remove default values from the exported configuration")
|
||||
|
||||
ConfigListCmd.Flags().Int("limit", 5, "Maximum number of configurations to list")
|
||||
ConfigListCmd.Flags().Bool("detailed", false, "Show old and new values for each change")
|
||||
ConfigListCmd.Flags().Bool("no-delta", false, "Skip delta computation, show only config metadata")
|
||||
|
||||
ConfigCmd.AddCommand(
|
||||
ConfigGetCmd,
|
||||
ConfigSetCmd,
|
||||
|
|
@ -151,6 +174,8 @@ func init() {
|
|||
ConfigMigrateCmd,
|
||||
ConfigSubpathCmd,
|
||||
ConfigExportCmd,
|
||||
ConfigListCmd,
|
||||
ConfigRollbackCmd,
|
||||
)
|
||||
RootCmd.AddCommand(ConfigCmd)
|
||||
}
|
||||
|
|
@ -611,3 +636,105 @@ func configExportCmdF(c client.Client, cmd *cobra.Command, _ []string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func configListCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
limit, _ := cmd.Flags().GetInt("limit")
|
||||
detailed, _ := cmd.Flags().GetBool("detailed")
|
||||
noDelta, _ := cmd.Flags().GetBool("no-delta")
|
||||
|
||||
includeDiffs := "true"
|
||||
if detailed {
|
||||
includeDiffs = "detailed"
|
||||
}
|
||||
if noDelta {
|
||||
includeDiffs = ""
|
||||
}
|
||||
|
||||
items, _, err := c.ListConfigurations(context.TODO(), limit, includeDiffs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list configurations: %w", err)
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
if printer.GetFormat() == printer.FormatJSON {
|
||||
printer.Print([]any{})
|
||||
} else {
|
||||
printer.Print("No configurations found.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// "%-26s %s %-8s " = Id(26) + 2 + date(20) + 2 + status(8) + 2
|
||||
const prefix = "%-26s %s %-8s"
|
||||
const padWidth = 26 + 2 + 20 + 2 + 8 + 2
|
||||
pad := strings.Repeat(" ", padWidth)
|
||||
|
||||
for _, item := range items {
|
||||
created := time.UnixMilli(item.CreateAt).UTC().Format(time.RFC3339)
|
||||
status := "inactive"
|
||||
if item.Active {
|
||||
status = "active"
|
||||
}
|
||||
header := fmt.Sprintf(prefix, item.Id, created, status)
|
||||
|
||||
if printer.GetFormat() != printer.FormatJSON && len(item.Changes) > 0 {
|
||||
for i, change := range item.Changes {
|
||||
var changeLine string
|
||||
if detailed {
|
||||
oldStr := formatConfigValue(change.OldValue)
|
||||
newStr := formatConfigValue(change.NewValue)
|
||||
if oldStr == model.FakeSetting || newStr == model.FakeSetting {
|
||||
changeLine = fmt.Sprintf("%s: [redacted]", change.Path)
|
||||
} else {
|
||||
changeLine = fmt.Sprintf("%s: %s -> %s", change.Path, oldStr, newStr)
|
||||
}
|
||||
} else {
|
||||
changeLine = change.Path
|
||||
}
|
||||
if i == 0 {
|
||||
printer.PrintT(header+" "+changeLine, item)
|
||||
} else {
|
||||
printer.Print(pad + changeLine)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printer.PrintT(header, item)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatConfigValue(v any) string {
|
||||
if v == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
rv := reflect.ValueOf(v)
|
||||
switch rv.Kind() {
|
||||
case reflect.Map, reflect.Slice:
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
return string(b)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f := rv.Float()
|
||||
if f == float64(int64(f)) {
|
||||
return strconv.FormatInt(int64(f), 10)
|
||||
}
|
||||
return strconv.FormatFloat(f, 'f', -1, 64)
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func configRollbackCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
_, _, err := c.RollbackConfig(context.TODO(), args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to rollback configuration: %w", err)
|
||||
}
|
||||
|
||||
printer.Print(fmt.Sprintf("Configuration rolled back successfully to %s.", args[0]))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1025,3 +1025,198 @@ func (s *MmctlUnitTestSuite) TestConfigExportCmd() {
|
|||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestConfigListCmd() {
|
||||
newListCmd := func() *cobra.Command {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Int("limit", 5, "")
|
||||
cmd.Flags().Bool("detailed", false, "")
|
||||
cmd.Flags().Bool("no-delta", false, "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
s.Run("List configurations with deltas by default", func() {
|
||||
printer.Clean()
|
||||
items := []*model.ConfigListItem{
|
||||
{
|
||||
Id: "abc123def456ghi789jkl0mn", CreateAt: 1700000000000, Active: true,
|
||||
Changes: []model.ConfigChange{{Path: "ServiceSettings.SiteURL"}},
|
||||
},
|
||||
{Id: "xyz789abc012def345ghi6mn", CreateAt: 1699999000000, Active: false},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListConfigurations(context.TODO(), 5, "true").
|
||||
Return(items, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := configListCmdF(s.client, newListCmd(), []string{})
|
||||
s.Require().Nil(err)
|
||||
// In JSON mode (test default), changes are embedded in struct, not separate lines
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
s.Require().Len(printer.GetErrorLines(), 0)
|
||||
// Verify the changes are embedded in the first item
|
||||
item := printer.GetLines()[0].(*model.ConfigListItem)
|
||||
s.Require().Len(item.Changes, 1)
|
||||
s.Require().Equal("ServiceSettings.SiteURL", item.Changes[0].Path)
|
||||
})
|
||||
|
||||
s.Run("List configurations with custom limit", func() {
|
||||
printer.Clean()
|
||||
items := []*model.ConfigListItem{
|
||||
{Id: "abc123def456ghi789jkl0mn", CreateAt: 1700000000000, Active: true},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListConfigurations(context.TODO(), 10, "true").
|
||||
Return(items, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := newListCmd()
|
||||
_ = cmd.Flags().Set("limit", "10")
|
||||
|
||||
err := configListCmdF(s.client, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
})
|
||||
|
||||
s.Run("List configurations returns error", func() {
|
||||
printer.Clean()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListConfigurations(context.TODO(), 5, "true").
|
||||
Return(nil, &model.Response{}, errors.New("database error")).
|
||||
Times(1)
|
||||
|
||||
err := configListCmdF(s.client, newListCmd(), []string{})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), "unable to list configurations")
|
||||
})
|
||||
|
||||
s.Run("No configurations found", func() {
|
||||
printer.Clean()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListConfigurations(context.TODO(), 5, "true").
|
||||
Return([]*model.ConfigListItem{}, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := configListCmdF(s.client, newListCmd(), []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
})
|
||||
|
||||
s.Run("Detailed mode shows old and new values", func() {
|
||||
printer.Clean()
|
||||
items := []*model.ConfigListItem{
|
||||
{
|
||||
Id: "abc123def456ghi789jkl0mn", CreateAt: 1700000000000, Active: true,
|
||||
Changes: []model.ConfigChange{
|
||||
{Path: "ServiceSettings.SiteURL", OldValue: "", NewValue: "https://mm.example.com"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListConfigurations(context.TODO(), 5, "detailed").
|
||||
Return(items, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := newListCmd()
|
||||
_ = cmd.Flags().Set("detailed", "true")
|
||||
|
||||
err := configListCmdF(s.client, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
// In JSON mode, changes embedded in struct
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
item := printer.GetLines()[0].(*model.ConfigListItem)
|
||||
s.Require().Len(item.Changes, 1)
|
||||
s.Require().Equal("https://mm.example.com", item.Changes[0].NewValue)
|
||||
})
|
||||
|
||||
s.Run("No-delta takes precedence over detailed", func() {
|
||||
printer.Clean()
|
||||
items := []*model.ConfigListItem{
|
||||
{Id: "abc123def456ghi789jkl0mn", CreateAt: 1700000000000, Active: true},
|
||||
{Id: "xyz789abc012def345ghi6mn", CreateAt: 1699999000000, Active: false},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListConfigurations(context.TODO(), 5, "").
|
||||
Return(items, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := newListCmd()
|
||||
_ = cmd.Flags().Set("detailed", "true")
|
||||
_ = cmd.Flags().Set("no-delta", "true")
|
||||
|
||||
err := configListCmdF(s.client, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
// Verify no changes are present on either item
|
||||
for _, line := range printer.GetLines() {
|
||||
item := line.(*model.ConfigListItem)
|
||||
s.Require().Empty(item.Changes)
|
||||
}
|
||||
})
|
||||
|
||||
s.Run("No-delta mode skips diffs", func() {
|
||||
printer.Clean()
|
||||
items := []*model.ConfigListItem{
|
||||
{Id: "abc123def456ghi789jkl0mn", CreateAt: 1700000000000, Active: true},
|
||||
{Id: "xyz789abc012def345ghi6mn", CreateAt: 1699999000000, Active: false},
|
||||
}
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
ListConfigurations(context.TODO(), 5, "").
|
||||
Return(items, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := newListCmd()
|
||||
_ = cmd.Flags().Set("no-delta", "true")
|
||||
|
||||
err := configListCmdF(s.client, cmd, []string{})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 2)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MmctlUnitTestSuite) TestConfigRollbackCmd() {
|
||||
s.Run("Successful rollback", func() {
|
||||
printer.Clean()
|
||||
cfg := &model.Config{}
|
||||
cfg.SetDefaults()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RollbackConfig(context.TODO(), "abc123def456ghi789jkl0mn").
|
||||
Return(cfg, &model.Response{}, nil).
|
||||
Times(1)
|
||||
|
||||
err := configRollbackCmdF(s.client, &cobra.Command{}, []string{"abc123def456ghi789jkl0mn"})
|
||||
s.Require().Nil(err)
|
||||
s.Require().Len(printer.GetLines(), 1)
|
||||
s.Require().Contains(printer.GetLines()[0], "rolled back successfully")
|
||||
})
|
||||
|
||||
s.Run("Rollback returns error", func() {
|
||||
printer.Clean()
|
||||
|
||||
s.client.
|
||||
EXPECT().
|
||||
RollbackConfig(context.TODO(), "nonexistent").
|
||||
Return(nil, &model.Response{}, errors.New("configuration not found")).
|
||||
Times(1)
|
||||
|
||||
err := configRollbackCmdF(s.client, &cobra.Command{}, []string{"nonexistent"})
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Contains(err.Error(), "unable to rollback configuration")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1691,6 +1691,22 @@ func (mr *MockClientMockRecorder) ListCommands(arg0, arg1, arg2 interface{}) *go
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCommands", reflect.TypeOf((*MockClient)(nil).ListCommands), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// ListConfigurations mocks base method.
|
||||
func (m *MockClient) ListConfigurations(arg0 context.Context, arg1 int, arg2 string) ([]*model.ConfigListItem, *model.Response, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListConfigurations", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].([]*model.ConfigListItem)
|
||||
ret1, _ := ret[1].(*model.Response)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// ListConfigurations indicates an expected call of ListConfigurations.
|
||||
func (mr *MockClientMockRecorder) ListConfigurations(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListConfigurations", reflect.TypeOf((*MockClient)(nil).ListConfigurations), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// ListExports mocks base method.
|
||||
func (m *MockClient) ListExports(arg0 context.Context) ([]string, *model.Response, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
@ -2218,6 +2234,22 @@ func (mr *MockClientMockRecorder) RevokeUserAccessToken(arg0, arg1 interface{})
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeUserAccessToken", reflect.TypeOf((*MockClient)(nil).RevokeUserAccessToken), arg0, arg1)
|
||||
}
|
||||
|
||||
// RollbackConfig mocks base method.
|
||||
func (m *MockClient) RollbackConfig(arg0 context.Context, arg1 string) (*model.Config, *model.Response, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RollbackConfig", arg0, arg1)
|
||||
ret0, _ := ret[0].(*model.Config)
|
||||
ret1, _ := ret[1].(*model.Response)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// RollbackConfig indicates an expected call of RollbackConfig.
|
||||
func (mr *MockClientMockRecorder) RollbackConfig(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RollbackConfig", reflect.TypeOf((*MockClient)(nil).RollbackConfig), arg0, arg1)
|
||||
}
|
||||
|
||||
// SearchTeams mocks base method.
|
||||
func (m *MockClient) SearchTeams(arg0 context.Context, arg1 *model.TeamSearch) ([]*model.Team, *model.Response, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
|||
|
|
@ -62,6 +62,11 @@ func SetFormat(t string) {
|
|||
printer.Format = t
|
||||
}
|
||||
|
||||
// GetFormat returns the current output format
|
||||
func GetFormat() string {
|
||||
return printer.Format
|
||||
}
|
||||
|
||||
func SetCommand(cmd *cobra.Command) {
|
||||
printer.cmd = cmd
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
|
@ -350,3 +351,153 @@ func (ds *DatabaseStore) cleanUp(thresholdCreateAt int64) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *DatabaseStore) listConfigurations(limit int, includeDiffs string) ([]*model.ConfigListItem, error) {
|
||||
type configListRow struct {
|
||||
Id string `db:"id"`
|
||||
CreateAt int64 `db:"createat"`
|
||||
Active bool `db:"active"`
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT Id, CreateAt, COALESCE(Active, false) AS Active
|
||||
FROM Configurations
|
||||
ORDER BY Active DESC NULLS LAST, CreateAt DESC
|
||||
LIMIT $1
|
||||
`
|
||||
|
||||
// When diffs are enabled, fetch one extra predecessor row so the oldest
|
||||
// visible item can be diffed against its predecessor.
|
||||
queryLimit := limit
|
||||
if includeDiffs != "" {
|
||||
queryLimit = limit + 1
|
||||
}
|
||||
|
||||
var rows []configListRow
|
||||
if err := ds.db.Select(&rows, query, queryLimit); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to list configurations")
|
||||
}
|
||||
|
||||
// Build the full set for diffing, but only return up to limit items
|
||||
allItems := make([]*model.ConfigListItem, len(rows))
|
||||
for i, row := range rows {
|
||||
allItems[i] = &model.ConfigListItem{
|
||||
Id: row.Id,
|
||||
CreateAt: row.CreateAt,
|
||||
Active: row.Active,
|
||||
}
|
||||
}
|
||||
|
||||
// Items to return (capped at limit)
|
||||
items := allItems
|
||||
if len(items) > limit {
|
||||
items = allItems[:limit]
|
||||
}
|
||||
|
||||
if includeDiffs == "" || len(allItems) <= 1 {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// Fetch config values for all rows (including the extra predecessor)
|
||||
// and compute diffs between consecutive entries.
|
||||
ids := make([]string, len(allItems))
|
||||
for i, item := range allItems {
|
||||
ids[i] = item.Id
|
||||
}
|
||||
|
||||
values, err := ds.getConfigValuesByIDs(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal configs and apply SetDefaults() so that diffs only reflect
|
||||
// intentional user changes, not new fields introduced by server upgrades.
|
||||
// Without SetDefaults(), upgrading the server would cause every new config
|
||||
// field to appear as a diff (nil -> default value), which is noise.
|
||||
configs := make(map[string]*model.Config, len(values))
|
||||
for id, raw := range values {
|
||||
cfg := &model.Config{}
|
||||
if err := json.Unmarshal(raw, cfg); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal config %s", id)
|
||||
}
|
||||
cfg.SetDefaults()
|
||||
configs[id] = cfg
|
||||
}
|
||||
|
||||
// Sort chronologically (oldest first) for diffing, using allItems
|
||||
// so the extra predecessor is included in the diff computation.
|
||||
sorted := make([]*model.ConfigListItem, len(allItems))
|
||||
copy(sorted, allItems)
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].CreateAt < sorted[j].CreateAt
|
||||
})
|
||||
|
||||
detailed := includeDiffs == "detailed"
|
||||
changesMap := make(map[string][]model.ConfigChange)
|
||||
for i := 1; i < len(sorted); i++ {
|
||||
prevCfg := configs[sorted[i-1].Id]
|
||||
currCfg := configs[sorted[i].Id]
|
||||
if prevCfg == nil || currCfg == nil {
|
||||
continue
|
||||
}
|
||||
diffs, diffErr := Diff(prevCfg, currCfg)
|
||||
if diffErr != nil {
|
||||
continue
|
||||
}
|
||||
diffs = diffs.Sanitize()
|
||||
|
||||
changes := make([]model.ConfigChange, len(diffs))
|
||||
for j, d := range diffs {
|
||||
changes[j] = model.ConfigChange{Path: d.Path}
|
||||
if detailed {
|
||||
changes[j].OldValue = d.BaseVal
|
||||
changes[j].NewValue = d.ActualVal
|
||||
}
|
||||
}
|
||||
changesMap[sorted[i].Id] = changes
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if c, ok := changesMap[item.Id]; ok {
|
||||
item.Changes = c
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (ds *DatabaseStore) getConfigValuesByIDs(ids []string) (map[string][]byte, error) {
|
||||
query, args, err := sqlx.In("SELECT Id, Value FROM Configurations WHERE Id IN (?)", ids)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to build query")
|
||||
}
|
||||
query = ds.db.Rebind(query)
|
||||
|
||||
type row struct {
|
||||
Id string `db:"id"`
|
||||
Value []byte `db:"value"`
|
||||
}
|
||||
var rows []row
|
||||
if err := ds.db.Select(&rows, query, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to fetch configuration values")
|
||||
}
|
||||
|
||||
result := make(map[string][]byte, len(rows))
|
||||
for _, r := range rows {
|
||||
result[r.Id] = r.Value
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ds *DatabaseStore) getConfigByID(id string) (*model.Config, error) {
|
||||
var value []byte
|
||||
if err := ds.db.Get(&value, "SELECT Value FROM Configurations WHERE Id = $1", id); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to fetch configuration %s", id)
|
||||
}
|
||||
|
||||
cfg := &model.Config{}
|
||||
if err := json.Unmarshal(value, cfg); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal configuration %s", id)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -411,3 +411,25 @@ func (s *Store) CleanUp() error {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ListConfigurations returns metadata for recent configurations.
|
||||
// This only works with DatabaseStore; returns an error for FileStore.
|
||||
func (s *Store) ListConfigurations(limit int, includeDiffs string) ([]*model.ConfigListItem, error) {
|
||||
switch bs := s.backingStore.(type) {
|
||||
case *DatabaseStore:
|
||||
return bs.listConfigurations(limit, includeDiffs)
|
||||
default:
|
||||
return nil, errors.New("listing configurations is only supported with database-backed config stores")
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfigByID retrieves a historical configuration by its ID.
|
||||
// This only works with DatabaseStore; returns an error for FileStore.
|
||||
func (s *Store) GetConfigByID(id string) (*model.Config, error) {
|
||||
switch bs := s.backingStore.(type) {
|
||||
case *DatabaseStore:
|
||||
return bs.getConfigByID(id)
|
||||
default:
|
||||
return nil, errors.New("retrieving configurations by ID is only supported with database-backed config stores")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1897,6 +1897,18 @@
|
|||
"id": "api.config.get_config.restricted_merge.app_error",
|
||||
"translation": "Failed to merge given config."
|
||||
},
|
||||
{
|
||||
"id": "api.config.list_configurations.app_error",
|
||||
"translation": "Unable to list configurations."
|
||||
},
|
||||
{
|
||||
"id": "api.config.rollback_config.app_error",
|
||||
"translation": "Unable to rollback configuration."
|
||||
},
|
||||
{
|
||||
"id": "api.config.rollback_config.not_found.app_error",
|
||||
"translation": "Configuration not found."
|
||||
},
|
||||
{
|
||||
"id": "api.config.migrate_config.app_error",
|
||||
"translation": "Failed to migrate config store."
|
||||
|
|
|
|||
|
|
@ -137,7 +137,9 @@ const (
|
|||
const (
|
||||
AuditEventConfigReload = "configReload" // reload server configuration
|
||||
AuditEventGetConfig = "getConfig" // get current server configuration
|
||||
AuditEventListConfigurations = "listConfigurations" // list stored configuration history
|
||||
AuditEventLocalGetClientConfig = "localGetClientConfig" // get client configuration locally
|
||||
AuditEventRollbackConfig = "rollbackConfig" // rollback to a previous configuration
|
||||
AuditEventLocalGetConfig = "localGetConfig" // get server configuration locally
|
||||
AuditEventLocalPatchConfig = "localPatchConfig" // update server configuration locally
|
||||
AuditEventLocalUpdateConfig = "localUpdateConfig" // update server configuration locally
|
||||
|
|
|
|||
|
|
@ -4451,6 +4451,34 @@ func (c *Client4) ReloadConfig(ctx context.Context) (*Response, error) {
|
|||
return BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// ListConfigurations returns metadata for stored configuration entries.
|
||||
func (c *Client4) ListConfigurations(ctx context.Context, limit int, includeDiffs string) ([]*ConfigListItem, *Response, error) {
|
||||
query := url.Values{}
|
||||
if limit > 0 {
|
||||
query.Set("limit", strconv.Itoa(limit))
|
||||
}
|
||||
if includeDiffs != "" {
|
||||
query.Set("include_diffs", includeDiffs)
|
||||
}
|
||||
r, err := c.doAPIGetWithQuery(ctx, c.configRoute().Join("list"), query, "")
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
return DecodeJSONFromResponse[[]*ConfigListItem](r)
|
||||
}
|
||||
|
||||
// RollbackConfig restores a historical configuration by ID.
|
||||
func (c *Client4) RollbackConfig(ctx context.Context, configID string) (*Config, *Response, error) {
|
||||
body := map[string]string{"config_id": configID}
|
||||
r, err := c.doAPIPostJSON(ctx, c.configRoute().Join("rollback"), body)
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
return DecodeJSONFromResponse[*Config](r)
|
||||
}
|
||||
|
||||
// GetClientConfig will retrieve the parts of the server configuration needed by the client.
|
||||
func (c *Client4) GetClientConfig(ctx context.Context, etag string) (map[string]string, *Response, error) {
|
||||
r, err := c.doAPIGet(ctx, c.configRoute().Join("client"), etag)
|
||||
|
|
|
|||
|
|
@ -5359,6 +5359,89 @@ type GetConfigOptions struct {
|
|||
RemoveDefaults bool
|
||||
}
|
||||
|
||||
// ConfigChange represents a single setting change between two configuration versions.
|
||||
type ConfigChange struct {
|
||||
Path string `json:"path"`
|
||||
OldValue any `json:"old_value,omitempty"`
|
||||
NewValue any `json:"new_value,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigListItem represents metadata about a stored configuration entry.
|
||||
type ConfigListItem struct {
|
||||
Id string `json:"id"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
Active bool `json:"active"`
|
||||
Changes []ConfigChange `json:"changes,omitempty"`
|
||||
}
|
||||
|
||||
// collectTaggedPaths walks a struct type and returns all dot-separated field
|
||||
// paths that carry the given tag value in the specified tag key.
|
||||
func collectTaggedPaths(t reflect.Type, tagKey, tagValue, prefix string) map[string]bool {
|
||||
paths := map[string]bool{}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
fieldPath := field.Name
|
||||
if prefix != "" {
|
||||
fieldPath = prefix + "." + field.Name
|
||||
}
|
||||
|
||||
tags := strings.Split(field.Tag.Get(tagKey), ",")
|
||||
if isTagPresent(tagValue, tags) {
|
||||
paths[fieldPath] = true
|
||||
continue
|
||||
}
|
||||
|
||||
ft := field.Type
|
||||
if ft.Kind() == reflect.Ptr {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
if ft.Kind() == reflect.Struct {
|
||||
for p := range collectTaggedPaths(ft, tagKey, tagValue, fieldPath) {
|
||||
paths[p] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// CloudRestrictedPaths returns the set of config field paths tagged as cloud_restrictable.
|
||||
func CloudRestrictedPaths() map[string]bool {
|
||||
return collectTaggedPaths(reflect.TypeFor[Config](), ConfigAccessTagType, ConfigAccessTagCloudRestrictable, "")
|
||||
}
|
||||
|
||||
// isCloudRestricted checks whether a change path matches or is a descendant
|
||||
// of any cloud-restrictable config field path (e.g. "SqlSettings.ReplicaLagSettings"
|
||||
// also matches "SqlSettings.ReplicaLagSettings[0].DataSource").
|
||||
func isCloudRestricted(path string, restricted map[string]bool) bool {
|
||||
if restricted[path] {
|
||||
return true
|
||||
}
|
||||
for rp := range restricted {
|
||||
if strings.HasPrefix(path, rp+".") || strings.HasPrefix(path, rp+"[") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FilterCloudRestrictedChanges removes changes whose paths match or descend from
|
||||
// cloud-restrictable config fields.
|
||||
func FilterCloudRestrictedChanges(items []*ConfigListItem) {
|
||||
restricted := CloudRestrictedPaths()
|
||||
for _, item := range items {
|
||||
if len(item.Changes) == 0 {
|
||||
continue
|
||||
}
|
||||
filtered := make([]ConfigChange, 0, len(item.Changes))
|
||||
for _, ch := range item.Changes {
|
||||
if !isCloudRestricted(ch.Path, restricted) {
|
||||
filtered = append(filtered, ch)
|
||||
}
|
||||
}
|
||||
item.Changes = filtered
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue