diff options
author | Will Chandler <wchandler@gitlab.com> | 2023-08-01 21:46:29 +0300 |
---|---|---|
committer | Will Chandler <wchandler@gitlab.com> | 2023-08-01 21:46:29 +0300 |
commit | 108242aa2fe7ce3a98f315fc73138630f9117a6e (patch) | |
tree | 490c6a9a9c0b3e94746c4e23a257876a03c11fff | |
parent | 16578b4560ee72b4489937a44872803b1876c11e (diff) | |
parent | 5ebe6f6d13f1182fdf6ac3e27b0441fc6085eace (diff) |
Merge branch 'pks-praefect-config-command' into 'master'4573-featureflag-enable-batch-command-for-git-cat-file-1
praefect/config: Support generating configuration via external command
Closes #5076
See merge request https://gitlab.com/gitlab-org/gitaly/-/merge_requests/6157
Merged-by: Will Chandler <wchandler@gitlab.com>
Approved-by: Quang-Minh Nguyen <qmnguyen@gitlab.com>
Approved-by: Will Chandler <wchandler@gitlab.com>
Reviewed-by: Quang-Minh Nguyen <qmnguyen@gitlab.com>
Co-authored-by: Patrick Steinhardt <psteinhardt@gitlab.com>
-rw-r--r-- | internal/gitaly/config/log/log.go | 6 | ||||
-rw-r--r-- | internal/gitaly/config/prometheus/config.go | 4 | ||||
-rw-r--r-- | internal/gitaly/config/sentry/sentry.go | 4 | ||||
-rw-r--r-- | internal/praefect/config/config.go | 149 | ||||
-rw-r--r-- | internal/praefect/config/config_test.go | 266 | ||||
-rw-r--r-- | internal/praefect/config/node.go | 6 |
6 files changed, 362 insertions, 73 deletions
diff --git a/internal/gitaly/config/log/log.go b/internal/gitaly/config/log/log.go index ab7a78af5..fb46ee530 100644 --- a/internal/gitaly/config/log/log.go +++ b/internal/gitaly/config/log/log.go @@ -2,7 +2,7 @@ package log // Config contains logging configuration values type Config struct { - Dir string `toml:"dir,omitempty"` - Format string `toml:"format,omitempty"` - Level string `toml:"level,omitempty"` + Dir string `toml:"dir,omitempty" json:"dir"` + Format string `toml:"format,omitempty" json:"format"` + Level string `toml:"level,omitempty" json:"level"` } diff --git a/internal/gitaly/config/prometheus/config.go b/internal/gitaly/config/prometheus/config.go index f9ac7f51d..b6a189138 100644 --- a/internal/gitaly/config/prometheus/config.go +++ b/internal/gitaly/config/prometheus/config.go @@ -15,10 +15,10 @@ import ( // Config contains additional configuration data for prometheus type Config struct { // ScrapeTimeout is the allowed duration of a Prometheus scrape before timing out. - ScrapeTimeout duration.Duration `toml:"scrape_timeout,omitempty"` + ScrapeTimeout duration.Duration `toml:"scrape_timeout,omitempty" json:"scrape_timeout"` // GRPCLatencyBuckets configures the histogram buckets used for gRPC // latency measurements. - GRPCLatencyBuckets []float64 `toml:"grpc_latency_buckets,omitempty"` + GRPCLatencyBuckets []float64 `toml:"grpc_latency_buckets,omitempty" json:"grpc_latency_buckets"` } // DefaultConfig returns a new config with default values set. diff --git a/internal/gitaly/config/sentry/sentry.go b/internal/gitaly/config/sentry/sentry.go index a63207201..52bba10f6 100644 --- a/internal/gitaly/config/sentry/sentry.go +++ b/internal/gitaly/config/sentry/sentry.go @@ -10,8 +10,8 @@ import ( // Config contains configuration for sentry type Config struct { - DSN string `toml:"sentry_dsn,omitempty"` - Environment string `toml:"sentry_environment,omitempty"` + DSN string `toml:"sentry_dsn,omitempty" json:"sentry_dsn"` + Environment string `toml:"sentry_environment,omitempty" json:"sentry_environment"` } // ConfigureSentry configures the sentry DSN diff --git a/internal/praefect/config/config.go b/internal/praefect/config/config.go index de415ecdc..c5eb27d2a 100644 --- a/internal/praefect/config/config.go +++ b/internal/praefect/config/config.go @@ -2,10 +2,12 @@ package config import ( "bytes" + "encoding/json" "errors" "fmt" "io" "os" + "os/exec" "sort" "time" @@ -49,18 +51,18 @@ const ( // Failover contains configuration for the mechanism that tracks healthiness of the cluster nodes. type Failover struct { // Enabled is a trigger used to check if failover is enabled or not. - Enabled bool `toml:"enabled,omitempty"` + Enabled bool `toml:"enabled,omitempty" json:"enabled"` // ElectionStrategy is the strategy to use for electing primaries nodes. - ElectionStrategy ElectionStrategy `toml:"election_strategy,omitempty"` - ErrorThresholdWindow duration.Duration `toml:"error_threshold_window,omitempty"` - WriteErrorThresholdCount uint32 `toml:"write_error_threshold_count,omitempty"` - ReadErrorThresholdCount uint32 `toml:"read_error_threshold_count,omitempty"` + ElectionStrategy ElectionStrategy `toml:"election_strategy,omitempty" json:"election_strategy"` + ErrorThresholdWindow duration.Duration `toml:"error_threshold_window,omitempty" json:"error_threshold_window"` + WriteErrorThresholdCount uint32 `toml:"write_error_threshold_count,omitempty" json:"write_error_threshold_count"` + ReadErrorThresholdCount uint32 `toml:"read_error_threshold_count,omitempty" json:"read_error_threshold_count"` // BootstrapInterval allows set a time duration that would be used on startup to make initial health check. // The default value is 1s. - BootstrapInterval duration.Duration `toml:"bootstrap_interval,omitempty"` + BootstrapInterval duration.Duration `toml:"bootstrap_interval,omitempty" json:"bootstrap_interval"` // MonitorInterval allows set a time duration that would be used after bootstrap is completed to execute health checks. // The default value is 3s. - MonitorInterval duration.Duration `toml:"monitor_interval,omitempty"` + MonitorInterval duration.Duration `toml:"monitor_interval,omitempty" json:"monitor_interval"` } // ErrorThresholdsConfigured checks whether returns whether the errors thresholds are configured. If they @@ -122,10 +124,10 @@ func (f Failover) Validate() error { type BackgroundVerification struct { // VerificationInterval determines the duration after a replica due for reverification. // The feature is disabled if verification interval is 0 or below. - VerificationInterval duration.Duration `toml:"verification_interval,omitempty"` + VerificationInterval duration.Duration `toml:"verification_interval,omitempty" json:"verification_interval"` // DeleteInvalidRecords controls whether the background verifier will actually delete the metadata // records that point to non-existent replicas. - DeleteInvalidRecords bool `toml:"delete_invalid_records"` + DeleteInvalidRecords bool `toml:"delete_invalid_records" json:"delete_invalid_records"` } // Validate runs validation on all fields and compose all found errors. @@ -147,9 +149,9 @@ func DefaultBackgroundVerificationConfig() BackgroundVerification { type Reconciliation struct { // SchedulingInterval the interval between each automatic reconciliation run. If set to 0, // automatic reconciliation is disabled. - SchedulingInterval duration.Duration `toml:"scheduling_interval,omitempty"` + SchedulingInterval duration.Duration `toml:"scheduling_interval,omitempty" json:"scheduling_interval"` // HistogramBuckets configures the reconciliation scheduling duration histogram's buckets. - HistogramBuckets []float64 `toml:"histogram_buckets,omitempty"` + HistogramBuckets []float64 `toml:"histogram_buckets,omitempty" json:"histogram_buckets"` } // Validate runs validation on all fields and compose all found errors. @@ -178,10 +180,10 @@ func DefaultReconciliationConfig() Reconciliation { type Replication struct { // BatchSize controls how many replication jobs to dequeue and lock // in a single call to the database. - BatchSize uint `toml:"batch_size,omitempty"` + BatchSize uint `toml:"batch_size,omitempty" json:"batch_size"` // ParallelStorageProcessingWorkers is a number of workers used to process replication // events per virtual storage (how many storages would be processed in parallel). - ParallelStorageProcessingWorkers uint `toml:"parallel_storage_processing_workers,omitempty"` + ParallelStorageProcessingWorkers uint `toml:"parallel_storage_processing_workers,omitempty" json:"parallel_storage_processing_workers"` } // Validate runs validation on all fields and compose all found errors. @@ -199,36 +201,41 @@ func DefaultReplicationConfig() Replication { // Config is a container for everything found in the TOML config file type Config struct { - AllowLegacyElectors bool `toml:"i_understand_my_election_strategy_is_unsupported_and_will_be_removed_without_warning,omitempty"` - BackgroundVerification BackgroundVerification `toml:"background_verification,omitempty"` - Reconciliation Reconciliation `toml:"reconciliation,omitempty"` - Replication Replication `toml:"replication,omitempty"` - ListenAddr string `toml:"listen_addr,omitempty"` - TLSListenAddr string `toml:"tls_listen_addr,omitempty"` - SocketPath string `toml:"socket_path,omitempty"` - VirtualStorages []*VirtualStorage `toml:"virtual_storage,omitempty"` - Logging log.Config `toml:"logging,omitempty"` - Sentry sentry.Config `toml:"sentry,omitempty"` - PrometheusListenAddr string `toml:"prometheus_listen_addr,omitempty"` - Prometheus prometheus.Config `toml:"prometheus,omitempty"` - Auth auth.Config `toml:"auth,omitempty"` - TLS config.TLS `toml:"tls,omitempty"` - DB `toml:"database,omitempty"` - Failover Failover `toml:"failover,omitempty"` - MemoryQueueEnabled bool `toml:"memory_queue_enabled,omitempty"` - GracefulStopTimeout duration.Duration `toml:"graceful_stop_timeout,omitempty"` - RepositoriesCleanup RepositoriesCleanup `toml:"repositories_cleanup,omitempty"` - Yamux Yamux `toml:"yamux,omitempty"` + // ConfigCommand specifies the path to an executable that Praefect will run after loading the initial + // configuration. The executable is expected to write JSON-formatted configuration to its standard + // output that we will then deserialize and merge back into the initially-loaded configuration again. + // This is an easy mechanism to generate parts of the configuration at runtime, like for example secrets. + ConfigCommand string `toml:"config_command,omitempty" json:"config_command"` + AllowLegacyElectors bool `toml:"i_understand_my_election_strategy_is_unsupported_and_will_be_removed_without_warning,omitempty" json:"i_understand_my_election_strategy_is_unsupported_and_will_be_removed_without_warning"` + BackgroundVerification BackgroundVerification `toml:"background_verification,omitempty" json:"background_verification"` + Reconciliation Reconciliation `toml:"reconciliation,omitempty" json:"reconciliation"` + Replication Replication `toml:"replication,omitempty" json:"replication"` + ListenAddr string `toml:"listen_addr,omitempty" json:"listen_addr"` + TLSListenAddr string `toml:"tls_listen_addr,omitempty" json:"tls_listen_addr"` + SocketPath string `toml:"socket_path,omitempty" json:"socket_path"` + VirtualStorages []*VirtualStorage `toml:"virtual_storage,omitempty" json:"virtual_storage"` + Logging log.Config `toml:"logging,omitempty" json:"logging"` + Sentry sentry.Config `toml:"sentry,omitempty" json:"sentry"` + PrometheusListenAddr string `toml:"prometheus_listen_addr,omitempty" json:"prometheus_listen_addr"` + Prometheus prometheus.Config `toml:"prometheus,omitempty" json:"prometheus"` + Auth auth.Config `toml:"auth,omitempty" json:"auth"` + TLS config.TLS `toml:"tls,omitempty" json:"tls"` + DB `toml:"database,omitempty" json:"database"` + Failover Failover `toml:"failover,omitempty" json:"failover"` + MemoryQueueEnabled bool `toml:"memory_queue_enabled,omitempty" json:"memory_queue_enabled"` + GracefulStopTimeout duration.Duration `toml:"graceful_stop_timeout,omitempty" json:"graceful_stop_timeout"` + RepositoriesCleanup RepositoriesCleanup `toml:"repositories_cleanup,omitempty" json:"repositories_cleanup"` + Yamux Yamux `toml:"yamux,omitempty" json:"yamux"` } // Yamux contains Yamux related configuration values. type Yamux struct { // MaximumStreamWindowSizeBytes sets the maximum window size in bytes used for yamux streams. // Higher value can increase throughput at the cost of more memory usage. - MaximumStreamWindowSizeBytes uint32 `toml:"maximum_stream_window_size_bytes,omitempty"` + MaximumStreamWindowSizeBytes uint32 `toml:"maximum_stream_window_size_bytes,omitempty" json:"maximum_stream_window_size_bytes"` // AcceptBacklog sets the maximum number of stream openings in-flight before further openings // block. - AcceptBacklog uint `toml:"accept_backlog,omitempty"` + AcceptBacklog uint `toml:"accept_backlog,omitempty" json:"accept_backlog"` } func (cfg Yamux) validate() error { @@ -264,14 +271,14 @@ func DefaultYamuxConfig() Yamux { // VirtualStorage represents a set of nodes for a storage type VirtualStorage struct { - Name string `toml:"name,omitempty"` - Nodes []*Node `toml:"node,omitempty"` + Name string `toml:"name,omitempty" json:"name"` + Nodes []*Node `toml:"node,omitempty" json:"node"` // DefaultReplicationFactor is the replication factor set for new repositories. // A valid value is inclusive between 1 and the number of configured storages in the // virtual storage. Setting the value to 0 or below causes Praefect to not store any // host assignments, falling back to the behavior of replicating to every configured // storage - DefaultReplicationFactor int `toml:"default_replication_factor,omitempty"` + DefaultReplicationFactor int `toml:"default_replication_factor,omitempty" json:"default_replication_factor"` } // Validate runs validation on all fields and compose all found errors. @@ -313,6 +320,22 @@ func FromReader(reader io.Reader) (Config, error) { return Config{}, err } + if conf.ConfigCommand != "" { + output, err := exec.Command(conf.ConfigCommand).Output() + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return Config{}, fmt.Errorf("running config command: %w, stderr: %q", err, string(exitErr.Stderr)) + } + + return Config{}, fmt.Errorf("running config command: %w", err) + } + + if err := json.Unmarshal(output, &conf); err != nil { + return Config{}, fmt.Errorf("unmarshalling generated config: %w", err) + } + } + conf.setDefaults() return *conf, nil @@ -490,36 +513,36 @@ func (c Config) DefaultReplicationFactors() map[string]int { // DBConnection holds Postgres client configuration data. type DBConnection struct { - Host string `toml:"host,omitempty"` - Port int `toml:"port,omitempty"` - User string `toml:"user,omitempty"` - Password string `toml:"password,omitempty"` - DBName string `toml:"dbname,omitempty"` - SSLMode string `toml:"sslmode,omitempty"` - SSLCert string `toml:"sslcert,omitempty"` - SSLKey string `toml:"sslkey,omitempty"` - SSLRootCert string `toml:"sslrootcert,omitempty"` + Host string `toml:"host,omitempty" json:"host"` + Port int `toml:"port,omitempty" json:"port"` + User string `toml:"user,omitempty" json:"user"` + Password string `toml:"password,omitempty" json:"password"` + DBName string `toml:"dbname,omitempty" json:"dbname"` + SSLMode string `toml:"sslmode,omitempty" json:"sslmode"` + SSLCert string `toml:"sslcert,omitempty" json:"sslcert"` + SSLKey string `toml:"sslkey,omitempty" json:"sslkey"` + SSLRootCert string `toml:"sslrootcert,omitempty" json:"sslrootcert"` } // DB holds database configuration data. type DB struct { - Host string `toml:"host,omitempty"` - Port int `toml:"port,omitempty"` - User string `toml:"user,omitempty"` - Password string `toml:"password,omitempty"` - DBName string `toml:"dbname,omitempty"` - SSLMode string `toml:"sslmode,omitempty"` - SSLCert string `toml:"sslcert,omitempty"` - SSLKey string `toml:"sslkey,omitempty"` - SSLRootCert string `toml:"sslrootcert,omitempty"` - - SessionPooled DBConnection `toml:"session_pooled,omitempty"` + Host string `toml:"host,omitempty" json:"host"` + Port int `toml:"port,omitempty" json:"port"` + User string `toml:"user,omitempty" json:"user"` + Password string `toml:"password,omitempty" json:"password"` + DBName string `toml:"dbname,omitempty" json:"dbname"` + SSLMode string `toml:"sslmode,omitempty" json:"sslmode"` + SSLCert string `toml:"sslcert,omitempty" json:"sslcert"` + SSLKey string `toml:"sslkey,omitempty" json:"sslkey"` + SSLRootCert string `toml:"sslrootcert,omitempty" json:"sslrootcert"` + + SessionPooled DBConnection `toml:"session_pooled,omitempty" json:"session_pooled"` // The following configuration keys are deprecated and // will be removed. Use Host and Port attributes of // SessionPooled instead. - HostNoProxy string `toml:"host_no_proxy,omitempty"` - PortNoProxy int `toml:"port_no_proxy,omitempty"` + HostNoProxy string `toml:"host_no_proxy,omitempty" json:"host_no_proxy"` + PortNoProxy int `toml:"port_no_proxy,omitempty" json:"port_no_proxy"` } // RepositoriesCleanup configures repository synchronisation. @@ -527,11 +550,11 @@ type RepositoriesCleanup struct { // CheckInterval is a time period used to check if operation should be executed. // It is recommended to keep it less than run_interval configuration as some // nodes may be out of service, so they can be stale for too long. - CheckInterval duration.Duration `toml:"check_interval,omitempty"` + CheckInterval duration.Duration `toml:"check_interval,omitempty" json:"check_interval"` // RunInterval: the check runs if the previous operation was done at least RunInterval before. - RunInterval duration.Duration `toml:"run_interval,omitempty"` + RunInterval duration.Duration `toml:"run_interval,omitempty" json:"run_interval"` // RepositoriesInBatch is the number of repositories to pass as a batch for processing. - RepositoriesInBatch uint `toml:"repositories_in_batch,omitempty"` + RepositoriesInBatch uint `toml:"repositories_in_batch,omitempty" json:"repositories_in_batch"` } // Validate runs validation on all fields and compose all found errors. diff --git a/internal/praefect/config/config_test.go b/internal/praefect/config/config_test.go index e5d5dc4df..42ee6af60 100644 --- a/internal/praefect/config/config_test.go +++ b/internal/praefect/config/config_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "time" @@ -982,3 +983,268 @@ func TestConfig_ValidateV2(t *testing.T) { }, err) }) } + +func TestConfig_ConfigCommand(t *testing.T) { + t.Parallel() + + modifyDefaultConfig := func(modify func(cfg *Config)) Config { + cfg, err := FromReader(strings.NewReader("")) + require.NoError(t, err) + modify(&cfg) + return cfg + } + + writeScript := func(t *testing.T, script string) string { + return testhelper.WriteExecutable(t, + filepath.Join(testhelper.TempDir(t), "script"), + []byte("#!/bin/sh\n"+script), + ) + } + + type setupData struct { + cfg Config + expectedErr string + expectedCfg Config + } + + for _, tc := range []struct { + desc string + setup func(t *testing.T) setupData + }{ + { + desc: "nonexistent executable", + setup: func(t *testing.T) setupData { + return setupData{ + cfg: Config{ + ConfigCommand: "/does/not/exist", + }, + expectedErr: "running config command: fork/exec /does/not/exist: no such file or directory", + } + }, + }, + { + desc: "command points to non-executable file", + setup: func(t *testing.T) setupData { + cmd := filepath.Join(testhelper.TempDir(t), "script") + require.NoError(t, os.WriteFile(cmd, nil, perm.PrivateFile)) + + return setupData{ + cfg: Config{ + ConfigCommand: cmd, + }, + expectedErr: fmt.Sprintf( + "running config command: fork/exec %s: permission denied", cmd, + ), + } + }, + }, + { + desc: "executable returns error", + setup: func(t *testing.T) setupData { + return setupData{ + cfg: Config{ + ConfigCommand: writeScript(t, "echo error >&2 && exit 1"), + }, + expectedErr: "running config command: exit status 1, stderr: \"error\\n\"", + } + }, + }, + { + desc: "invalid JSON", + setup: func(t *testing.T) setupData { + return setupData{ + cfg: Config{ + ConfigCommand: writeScript(t, "echo 'this is not json'"), + }, + expectedErr: "unmarshalling generated config: invalid character 'h' in literal true (expecting 'r')", + } + }, + }, + { + desc: "mixed stdout and stderr", + setup: func(t *testing.T) setupData { + // We want to verify that we're able to correctly parse the output + // even if the process writes to both its stdout and stderr. + cmd := writeScript(t, "echo error >&2 && echo '{}'") + + return setupData{ + cfg: Config{ + ConfigCommand: cmd, + }, + expectedCfg: modifyDefaultConfig(func(cfg *Config) { + cfg.ConfigCommand = cmd + }), + } + }, + }, + { + desc: "empty script", + setup: func(t *testing.T) setupData { + cmd := writeScript(t, "echo '{}'") + + return setupData{ + cfg: Config{ + ConfigCommand: cmd, + }, + expectedCfg: modifyDefaultConfig(func(cfg *Config) { + cfg.ConfigCommand = cmd + }), + } + }, + }, + { + desc: "unknown value", + setup: func(t *testing.T) setupData { + cmd := writeScript(t, `echo '{"key_does_not_exist":"value"}'`) + + return setupData{ + cfg: Config{ + ConfigCommand: cmd, + }, + expectedCfg: modifyDefaultConfig(func(cfg *Config) { + cfg.ConfigCommand = cmd + }), + } + }, + }, + { + desc: "generated value", + setup: func(t *testing.T) setupData { + cmd := writeScript(t, `echo '{"socket_path": "value"}'`) + + return setupData{ + cfg: Config{ + ConfigCommand: cmd, + }, + expectedCfg: modifyDefaultConfig(func(cfg *Config) { + cfg.ConfigCommand = cmd + cfg.SocketPath = "value" + }), + } + }, + }, + { + desc: "overridden value", + setup: func(t *testing.T) setupData { + cmd := writeScript(t, `echo '{"socket_path": "overridden_value"}'`) + + return setupData{ + cfg: Config{ + ConfigCommand: cmd, + SocketPath: "initial_value", + }, + expectedCfg: modifyDefaultConfig(func(cfg *Config) { + cfg.ConfigCommand = cmd + cfg.SocketPath = "overridden_value" + }), + } + }, + }, + { + desc: "mixed configuration", + setup: func(t *testing.T) setupData { + cmd := writeScript(t, `echo '{"listen_addr": "listen_addr"}'`) + + return setupData{ + cfg: Config{ + ConfigCommand: cmd, + SocketPath: "socket_path", + }, + expectedCfg: modifyDefaultConfig(func(cfg *Config) { + cfg.ConfigCommand = cmd + cfg.SocketPath = "socket_path" + cfg.ListenAddr = "listen_addr" + }), + } + }, + }, + { + desc: "override default value", + setup: func(t *testing.T) setupData { + cmd := writeScript(t, `echo '{"storage": []}'`) + + return setupData{ + cfg: Config{ + ConfigCommand: cmd, + GracefulStopTimeout: duration.Duration(time.Second), + }, + expectedCfg: modifyDefaultConfig(func(cfg *Config) { + cfg.ConfigCommand = cmd + cfg.GracefulStopTimeout = duration.Duration(time.Second) + }), + } + }, + }, + { + desc: "subsections are being merged", + setup: func(t *testing.T) setupData { + cmd := writeScript(t, `cat <<-EOF + { + "failover": { + "bootstrap_interval": "13m" + } + } + EOF + `) + + return setupData{ + cfg: Config{ + ConfigCommand: cmd, + Failover: Failover{ + MonitorInterval: duration.Duration(14 * time.Minute), + }, + }, + expectedCfg: modifyDefaultConfig(func(cfg *Config) { + cfg.ConfigCommand = cmd + cfg.Failover.BootstrapInterval = duration.Duration(13 * time.Minute) + cfg.Failover.MonitorInterval = duration.Duration(14 * time.Minute) + }), + } + }, + }, + } { + tc := tc + + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + setup := tc.setup(t) + + t.Run("FromReader", func(t *testing.T) { + var cfgBuffer bytes.Buffer + require.NoError(t, toml.NewEncoder(&cfgBuffer).Encode(setup.cfg)) + + cfg, err := FromReader(&cfgBuffer) + + // We can't use `require.Equal()` for the error as it's basically impossible + // to reproduce the exact `exec.ExitError`. + if setup.expectedErr != "" { + require.EqualError(t, err, setup.expectedErr) + } else { + require.NoError(t, err) + } + require.Equal(t, setup.expectedCfg, cfg) + }) + + t.Run("FromFile", func(t *testing.T) { + cfgPath := filepath.Join(testhelper.TempDir(t), "praefect.toml") + + cfgFile, err := os.Create(cfgPath) + require.NoError(t, err) + require.NoError(t, toml.NewEncoder(cfgFile).Encode(setup.cfg)) + testhelper.MustClose(t, cfgFile) + + cfg, err := FromFile(cfgPath) + + // We can't use `require.Equal()` for the error as it's basically impossible + // to reproduce the exact `exec.ExitError`. + if setup.expectedErr != "" { + require.EqualError(t, err, setup.expectedErr) + } else { + require.NoError(t, err) + } + require.Equal(t, setup.expectedCfg, cfg) + }) + }) + } +} diff --git a/internal/praefect/config/node.go b/internal/praefect/config/node.go index 0f02ba251..5c32837c6 100644 --- a/internal/praefect/config/node.go +++ b/internal/praefect/config/node.go @@ -9,9 +9,9 @@ import ( // Node describes an address that serves a storage type Node struct { - Storage string `toml:"storage,omitempty"` - Address string `toml:"address,omitempty"` - Token string `toml:"token,omitempty"` + Storage string `toml:"storage,omitempty" json:"storage"` + Address string `toml:"address,omitempty" json:"address"` + Token string `toml:"token,omitempty" json:"token"` } //nolint:revive // This is unintentionally missing documentation. |