Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWill Chandler <wchandler@gitlab.com>2023-08-01 21:46:29 +0300
committerWill Chandler <wchandler@gitlab.com>2023-08-01 21:46:29 +0300
commit108242aa2fe7ce3a98f315fc73138630f9117a6e (patch)
tree490c6a9a9c0b3e94746c4e23a257876a03c11fff
parent16578b4560ee72b4489937a44872803b1876c11e (diff)
parent5ebe6f6d13f1182fdf6ac3e27b0441fc6085eace (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.go6
-rw-r--r--internal/gitaly/config/prometheus/config.go4
-rw-r--r--internal/gitaly/config/sentry/sentry.go4
-rw-r--r--internal/praefect/config/config.go149
-rw-r--r--internal/praefect/config/config_test.go266
-rw-r--r--internal/praefect/config/node.go6
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.