Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package config

import (
"bytes"
"fmt"
"os"

"github.com/BurntSushi/toml"
Expand Down Expand Up @@ -59,8 +61,13 @@ type StaticConfig struct {
// If set to "kubeconfig", the clusters will be loaded from those in the kubeconfig.
// If set to "in-cluster", the server will use the in cluster config
ClusterProviderStrategy string `toml:"cluster_provider_strategy,omitempty"`
// ClusterContexts is which context should be used for each cluster
ClusterContexts map[string]string `toml:"cluster_contexts"`

// ClusterProvider-specific configurations
// This map holds raw TOML primitives that will be parsed by registered provider parsers
ClusterProviderConfigs map[string]toml.Primitive `toml:"cluster_provider_configs,omitempty"`

// Internal: parsed provider configs (not exposed to TOML package)
parsedClusterProviderConfigs map[string]ProviderConfig
}

func Default() *StaticConfig {
Expand Down Expand Up @@ -88,8 +95,46 @@ func Read(configPath string) (*StaticConfig, error) {
// ReadToml reads the toml data and returns the StaticConfig.
func ReadToml(configData []byte) (*StaticConfig, error) {
config := Default()
if err := toml.Unmarshal(configData, config); err != nil {
md, err := toml.NewDecoder(bytes.NewReader(configData)).Decode(config)
if err != nil {
return nil, err
}

if err := config.parseClusterProviderConfigs(md); err != nil {
return nil, err
}

return config, nil
}

func (c *StaticConfig) GetProviderConfig(strategy string) (ProviderConfig, bool) {
config, ok := c.parsedClusterProviderConfigs[strategy]

return config, ok
}

func (c *StaticConfig) parseClusterProviderConfigs(md toml.MetaData) error {
if c.parsedClusterProviderConfigs == nil {
c.parsedClusterProviderConfigs = make(map[string]ProviderConfig, len(c.ClusterProviderConfigs))
}

for strategy, primitive := range c.ClusterProviderConfigs {
parser, ok := getProviderConfigParser(strategy)
if !ok {
continue
}

providerConfig, err := parser(primitive, md)
if err != nil {
return fmt.Errorf("failed to parse config for ClusterProvider '%s': %w", strategy, err)
}

if err := providerConfig.Validate(); err != nil {
return fmt.Errorf("invalid config file for ClusterProvider '%s': %w", strategy, err)
}

c.parsedClusterProviderConfigs[strategy] = providerConfig
}

return nil
}
28 changes: 16 additions & 12 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,25 @@ import (
"github.com/stretchr/testify/suite"
)

type ConfigSuite struct {
type BaseConfigSuite struct {
suite.Suite
}

func (s *BaseConfigSuite) writeConfig(content string) string {
s.T().Helper()
tempDir := s.T().TempDir()
path := filepath.Join(tempDir, "config.toml")
err := os.WriteFile(path, []byte(content), 0644)
if err != nil {
s.T().Fatalf("Failed to write config file %s: %v", path, err)
}
return path
}

type ConfigSuite struct {
BaseConfigSuite
}

func (s *ConfigSuite) TestReadConfigMissingFile() {
config, err := Read("non-existent-config.toml")
s.Run("returns error for missing file", func() {
Expand Down Expand Up @@ -159,17 +174,6 @@ func (s *ConfigSuite) TestReadConfigValidPreservesDefaultsForMissingFields() {
})
}

func (s *ConfigSuite) writeConfig(content string) string {
s.T().Helper()
tempDir := s.T().TempDir()
path := filepath.Join(tempDir, "config.toml")
err := os.WriteFile(path, []byte(content), 0644)
if err != nil {
s.T().Fatalf("Failed to write config file %s: %v", path, err)
}
return path
}

func TestConfig(t *testing.T) {
suite.Run(t, new(ConfigSuite))
}
33 changes: 33 additions & 0 deletions pkg/config/provider_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package config

import (
"fmt"

"github.com/BurntSushi/toml"
)

// ProviderConfig is the interface that all provider-specific configurations must implement.
// Each provider registers a factory function to parse its config from TOML primitives
type ProviderConfig interface {
Validate() error
}

type ProviderConfigParser func(primitive toml.Primitive, md toml.MetaData) (ProviderConfig, error)

var (
providerConfigParsers = make(map[string]ProviderConfigParser)
)

func RegisterProviderConfig(strategy string, parser ProviderConfigParser) {
if _, exists := providerConfigParsers[strategy]; exists {
panic(fmt.Sprintf("provider config parser already registered for strategy '%s'", strategy))
}

providerConfigParsers[strategy] = parser
}

func getProviderConfigParser(strategy string) (ProviderConfigParser, bool) {
provider, ok := providerConfigParsers[strategy]

return provider, ok
}
157 changes: 157 additions & 0 deletions pkg/config/provider_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package config

import (
"errors"
"testing"

"github.com/BurntSushi/toml"
"github.com/stretchr/testify/suite"
)

type ProviderConfigSuite struct {
BaseConfigSuite
originalProviderConfigParsers map[string]ProviderConfigParser
}

func (s *ProviderConfigSuite) SetupTest() {
s.originalProviderConfigParsers = make(map[string]ProviderConfigParser)
for k, v := range providerConfigParsers {
s.originalProviderConfigParsers[k] = v
}
}

func (s *ProviderConfigSuite) TearDownTest() {
providerConfigParsers = make(map[string]ProviderConfigParser)
for k, v := range s.originalProviderConfigParsers {
providerConfigParsers[k] = v
}
}

type ProviderConfigForTest struct {
BoolProp bool `toml:"bool_prop"`
StrProp string `toml:"str_prop"`
IntProp int `toml:"int_prop"`
}

var _ ProviderConfig = (*ProviderConfigForTest)(nil)

func (p *ProviderConfigForTest) Validate() error {
if p.StrProp == "force-error" {
return errors.New("validation error forced by test")
}
return nil
}

func providerConfigForTestParser(primitive toml.Primitive, md toml.MetaData) (ProviderConfig, error) {
var providerConfigForTest ProviderConfigForTest
if err := md.PrimitiveDecode(primitive, &providerConfigForTest); err != nil {
return nil, err
}
return &providerConfigForTest, nil
}

func (s *ProviderConfigSuite) TestRegisterProviderConfig() {
s.Run("panics when registering duplicate provider config parser", func() {
s.Panics(func() {
RegisterProviderConfig("test", providerConfigForTestParser)
RegisterProviderConfig("test", providerConfigForTestParser)
}, "Expected panic when registering duplicate provider config parser")
})
}

func (s *ProviderConfigSuite) TestReadConfigValid() {
RegisterProviderConfig("test", providerConfigForTestParser)
validConfigPath := s.writeConfig(`
cluster_provider_strategy = "test"
[cluster_provider_configs.test]
bool_prop = true
str_prop = "a string"
int_prop = 42
`)

config, err := Read(validConfigPath)
s.Run("returns no error for valid file with registered provider config", func() {
s.Require().NoError(err, "Expected no error for valid file, got %v", err)
})
s.Run("returns config for valid file with registered provider config", func() {
s.Require().NotNil(config, "Expected non-nil config for valid file")
})
s.Run("parses provider config correctly", func() {
providerConfig, ok := config.GetProviderConfig("test")
s.Require().True(ok, "Expected to find provider config for strategy 'test'")
s.Require().NotNil(providerConfig, "Expected non-nil provider config for strategy 'test'")
testProviderConfig, ok := providerConfig.(*ProviderConfigForTest)
s.Require().True(ok, "Expected provider config to be of type *ProviderConfigForTest")
s.Equal(true, testProviderConfig.BoolProp, "Expected BoolProp to be true")
s.Equal("a string", testProviderConfig.StrProp, "Expected StrProp to be 'a string'")
s.Equal(42, testProviderConfig.IntProp, "Expected IntProp to be 42")
})
}

func (s *ProviderConfigSuite) TestReadConfigInvalidProviderConfig() {
RegisterProviderConfig("test", providerConfigForTestParser)
invalidConfigPath := s.writeConfig(`
cluster_provider_strategy = "test"
[cluster_provider_configs.test]
bool_prop = true
str_prop = "force-error"
int_prop = 42
`)

config, err := Read(invalidConfigPath)
s.Run("returns error for invalid provider config", func() {
s.Require().NotNil(err, "Expected error for invalid provider config, got nil")
s.ErrorContains(err, "validation error forced by test", "Expected validation error from provider config")
})
s.Run("returns nil config for invalid provider config", func() {
s.Nil(config, "Expected nil config for invalid provider config")
})
}

func (s *ProviderConfigSuite) TestReadConfigUnregisteredProviderConfig() {
invalidConfigPath := s.writeConfig(`
cluster_provider_strategy = "unregistered"
[cluster_provider_configs.unregistered]
bool_prop = true
str_prop = "a string"
int_prop = 42
`)

config, err := Read(invalidConfigPath)
s.Run("returns no error for unregistered provider config", func() {
s.Require().NoError(err, "Expected no error for unregistered provider config, got %v", err)
})
s.Run("returns config for unregistered provider config", func() {
s.Require().NotNil(config, "Expected non-nil config for unregistered provider config")
})
s.Run("does not parse unregistered provider config", func() {
_, ok := config.GetProviderConfig("unregistered")
s.Require().False(ok, "Expected no provider config for unregistered strategy")
})
}

func (s *ProviderConfigSuite) TestReadConfigParserError() {
RegisterProviderConfig("test", func(primitive toml.Primitive, md toml.MetaData) (ProviderConfig, error) {
return nil, errors.New("parser error forced by test")
})
invalidConfigPath := s.writeConfig(`
cluster_provider_strategy = "test"
[cluster_provider_configs.test]
bool_prop = true
str_prop = "a string"
int_prop = 42
`)

config, err := Read(invalidConfigPath)
s.Run("returns error for provider config parser error", func() {
s.Require().NotNil(err, "Expected error for provider config parser error, got nil")
s.ErrorContains(err, "parser error forced by test", "Expected parser error from provider config")
})
s.Run("returns nil config for provider config parser error", func() {
s.Nil(config, "Expected nil config for provider config parser error")
})
}

func TestProviderConfig(t *testing.T) {
suite.Run(t, new(ProviderConfigSuite))
}
Loading