diff --git a/go/cmd/dolt/commands/sqlserver/command_line_config.go b/go/cmd/dolt/commands/sqlserver/command_line_config.go index 41d37130e1c..378732003d8 100755 --- a/go/cmd/dolt/commands/sqlserver/command_line_config.go +++ b/go/cmd/dolt/commands/sqlserver/command_line_config.go @@ -105,9 +105,11 @@ func NewCommandLineConfig(creds *cli.UserPassword, apr *argparser.ArgParseResult if creds == nil { if user, ok := apr.GetValue(cli.UserFlag); ok { config.withUser(user) + config.valuesSet[servercfg.UserKey] = struct{}{} } if password, ok := apr.GetValue(cli.PasswordFlag); ok { config.withPassword(password) + config.valuesSet[servercfg.PasswordKey] = struct{}{} } } else { config.withUser(creds.Username) @@ -163,7 +165,14 @@ func NewCommandLineConfig(creds *cli.UserPassword, apr *argparser.ArgParseResult } config.autoCommit = !apr.Contains(noAutoCommitFlag) + if apr.Contains(noAutoCommitFlag) { + config.valuesSet[servercfg.AutoCommitKey] = struct{}{} + } + config.allowCleartextPasswords = apr.Contains(allowCleartextPasswordsFlag) + if apr.Contains(allowCleartextPasswordsFlag) { + config.valuesSet[servercfg.AllowCleartextPasswordsKey] = struct{}{} + } if connStr, ok := apr.GetValue(goldenMysqlConn); ok { cli.Println(connStr) @@ -341,12 +350,14 @@ func (cfg *commandLineServerConfig) Socket() string { // WithHost updates the host and returns the called `*commandLineServerConfig`, which is useful for chaining calls. func (cfg *commandLineServerConfig) WithHost(host string) *commandLineServerConfig { cfg.host = host + cfg.valuesSet[servercfg.HostKey] = struct{}{} return cfg } // WithPort updates the port and returns the called `*commandLineServerConfig`, which is useful for chaining calls. func (cfg *commandLineServerConfig) WithPort(port int) *commandLineServerConfig { cfg.port = port + cfg.valuesSet[servercfg.PortKey] = struct{}{} return cfg } @@ -373,12 +384,14 @@ func (cfg *commandLineServerConfig) withTimeout(timeout uint64) *commandLineServ // withReadOnly updates the read only flag and returns the called `*commandLineServerConfig`, which is useful for chaining calls. func (cfg *commandLineServerConfig) withReadOnly(readonly bool) *commandLineServerConfig { cfg.readOnly = readonly + cfg.valuesSet[servercfg.ReadOnlyKey] = struct{}{} return cfg } // withLogLevel updates the log level and returns the called `*commandLineServerConfig`, which is useful for chaining calls. func (cfg *commandLineServerConfig) withLogLevel(loglevel servercfg.LogLevel) *commandLineServerConfig { cfg.logLevel = loglevel + cfg.valuesSet[servercfg.LogLevelKey] = struct{}{} return cfg } @@ -416,23 +429,27 @@ func (cfg *commandLineServerConfig) withBranchControlFilePath(branchControlFileP func (cfg *commandLineServerConfig) withAllowCleartextPasswords(allow bool) *commandLineServerConfig { cfg.allowCleartextPasswords = allow + cfg.valuesSet[servercfg.AllowCleartextPasswordsKey] = struct{}{} return cfg } // WithSocket updates the path to the unix socket file func (cfg *commandLineServerConfig) WithSocket(sockFilePath string) *commandLineServerConfig { cfg.socket = sockFilePath + cfg.valuesSet[servercfg.SocketKey] = struct{}{} return cfg } // WithRemotesapiPort sets the remotesapi port to use. func (cfg *commandLineServerConfig) WithRemotesapiPort(port *int) *commandLineServerConfig { cfg.remotesapiPort = port + cfg.valuesSet[servercfg.RemotesapiPortKey] = struct{}{} return cfg } func (cfg *commandLineServerConfig) WithRemotesapiReadOnly(readonly *bool) *commandLineServerConfig { cfg.remotesapiReadOnly = readonly + cfg.valuesSet[servercfg.RemotesapiReadOnlyKey] = struct{}{} return cfg } diff --git a/go/cmd/dolt/commands/sqlserver/server_test.go b/go/cmd/dolt/commands/sqlserver/server_test.go index c165b477a8f..496cb324337 100644 --- a/go/cmd/dolt/commands/sqlserver/server_test.go +++ b/go/cmd/dolt/commands/sqlserver/server_test.go @@ -17,6 +17,7 @@ package sqlserver import ( "net/http" "os" + "path/filepath" "strings" "sync" "testing" @@ -33,6 +34,7 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/sqle" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" "github.com/dolthub/dolt/go/libraries/utils/config" + "github.com/dolthub/dolt/go/libraries/utils/filesys" "github.com/dolthub/dolt/go/libraries/utils/svcs" ) @@ -510,3 +512,114 @@ func TestReadReplica(t *testing.T) { assert.ElementsMatch(t, res, []int{0}) }) } + +func TestGenerateYamlConfig(t *testing.T) { + args := []string{ + "--user", "my_name", + "--timeout", "11", + "--branch-control-file", "dir1/dir2/abc.db", + } + + privilegeFilePath, err := filepath.Localize(".doltcfg/privileges.db") + require.NoError(t, err) + + expected := `# Dolt SQL server configuration +# +# Uncomment and edit lines as necessary to modify your configuration. +# Full documentation: https://docs.dolthub.com/sql-reference/server/configuration +# + +# log_level: info + +# max_logged_query_len: 0 + +# encode_logged_query: false + +# behavior: + # read_only: false + # autocommit: true + # disable_client_multi_statements: false + # dolt_transaction_commit: false + # event_scheduler: "OFF" + +user: + name: my_name + # password: "" + +listener: + # host: localhost + # port: 3306 + # max_connections: 100 + read_timeout_millis: 11000 + write_timeout_millis: 11000 + # tls_key: key.pem + # tls_cert: cert.pem + # require_secure_transport: false + # allow_cleartext_passwords: false + # socket: /tmp/mysql.sock + +# data_dir: . + +# cfg_dir: .doltcfg + +# remotesapi: + # port: 8000 + # read_only: false + +# privilege_file: ` + privilegeFilePath + + ` + +branch_control_file: dir1/dir2/abc.db + +# user_session_vars: +# - name: root + # vars: + # dolt_log_level: warn + # dolt_show_system_tables: 1 + +# system_variables: + # dolt_log_level: info + # dolt_transaction_commit: 1 + +# jwks: [] + +# metrics: + # labels: {} + # host: localhost + # port: 9091 + +# cluster: + # standby_remotes: + # - name: standby_replica_one + # remote_url_template: https://standby_replica_one.svc.cluster.local:50051/{database} + # - name: standby_replica_two + # remote_url_template: https://standby_replica_two.svc.cluster.local:50051/{database} + # bootstrap_role: primary + # bootstrap_epoch: 1 + # remotesapi: + # address: 127.0.0.1 + # port: 50051 + # tls_key: remotesapi_key.pem + # tls_cert: remotesapi_chain.pem + # tls_ca: standby_cas.pem + # server_name_urls: + # - https://standby_replica_one.svc.cluster.local + # - https://standby_replica_two.svc.cluster.local + # server_name_dns: + # - standby_replica_one.svc.cluster.local + # - standby_replica_two.svc.cluster.local` + + ap := SqlServerCmd{}.ArgParser() + + dEnv := sqle.CreateTestEnv() + + cwd, err := os.Getwd() + require.NoError(t, err) + cwdFs, err := filesys.LocalFilesysWithWorkingDir(cwd) + require.NoError(t, err) + + serverConfig, err := ServerConfigFromArgs(ap, nil, args, dEnv, cwdFs) + require.NoError(t, err) + + assert.Equal(t, expected, generateYamlConfig(serverConfig)) +} diff --git a/go/cmd/dolt/commands/sqlserver/sqlserver.go b/go/cmd/dolt/commands/sqlserver/sqlserver.go index 1d91bb8cc0c..f6d36563ca5 100644 --- a/go/cmd/dolt/commands/sqlserver/sqlserver.go +++ b/go/cmd/dolt/commands/sqlserver/sqlserver.go @@ -18,6 +18,7 @@ import ( "context" "errors" "fmt" + "os" "path/filepath" "strings" @@ -244,6 +245,11 @@ func StartServer(ctx context.Context, versionStr, commandStr string, args []stri return err } + err = generateYamlConfigIfNone(ap, help, args, dEnv, serverConfig) + if err != nil { + return err + } + err = servercfg.ApplySystemVariables(serverConfig, sql.SystemVariables) if err != nil { return err @@ -476,8 +482,17 @@ func setupDoltConfig(dEnv *env.DoltEnv, cwd filesys.Filesys, apr *argparser.ArgP } serverConfig.withCfgDir(cfgDirPath) + if cfgDirSpecified { + serverConfig.valuesSet[servercfg.CfgDirKey] = struct{}{} + } + + if dataDirSpecified { + serverConfig.valuesSet[servercfg.DataDirKey] = struct{}{} + } + if privsFp, ok := apr.GetValue(commands.PrivsFilePathFlag); ok { serverConfig.withPrivilegeFilePath(privsFp) + serverConfig.valuesSet[servercfg.PrivilegeFilePathKey] = struct{}{} } else { path, err := dEnv.FS.Abs(filepath.Join(cfgDirPath, commands.DefaultPrivsName)) if err != nil { @@ -488,6 +503,7 @@ func setupDoltConfig(dEnv *env.DoltEnv, cwd filesys.Filesys, apr *argparser.ArgP if branchControlFilePath, ok := apr.GetValue(commands.BranchCtrlPathFlag); ok { serverConfig.withBranchControlFilePath(branchControlFilePath) + serverConfig.valuesSet[servercfg.BranchControlFilePathKey] = struct{}{} } else { path, err := dEnv.FS.Abs(filepath.Join(cfgDirPath, commands.DefaultBranchCtrlName)) if err != nil { @@ -498,3 +514,49 @@ func setupDoltConfig(dEnv *env.DoltEnv, cwd filesys.Filesys, apr *argparser.ArgP return nil } + +// generateYamlConfigIfNone creates a YAML config file in the database directory if one is not specified in the args +// and one doesn't already exist in the database directory. The fields of the YAML file are generated using the values +// in serverConfig that were explicitly set by the command line args. +func generateYamlConfigIfNone( + ap *argparser.ArgParser, + help cli.UsagePrinter, + args []string, + dEnv *env.DoltEnv, + serverConfig servercfg.ServerConfig) error { + const yamlConfigName = "config.yaml" + + apr := cli.ParseArgsOrDie(ap, args, help) + + if apr.Contains(configFileFlag) { + return nil + } + + path := filepath.Join(serverConfig.DataDir(), yamlConfigName) + exists, _ := dEnv.FS.Exists(path) + if exists { + return nil + } + + generatedYaml := generateYamlConfig(serverConfig) + + err := dEnv.FS.WriteFile(path, []byte(generatedYaml), os.ModePerm) + if err != nil { + return err + } + + return nil +} + +// generateYamlConfig returns a YAML string containing the fields in serverConfig that +// were explicitly set by the command line args, along with commented-out placeholders for any +// fields that were not explicitly set by the command line args. +func generateYamlConfig(serverConfig servercfg.ServerConfig) string { + yamlConfig := servercfg.ServerConfigSetValuesAsYAMLConfig(serverConfig) + + return `# Dolt SQL server configuration +# +# Uncomment and edit lines as necessary to modify your configuration. +# Full documentation: https://docs.dolthub.com/sql-reference/server/configuration +#` + "\n\n" + yamlConfig.VerboseString() +} diff --git a/go/libraries/doltcore/servercfg/serverconfig.go b/go/libraries/doltcore/servercfg/serverconfig.go index 725abfd14b3..2a5c1a3e2ca 100644 --- a/go/libraries/doltcore/servercfg/serverconfig.go +++ b/go/libraries/doltcore/servercfg/serverconfig.go @@ -200,6 +200,10 @@ type ServerConfig interface { // DefaultServerConfig creates a `*ServerConfig` that has all of the options set to their default values. func DefaultServerConfig() ServerConfig { + return defaultServerConfigYAML() +} + +func defaultServerConfigYAML() *YAMLConfig { return &YAMLConfig{ LogLevelStr: ptr(string(DefaultLogLevel)), MaxQueryLenInLogs: ptr(DefaultMaxLoggedQueryLen), @@ -265,10 +269,39 @@ func ValidateConfig(config ServerConfig) error { } const ( - MaxConnectionsKey = "max_connections" - ReadTimeoutKey = "net_read_timeout" - WriteTimeoutKey = "net_write_timeout" - EventSchedulerKey = "event_scheduler" + HostKey = "host" + PortKey = "port" + UserKey = "user" + PasswordKey = "password" + ReadTimeoutKey = "net_read_timeout" + WriteTimeoutKey = "net_write_timeout" + ReadOnlyKey = "read_only" + LogLevelKey = "log_level" + AutoCommitKey = "autocommit" + DoltTransactionCommitKey = "dolt_transaction_commit" + DataDirKey = "data_dir" + CfgDirKey = "cfg_dir" + MaxConnectionsKey = "max_connections" + TLSKeyKey = "tls_key" + TLSCertKey = "tls_cert" + RequireSecureTransportKey = "require_secure_transport" + MaxLoggedQueryLenKey = "max_logged_query_len" + ShouldEncodeLoggedQueryKey = "should_encode_logged_query" + DisableClientMultiStatementsKey = "disable_client_multi_statements" + MetricsLabelsKey = "metrics_labels" + MetricsHostKey = "metrics_host" + MetricsPortKey = "metrics_port" + PrivilegeFilePathKey = "privilege_file_path" + BranchControlFilePathKey = "branch_control_file_path" + UserVarsKey = "user_vars" + SystemVarsKey = "system_vars" + JwksConfigKey = "jwks_config" + AllowCleartextPasswordsKey = "allow_cleartext_passwords" + SocketKey = "socket" + RemotesapiPortKey = "remotesapi_port" + RemotesapiReadOnlyKey = "remotesapi_read_only" + ClusterConfigKey = "cluster_config" + EventSchedulerKey = "event_scheduler" ) type SystemVariableTarget interface { diff --git a/go/libraries/doltcore/servercfg/testdata/minver_validation.txt b/go/libraries/doltcore/servercfg/testdata/minver_validation.txt index fc9d38d5f1f..3c6e17e97ff 100644 --- a/go/libraries/doltcore/servercfg/testdata/minver_validation.txt +++ b/go/libraries/doltcore/servercfg/testdata/minver_validation.txt @@ -4,38 +4,46 @@ LogLevelStr *string 0.0.0 log_level,omitempty MaxQueryLenInLogs *int 0.0.0 max_logged_query_len,omitempty EncodeLoggedQuery *bool 0.0.0 encode_logged_query,omitempty -BehaviorConfig servercfg.BehaviorYAMLConfig 0.0.0 behavior --ReadOnly *bool 0.0.0 read_only --AutoCommit *bool 0.0.0 autocommit +BehaviorConfig servercfg.BehaviorYAMLConfig 0.0.0 behavior,omitempty +-ReadOnly *bool 0.0.0 read_only,omitempty +-AutoCommit *bool 0.0.0 autocommit,omitempty -PersistenceBehavior *string 0.0.0 persistence_behavior,omitempty --DisableClientMultiStatements *bool 0.0.0 disable_client_multi_statements --DoltTransactionCommit *bool 0.0.0 dolt_transaction_commit +-DisableClientMultiStatements *bool 0.0.0 disable_client_multi_statements,omitempty +-DoltTransactionCommit *bool 0.0.0 dolt_transaction_commit,omitempty -EventSchedulerStatus *string 1.17.0 event_scheduler,omitempty -UserConfig servercfg.UserYAMLConfig 0.0.0 user --Name *string 0.0.0 name --Password *string 0.0.0 password -ListenerConfig servercfg.ListenerYAMLConfig 0.0.0 listener --HostStr *string 0.0.0 host --PortNumber *int 0.0.0 port --MaxConnections *uint64 0.0.0 max_connections --ReadTimeoutMillis *uint64 0.0.0 read_timeout_millis --WriteTimeoutMillis *uint64 0.0.0 write_timeout_millis --TLSKey *string 0.0.0 tls_key --TLSCert *string 0.0.0 tls_cert --RequireSecureTransport *bool 0.0.0 require_secure_transport --AllowCleartextPasswords *bool 0.0.0 allow_cleartext_passwords +UserConfig servercfg.UserYAMLConfig 0.0.0 user,omitempty +-Name *string 0.0.0 name,omitempty +-Password *string 0.0.0 password,omitempty +ListenerConfig servercfg.ListenerYAMLConfig 0.0.0 listener,omitempty +-HostStr *string 0.0.0 host,omitempty +-PortNumber *int 0.0.0 port,omitempty +-MaxConnections *uint64 0.0.0 max_connections,omitempty +-ReadTimeoutMillis *uint64 0.0.0 read_timeout_millis,omitempty +-WriteTimeoutMillis *uint64 0.0.0 write_timeout_millis,omitempty +-TLSKey *string 0.0.0 tls_key,omitempty +-TLSCert *string 0.0.0 tls_cert,omitempty +-RequireSecureTransport *bool 0.0.0 require_secure_transport,omitempty +-AllowCleartextPasswords *bool 0.0.0 allow_cleartext_passwords,omitempty -Socket *string 0.0.0 socket,omitempty PerformanceConfig *servercfg.PerformanceYAMLConfig 0.0.0 performance,omitempty -QueryParallelism *int 0.0.0 query_parallelism,omitempty DataDirStr *string 0.0.0 data_dir,omitempty CfgDirStr *string 0.0.0 cfg_dir,omitempty -MetricsConfig servercfg.MetricsYAMLConfig 0.0.0 metrics --Labels map[string]string 0.0.0 labels --Host *string 0.0.0 host --Port *int 0.0.0 port -RemotesapiConfig servercfg.RemotesapiYAMLConfig 0.0.0 remotesapi +RemotesapiConfig servercfg.RemotesapiYAMLConfig 0.0.0 remotesapi,omitempty -Port_ *int 0.0.0 port,omitempty -ReadOnly_ *bool 1.30.5 read_only,omitempty +PrivilegeFile *string 0.0.0 privilege_file,omitempty +BranchControlFile *string 0.0.0 branch_control_file,omitempty +Vars []servercfg.UserSessionVars 0.0.0 user_session_vars,omitempty +-Name string 0.0.0 name +-Vars map[string]interface{} 0.0.0 vars +SystemVars_ map[string]interface{} 1.11.1 system_variables,omitempty +Jwks *[]servercfg.JwksConfig 0.0.0 jwks,omitempty +GoldenMysqlConn *string 0.0.0 golden_mysql_conn,omitempty +MetricsConfig servercfg.MetricsYAMLConfig 0.0.0 metrics,omitempty +-Labels *map[string]string 0.0.0 labels,omitempty +-Host *string 0.0.0 host,omitempty +-Port *int 0.0.0 port,omitempty ClusterCfg *servercfg.ClusterYAMLConfig 0.0.0 cluster,omitempty -StandbyRemotes_ []servercfg.StandbyRemoteYAMLConfig 0.0.0 standby_remotes --Name_ string 0.0.0 name @@ -49,16 +57,4 @@ ClusterCfg *servercfg.ClusterYAMLConfig 0.0.0 cluster,omitempty --TLSCert_ string 0.0.0 tls_cert --TLSCA_ string 0.0.0 tls_ca --URLMatches []string 0.0.0 server_name_urls ---DNSMatches []string 0.0.0 server_name_dns -PrivilegeFile *string 0.0.0 privilege_file,omitempty -BranchControlFile *string 0.0.0 branch_control_file,omitempty -Vars []servercfg.UserSessionVars 0.0.0 user_session_vars --Name string 0.0.0 name --Vars map[string]interface{} 0.0.0 vars -SystemVars_ map[string]interface{} 1.11.1 system_variables,omitempty -Jwks []servercfg.JwksConfig 0.0.0 jwks --Name string 0.0.0 name --LocationUrl string 0.0.0 location_url --Claims map[string]string 0.0.0 claims --FieldsToLog []string 0.0.0 fields_to_log -GoldenMysqlConn *string 0.0.0 golden_mysql_conn,omitempty \ No newline at end of file +--DNSMatches []string 0.0.0 server_name_dns \ No newline at end of file diff --git a/go/libraries/doltcore/servercfg/yaml_config.go b/go/libraries/doltcore/servercfg/yaml_config.go index 02a1672cdc1..739ab45163a 100644 --- a/go/libraries/doltcore/servercfg/yaml_config.go +++ b/go/libraries/doltcore/servercfg/yaml_config.go @@ -47,10 +47,24 @@ func nillableIntPtr(n int) *int { return &n } +func nillableSlicePtr[T any](s []T) *[]T { + if len(s) == 0 { + return nil + } + return &s +} + +func nillableMapPtr[K comparable, V any](m map[K]V) *map[K]V { + if len(m) == 0 { + return nil + } + return &m +} + // BehaviorYAMLConfig contains server configuration regarding how the server should behave type BehaviorYAMLConfig struct { - ReadOnly *bool `yaml:"read_only"` - AutoCommit *bool `yaml:"autocommit"` + ReadOnly *bool `yaml:"read_only,omitempty"` + AutoCommit *bool `yaml:"autocommit,omitempty"` // PersistenceBehavior is unused, but still present to prevent breaking any YAML configs that still use it. PersistenceBehavior *string `yaml:"persistence_behavior,omitempty"` // Disable processing CLIENT_MULTI_STATEMENTS support on the @@ -59,35 +73,35 @@ type BehaviorYAMLConfig struct { // does), and then sends statements that contain embedded unquoted ';'s // (such as a CREATE TRIGGER), then those incoming queries will be // misprocessed. - DisableClientMultiStatements *bool `yaml:"disable_client_multi_statements"` + DisableClientMultiStatements *bool `yaml:"disable_client_multi_statements,omitempty"` // DoltTransactionCommit enables the @@dolt_transaction_commit system variable, which // automatically creates a Dolt commit when any SQL transaction is committed. - DoltTransactionCommit *bool `yaml:"dolt_transaction_commit"` + DoltTransactionCommit *bool `yaml:"dolt_transaction_commit,omitempty"` EventSchedulerStatus *string `yaml:"event_scheduler,omitempty" minver:"1.17.0"` } // UserYAMLConfig contains server configuration regarding the user account clients must use to connect type UserYAMLConfig struct { - Name *string `yaml:"name"` - Password *string `yaml:"password"` + Name *string `yaml:"name,omitempty"` + Password *string `yaml:"password,omitempty"` } // ListenerYAMLConfig contains information on the network connection that the server will open type ListenerYAMLConfig struct { - HostStr *string `yaml:"host"` - PortNumber *int `yaml:"port"` - MaxConnections *uint64 `yaml:"max_connections"` - ReadTimeoutMillis *uint64 `yaml:"read_timeout_millis"` - WriteTimeoutMillis *uint64 `yaml:"write_timeout_millis"` + HostStr *string `yaml:"host,omitempty"` + PortNumber *int `yaml:"port,omitempty"` + MaxConnections *uint64 `yaml:"max_connections,omitempty"` + ReadTimeoutMillis *uint64 `yaml:"read_timeout_millis,omitempty"` + WriteTimeoutMillis *uint64 `yaml:"write_timeout_millis,omitempty"` // TLSKey is a file system path to an unencrypted private TLS key in PEM format. - TLSKey *string `yaml:"tls_key"` + TLSKey *string `yaml:"tls_key,omitempty"` // TLSCert is a file system path to a TLS certificate chain in PEM format. - TLSCert *string `yaml:"tls_cert"` + TLSCert *string `yaml:"tls_cert,omitempty"` // RequireSecureTransport can enable a mode where non-TLS connections are turned away. - RequireSecureTransport *bool `yaml:"require_secure_transport"` + RequireSecureTransport *bool `yaml:"require_secure_transport,omitempty"` // AllowCleartextPasswords enables use of cleartext passwords. - AllowCleartextPasswords *bool `yaml:"allow_cleartext_passwords"` + AllowCleartextPasswords *bool `yaml:"allow_cleartext_passwords,omitempty"` // Socket is unix socket file path Socket *string `yaml:"socket,omitempty"` } @@ -99,9 +113,9 @@ type PerformanceYAMLConfig struct { } type MetricsYAMLConfig struct { - Labels map[string]string `yaml:"labels"` - Host *string `yaml:"host"` - Port *int `yaml:"port"` + Labels *map[string]string `yaml:"labels,omitempty"` + Host *string `yaml:"host,omitempty"` + Port *int `yaml:"port,omitempty"` } type RemotesapiYAMLConfig struct { @@ -127,22 +141,22 @@ type YAMLConfig struct { LogLevelStr *string `yaml:"log_level,omitempty"` MaxQueryLenInLogs *int `yaml:"max_logged_query_len,omitempty"` EncodeLoggedQuery *bool `yaml:"encode_logged_query,omitempty"` - BehaviorConfig BehaviorYAMLConfig `yaml:"behavior"` - UserConfig UserYAMLConfig `yaml:"user"` - ListenerConfig ListenerYAMLConfig `yaml:"listener"` + BehaviorConfig BehaviorYAMLConfig `yaml:"behavior,omitempty"` + UserConfig UserYAMLConfig `yaml:"user,omitempty"` + ListenerConfig ListenerYAMLConfig `yaml:"listener,omitempty"` PerformanceConfig *PerformanceYAMLConfig `yaml:"performance,omitempty"` DataDirStr *string `yaml:"data_dir,omitempty"` CfgDirStr *string `yaml:"cfg_dir,omitempty"` - MetricsConfig MetricsYAMLConfig `yaml:"metrics"` - RemotesapiConfig RemotesapiYAMLConfig `yaml:"remotesapi"` - ClusterCfg *ClusterYAMLConfig `yaml:"cluster,omitempty"` + RemotesapiConfig RemotesapiYAMLConfig `yaml:"remotesapi,omitempty"` PrivilegeFile *string `yaml:"privilege_file,omitempty"` BranchControlFile *string `yaml:"branch_control_file,omitempty"` // TODO: Rename to UserVars_ - Vars []UserSessionVars `yaml:"user_session_vars"` + Vars []UserSessionVars `yaml:"user_session_vars,omitempty"` SystemVars_ map[string]interface{} `yaml:"system_variables,omitempty" minver:"1.11.1"` - Jwks []JwksConfig `yaml:"jwks"` + Jwks *[]JwksConfig `yaml:"jwks,omitempty"` GoldenMysqlConn *string `yaml:"golden_mysql_conn,omitempty"` + MetricsConfig MetricsYAMLConfig `yaml:"metrics,omitempty"` + ClusterCfg *ClusterYAMLConfig `yaml:"cluster,omitempty"` } var _ ServerConfig = YAMLConfig{} @@ -206,7 +220,7 @@ func ServerConfigAsYAMLConfig(cfg ServerConfig) *YAMLConfig { DataDirStr: ptr(cfg.DataDir()), CfgDirStr: ptr(cfg.CfgDir()), MetricsConfig: MetricsYAMLConfig{ - Labels: cfg.MetricsLabels(), + Labels: nillableMapPtr(cfg.MetricsLabels()), Host: nillableStrPtr(cfg.MetricsHost()), Port: ptr(cfg.MetricsPort()), }, @@ -219,7 +233,7 @@ func ServerConfigAsYAMLConfig(cfg ServerConfig) *YAMLConfig { BranchControlFile: ptr(cfg.BranchControlFilePath()), SystemVars_: systemVars, Vars: cfg.UserVars(), - Jwks: cfg.JwksConfig(), + Jwks: nillableSlicePtr(cfg.JwksConfig()), } } @@ -244,6 +258,66 @@ func clusterConfigAsYAMLConfig(config ClusterConfig) *ClusterYAMLConfig { } } +// ServerConfigSetValuesAsYAMLConfig returns a YAMLConfig containing only values +// that were explicitly set in the given ServerConfig. +func ServerConfigSetValuesAsYAMLConfig(cfg ServerConfig) *YAMLConfig { + systemVars := cfg.SystemVars() + + return &YAMLConfig{ + LogLevelStr: zeroIf(ptr(string(cfg.LogLevel())), !cfg.ValueSet(LogLevelKey)), + MaxQueryLenInLogs: zeroIf(ptr(cfg.MaxLoggedQueryLen()), !cfg.ValueSet(MaxLoggedQueryLenKey)), + EncodeLoggedQuery: zeroIf(ptr(cfg.ShouldEncodeLoggedQuery()), !cfg.ValueSet(ShouldEncodeLoggedQueryKey)), + BehaviorConfig: BehaviorYAMLConfig{ + ReadOnly: zeroIf(ptr(cfg.ReadOnly()), !cfg.ValueSet(ReadOnlyKey)), + AutoCommit: zeroIf(ptr(cfg.AutoCommit()), !cfg.ValueSet(AutoCommitKey)), + DisableClientMultiStatements: zeroIf(ptr(cfg.DisableClientMultiStatements()), !cfg.ValueSet(DisableClientMultiStatementsKey)), + DoltTransactionCommit: zeroIf(ptr(cfg.DoltTransactionCommit()), !cfg.ValueSet(DoltTransactionCommitKey)), + EventSchedulerStatus: zeroIf(ptr(cfg.EventSchedulerStatus()), !cfg.ValueSet(EventSchedulerKey)), + }, + UserConfig: UserYAMLConfig{ + Name: zeroIf(ptr(cfg.User()), !cfg.ValueSet(UserKey)), + Password: zeroIf(ptr(cfg.Password()), !cfg.ValueSet(PasswordKey)), + }, + ListenerConfig: ListenerYAMLConfig{ + HostStr: zeroIf(ptr(cfg.Host()), !cfg.ValueSet(HostKey)), + PortNumber: zeroIf(ptr(cfg.Port()), !cfg.ValueSet(PortKey)), + MaxConnections: zeroIf(ptr(cfg.MaxConnections()), !cfg.ValueSet(MaxConnectionsKey)), + ReadTimeoutMillis: zeroIf(ptr(cfg.ReadTimeout()), !cfg.ValueSet(ReadTimeoutKey)), + WriteTimeoutMillis: zeroIf(ptr(cfg.WriteTimeout()), !cfg.ValueSet(WriteTimeoutKey)), + TLSKey: zeroIf(ptr(cfg.TLSKey()), !cfg.ValueSet(TLSKeyKey)), + TLSCert: zeroIf(ptr(cfg.TLSCert()), !cfg.ValueSet(TLSCertKey)), + RequireSecureTransport: zeroIf(ptr(cfg.RequireSecureTransport()), !cfg.ValueSet(RequireSecureTransportKey)), + AllowCleartextPasswords: zeroIf(ptr(cfg.AllowCleartextPasswords()), !cfg.ValueSet(AllowCleartextPasswordsKey)), + Socket: zeroIf(ptr(cfg.Socket()), !cfg.ValueSet(SocketKey)), + }, + DataDirStr: zeroIf(ptr(cfg.DataDir()), !cfg.ValueSet(DataDirKey)), + CfgDirStr: zeroIf(ptr(cfg.CfgDir()), !cfg.ValueSet(CfgDirKey)), + MetricsConfig: MetricsYAMLConfig{ + Labels: zeroIf(ptr(cfg.MetricsLabels()), !cfg.ValueSet(MetricsLabelsKey)), + Host: zeroIf(ptr(cfg.MetricsHost()), !cfg.ValueSet(MetricsHostKey)), + Port: zeroIf(ptr(cfg.MetricsPort()), !cfg.ValueSet(MetricsPortKey)), + }, + RemotesapiConfig: RemotesapiYAMLConfig{ + Port_: zeroIf(cfg.RemotesapiPort(), !cfg.ValueSet(RemotesapiPortKey)), + ReadOnly_: zeroIf(cfg.RemotesapiReadOnly(), !cfg.ValueSet(RemotesapiReadOnlyKey)), + }, + ClusterCfg: zeroIf(clusterConfigAsYAMLConfig(cfg.ClusterConfig()), !cfg.ValueSet(ClusterConfigKey)), + PrivilegeFile: zeroIf(ptr(cfg.PrivilegeFilePath()), !cfg.ValueSet(PrivilegeFilePathKey)), + BranchControlFile: zeroIf(ptr(cfg.BranchControlFilePath()), !cfg.ValueSet(BranchControlFilePathKey)), + SystemVars_: zeroIf(systemVars, !cfg.ValueSet(SystemVarsKey)), + Vars: zeroIf(cfg.UserVars(), !cfg.ValueSet(UserVarsKey)), + Jwks: zeroIf(ptr(cfg.JwksConfig()), !cfg.ValueSet(JwksConfigKey)), + } +} + +func zeroIf[T any](val T, condition bool) T { + if condition { + var zero T + return zero + } + return val +} + // String returns the YAML representation of the config func (cfg YAMLConfig) String() string { data, err := yaml.Marshal(cfg) @@ -265,7 +339,7 @@ func (cfg YAMLConfig) String() string { } r, _ := utf8.DecodeRuneInString(lines[i]) - if !unicode.IsSpace(r) { + if !unicode.IsSpace(r) && r != '-' { formatted = append(formatted, "") } @@ -276,6 +350,209 @@ func (cfg YAMLConfig) String() string { return result } +// VerboseString behaves like String, but includes commented-out placeholders for empty fields instead of omitting them. +func (cfg YAMLConfig) VerboseString() string { + withPlaceholders := cfg.withPlaceholdersFilledIn() + + return commentYAMLDiffs(cfg.String(), withPlaceholders.String()) +} + +// withPlaceholdersFilledIn returns the config with placeholder values in place of nil values. +// +// The placeholder value for a field will be its default value if one exists, or an arbitrary +// example value if no default exists. Deprecated or unused fields will not be given placeholder values. +// +// The config generated by this function should only be used to produce example values for +// commented-out YAML fields, and shouldn't be used to actually configure anything. +func (cfg YAMLConfig) withPlaceholdersFilledIn() YAMLConfig { + withPlaceholders := cfg.withDefaultsFilledIn() + + if withPlaceholders.BehaviorConfig.DisableClientMultiStatements == nil { + withPlaceholders.BehaviorConfig.DisableClientMultiStatements = ptr(false) + } + if withPlaceholders.BehaviorConfig.EventSchedulerStatus == nil { + withPlaceholders.BehaviorConfig.EventSchedulerStatus = ptr("OFF") + } + + if withPlaceholders.ListenerConfig.TLSKey == nil { + withPlaceholders.ListenerConfig.TLSKey = ptr("key.pem") + } + if withPlaceholders.ListenerConfig.TLSCert == nil { + withPlaceholders.ListenerConfig.TLSCert = ptr("cert.pem") + } + if withPlaceholders.ListenerConfig.RequireSecureTransport == nil { + withPlaceholders.ListenerConfig.RequireSecureTransport = ptr(false) + } + if withPlaceholders.ListenerConfig.Socket == nil { + withPlaceholders.ListenerConfig.Socket = ptr(DefaultUnixSocketFilePath) + } + + if withPlaceholders.MetricsConfig.Labels == nil { + withPlaceholders.MetricsConfig.Labels = &map[string]string{} + } + if withPlaceholders.MetricsConfig.Host == nil { + withPlaceholders.MetricsConfig.Host = ptr("localhost") + } + if withPlaceholders.MetricsConfig.Port == nil { + withPlaceholders.MetricsConfig.Port = ptr(9091) + } + + if withPlaceholders.RemotesapiConfig.Port_ == nil { + withPlaceholders.RemotesapiConfig.Port_ = ptr(8000) + } + if withPlaceholders.RemotesapiConfig.ReadOnly_ == nil { + withPlaceholders.RemotesapiConfig.ReadOnly_ = ptr(false) + } + + if withPlaceholders.ClusterCfg == nil { + withPlaceholders.ClusterCfg = &ClusterYAMLConfig{ + StandbyRemotes_: []StandbyRemoteYAMLConfig{ + StandbyRemoteYAMLConfig{ + Name_: "standby_replica_one", + RemoteURLTemplate_: "https://standby_replica_one.svc.cluster.local:50051/{database}", + }, + StandbyRemoteYAMLConfig{ + Name_: "standby_replica_two", + RemoteURLTemplate_: "https://standby_replica_two.svc.cluster.local:50051/{database}", + }, + }, + BootstrapRole_: "primary", + BootstrapEpoch_: 1, + RemotesAPI: ClusterRemotesAPIYAMLConfig{ + Addr_: "127.0.0.1", + Port_: 50051, + TLSKey_: "remotesapi_key.pem", + TLSCert_: "remotesapi_chain.pem", + TLSCA_: "standby_cas.pem", + URLMatches: []string{ + "https://standby_replica_one.svc.cluster.local", + "https://standby_replica_two.svc.cluster.local", + }, + DNSMatches: []string{ + "standby_replica_one.svc.cluster.local", + "standby_replica_two.svc.cluster.local", + }, + }, + } + } + + if withPlaceholders.Vars == nil { + withPlaceholders.Vars = []UserSessionVars{ + UserSessionVars{ + Name: "root", + Vars: map[string]interface{}{ + "dolt_show_system_tables": 1, + "dolt_log_level": "warn", + }, + }, + } + } + + if withPlaceholders.SystemVars_ == nil { + withPlaceholders.SystemVars_ = map[string]interface{}{ + "dolt_transaction_commit": 1, + "dolt_log_level": "info", + } + } + + if withPlaceholders.Jwks == nil { + withPlaceholders.Jwks = &[]JwksConfig{} + } + + return withPlaceholders +} + +// withDefaultsFilledIn returns the config with default values in place of nil values. +func (cfg YAMLConfig) withDefaultsFilledIn() YAMLConfig { + defaults := defaultServerConfigYAML() + withDefaults := cfg + + if withDefaults.LogLevelStr == nil { + withDefaults.LogLevelStr = defaults.LogLevelStr + } + if withDefaults.MaxQueryLenInLogs == nil { + withDefaults.MaxQueryLenInLogs = defaults.MaxQueryLenInLogs + } + if withDefaults.EncodeLoggedQuery == nil { + withDefaults.EncodeLoggedQuery = defaults.EncodeLoggedQuery + } + + if withDefaults.BehaviorConfig.ReadOnly == nil { + withDefaults.BehaviorConfig.ReadOnly = defaults.BehaviorConfig.ReadOnly + } + if withDefaults.BehaviorConfig.AutoCommit == nil { + withDefaults.BehaviorConfig.AutoCommit = defaults.BehaviorConfig.AutoCommit + } + if withDefaults.BehaviorConfig.DoltTransactionCommit == nil { + withDefaults.BehaviorConfig.DoltTransactionCommit = defaults.BehaviorConfig.DoltTransactionCommit + } + + if withDefaults.UserConfig.Name == nil { + withDefaults.UserConfig.Name = defaults.UserConfig.Name + } + if withDefaults.UserConfig.Password == nil { + withDefaults.UserConfig.Password = defaults.UserConfig.Password + } + + if withDefaults.ListenerConfig.HostStr == nil { + withDefaults.ListenerConfig.HostStr = defaults.ListenerConfig.HostStr + } + if withDefaults.ListenerConfig.PortNumber == nil { + withDefaults.ListenerConfig.PortNumber = defaults.ListenerConfig.PortNumber + } + if withDefaults.ListenerConfig.MaxConnections == nil { + withDefaults.ListenerConfig.MaxConnections = defaults.ListenerConfig.MaxConnections + } + if withDefaults.ListenerConfig.ReadTimeoutMillis == nil { + withDefaults.ListenerConfig.ReadTimeoutMillis = defaults.ListenerConfig.ReadTimeoutMillis + } + if withDefaults.ListenerConfig.WriteTimeoutMillis == nil { + withDefaults.ListenerConfig.WriteTimeoutMillis = defaults.ListenerConfig.WriteTimeoutMillis + } + if withDefaults.ListenerConfig.AllowCleartextPasswords == nil { + withDefaults.ListenerConfig.AllowCleartextPasswords = defaults.ListenerConfig.AllowCleartextPasswords + } + + if withDefaults.DataDirStr == nil { + withDefaults.DataDirStr = defaults.DataDirStr + } + if withDefaults.CfgDirStr == nil { + withDefaults.CfgDirStr = defaults.CfgDirStr + } + if withDefaults.PrivilegeFile == nil { + withDefaults.PrivilegeFile = defaults.PrivilegeFile + } + if withDefaults.BranchControlFile == nil { + withDefaults.BranchControlFile = defaults.BranchControlFile + } + + return withDefaults +} + +// commentYAMLDiffs takes YAML-formatted strings |a| and |b| and returns a YAML-formatted string +// containing all of the lines in |a|, along with comments containing all of the lines in |b| that are not in |a|. +// +// Assumes all lines in |a| appear in |b|, with the same relative ordering. +func commentYAMLDiffs(a, b string) string { + linesA := strings.Split(a, "\n") + linesB := strings.Split(b, "\n") + + aIdx := 0 + for bIdx := range linesB { + if aIdx >= len(linesA) || linesA[aIdx] != linesB[bIdx] { + withoutSpace := strings.TrimSpace(linesB[bIdx]) + if len(withoutSpace) > 0 { + space := linesB[bIdx][:len(linesB[bIdx])-len(withoutSpace)] + linesB[bIdx] = space + "# " + withoutSpace + } + } else { + aIdx++ + } + } + + return strings.Join(linesB, "\n") +} + // Host returns the domain that the server will run on. Accepts an IPv4 or IPv6 address, in addition to localhost. func (cfg YAMLConfig) Host() string { if cfg.ListenerConfig.HostStr == nil { @@ -397,7 +674,10 @@ func (cfg YAMLConfig) DisableClientMultiStatements() bool { // MetricsLabels returns labels that are applied to all prometheus metrics func (cfg YAMLConfig) MetricsLabels() map[string]string { - return cfg.MetricsConfig.Labels + if cfg.MetricsConfig.Labels != nil { + return *cfg.MetricsConfig.Labels + } + return nil } func (cfg YAMLConfig) MetricsHost() string { @@ -464,7 +744,7 @@ func (cfg YAMLConfig) SystemVars() map[string]interface{} { // wksConfig is JSON Web Key Set config, and used to validate a user authed with a jwt (JSON Web Token). func (cfg YAMLConfig) JwksConfig() []JwksConfig { if cfg.Jwks != nil { - return cfg.Jwks + return *cfg.Jwks } return nil } diff --git a/go/libraries/doltcore/servercfg/yaml_config_test.go b/go/libraries/doltcore/servercfg/yaml_config_test.go index 67a46e1da07..4125ae72837 100644 --- a/go/libraries/doltcore/servercfg/yaml_config_test.go +++ b/go/libraries/doltcore/servercfg/yaml_config_test.go @@ -95,7 +95,7 @@ jwks: expected.MetricsConfig = MetricsYAMLConfig{ Host: ptr("123.45.67.89"), Port: ptr(9091), - Labels: map[string]string{ + Labels: &map[string]string{ "label1": "value1", "label2": "2", "label3": "true", @@ -121,7 +121,7 @@ jwks: }, }, } - expected.Jwks = []JwksConfig{ + expected.Jwks = &[]JwksConfig{ { Name: "jwks_name", LocationUrl: "https://website.com", @@ -427,3 +427,128 @@ metrics: assert.Equal(t, "localhost", cfg.MetricsHost()) assert.Equal(t, -1, cfg.MetricsPort()) } + +// Tests that YAMLConfig.String() and YAMLConfig.VerboseString() produce equivalent YAML. +func TestYAMLConfigVerboseStringEquivalent(t *testing.T) { + yamlEquivalent := func(a, b string) bool { + var unmarshaled1 any + err := yaml.Unmarshal([]byte(a), &unmarshaled1) + require.NoError(t, err) + + var unmarshaled2 any + err = yaml.Unmarshal([]byte(a), &unmarshaled2) + require.NoError(t, err) + + remarshaled1, err := yaml.Marshal(unmarshaled1) + require.NoError(t, err) + + remarshaled2, err := yaml.Marshal(unmarshaled1) + require.NoError(t, err) + + return string(remarshaled1) == string(remarshaled2) + } + + configs := []YAMLConfig{ + YAMLConfig{ + LogLevelStr: ptr("warn"), + MaxQueryLenInLogs: ptr(1234), + ListenerConfig: ListenerYAMLConfig{ + HostStr: ptr("XXYYZZ"), + PortNumber: ptr(33333), + }, + DataDirStr: ptr("abcdef"), + GoldenMysqlConn: ptr("abc123"), + }, + YAMLConfig{ + MetricsConfig: MetricsYAMLConfig{ + Labels: &map[string]string{ + "xyz": "123", + "0": "AAABBB", + }, + Host: ptr("!!!!!!!!"), + }, + }, + YAMLConfig{ + MetricsConfig: MetricsYAMLConfig{ + Port: ptr(0), + }, + RemotesapiConfig: RemotesapiYAMLConfig{ + Port_: ptr(111), + ReadOnly_: ptr(false), + }, + }, + } + + for _, config := range configs { + assert.True(t, yamlEquivalent(config.String(), config.VerboseString())) + } +} + +func TestCommentYAMLDiffs(t *testing.T) { + a := `abc: 100 +dddddd: "1234" +fire: water + +a: + b: + c: 1001011 + + t: g + +x: +- we +- se +- ll` + + b := `abc: 100 +dddddd: "1234" + +fire: water +extra1: 12345 + +a: + b: + c: 1001011 + extra2: iiiii + + t: g + +x: +- we +- extra3 +- extra4 +- se +- extra5 +- ll + +extra6: + extra7: + extra8: 999` + + expected := `abc: 100 +dddddd: "1234" + +fire: water +# extra1: 12345 + +a: + b: + c: 1001011 + # extra2: iiiii + + t: g + +x: +- we +# - extra3 +# - extra4 +- se +# - extra5 +- ll + +# extra6: + # extra7: + # extra8: 999` + + assert.Equal(t, expected, commentYAMLDiffs(a, b)) +} diff --git a/integration-tests/bats/helper/query-server-common.bash b/integration-tests/bats/helper/query-server-common.bash index 456be9d53ea..cf9e05aef89 100644 --- a/integration-tests/bats/helper/query-server-common.bash +++ b/integration-tests/bats/helper/query-server-common.bash @@ -60,12 +60,18 @@ start_sql_server() { # arguments to dolt-sql-server (excluding --port, which is defined in # this func) start_sql_server_with_args() { - DEFAULT_DB="" PORT=$( definePORT ) + start_sql_server_with_args_no_port "$@" --port=$PORT +} + +# behaves like start_sql_server_with_args, but doesn't define --port. +# caller must set variable PORT to proper value before calling. +start_sql_server_with_args_no_port() { + DEFAULT_DB="" if [ "$IS_WINDOWS" == true ]; then - dolt sql-server "$@" --port=$PORT & + dolt sql-server "$@" & else - dolt sql-server "$@" --port=$PORT --socket "dolt.$PORT.sock" & + dolt sql-server "$@" --socket "dolt.$PORT.sock" & fi SERVER_PID=$! wait_for_connection $PORT 8500 diff --git a/integration-tests/bats/sql-server-config-file-generation.bats b/integration-tests/bats/sql-server-config-file-generation.bats new file mode 100644 index 00000000000..151c92787ef --- /dev/null +++ b/integration-tests/bats/sql-server-config-file-generation.bats @@ -0,0 +1,142 @@ +#!/usr/bin/env bats +load "$BATS_TEST_DIRNAME/helper/common.bash" +load "$BATS_TEST_DIRNAME/helper/query-server-common.bash" + +CONFIG_FILE_NAME=config.yaml + +DATABASE_DIRS=( + . + mydir + nest1/nest2/nest3 +) + +setup() { + if [ "$SQL_ENGINE" = "remote-engine" ]; then + skip "This test tests remote connections directly, SQL_ENGINE is not needed." + fi + setup_common +} + +teardown() { + stop_sql_server + teardown_common +} + +@test "sql-server-config-file-generation: config file is generated if one doesn't exist" { + for data_dir in "${DATABASE_DIRS[@]}"; do + if [[ "$data_dir" != "." ]]; then + mkdir -p "$data_dir" + fi + + start_sql_server_with_args --data-dir "$data_dir" --host 0.0.0.0 --user dolt + + [[ -f "$data_dir/$CONFIG_FILE_NAME" ]] || false + + rm "$data_dir/$CONFIG_FILE_NAME" + stop_sql_server + done +} + +@test "sql-server-config-file-generation: config file isn't generated if one already exists" { + for data_dir in "${DATABASE_DIRS[@]}"; do + if [[ "$data_dir" != "." ]]; then + mkdir -p "$data_dir" + fi + + echo "Don't overwrite me!" >"$data_dir/$CONFIG_FILE_NAME" + + start_sql_server_with_args --data-dir "$data_dir" --host 0.0.0.0 --user dolt + + run cat "$data_dir/$CONFIG_FILE_NAME" + [ $status -eq 0 ] + [[ "$output" =~ "Don't overwrite me!" ]] || false + + rm "$data_dir/$CONFIG_FILE_NAME" + stop_sql_server + done +} + +@test "sql-server-config-file-generation: config file isn't generated if a config is specified in args" { + for data_dir in "${DATABASE_DIRS[@]}"; do + if [[ "$data_dir" != "." ]]; then + mkdir -p "$data_dir" + fi + + NOT_CONFIG_FILE_NAME="not-$CONFIG_FILE_NAME" + PORT=$(definePORT) + + cat >"$data_dir/$NOT_CONFIG_FILE_NAME" <