Skip to content

Commit ba18f13

Browse files
authored
feat: provider config parsers are able to resolve filepaths relative to the config file path (containers#412)
Signed-off-by: Calum Murray <[email protected]>
1 parent e420f16 commit ba18f13

File tree

3 files changed

+93
-9
lines changed

3 files changed

+93
-9
lines changed

pkg/config/config.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package config
22

33
import (
44
"bytes"
5+
"context"
56
"fmt"
67
"os"
8+
"path/filepath"
79

810
"github.com/BurntSushi/toml"
911
)
@@ -68,6 +70,9 @@ type StaticConfig struct {
6870

6971
// Internal: parsed provider configs (not exposed to TOML package)
7072
parsedClusterProviderConfigs map[string]ProviderConfig
73+
74+
// Internal: the config.toml directory, to help resolve relative file paths
75+
configDirPath string
7176
}
7277

7378
type GroupVersionKind struct {
@@ -76,23 +81,48 @@ type GroupVersionKind struct {
7681
Kind string `toml:"kind,omitempty"`
7782
}
7883

79-
// Read reads the toml file and returns the StaticConfig.
80-
func Read(configPath string) (*StaticConfig, error) {
84+
type ReadConfigOpt func(cfg *StaticConfig)
85+
86+
func withDirPath(path string) ReadConfigOpt {
87+
return func(cfg *StaticConfig) {
88+
cfg.configDirPath = path
89+
}
90+
}
91+
92+
// Read reads the toml file and returns the StaticConfig, with any opts applied.
93+
func Read(configPath string, opts ...ReadConfigOpt) (*StaticConfig, error) {
8194
configData, err := os.ReadFile(configPath)
8295
if err != nil {
8396
return nil, err
8497
}
85-
return ReadToml(configData)
98+
99+
// get and save the absolute dir path to the config file, so that other config parsers can use it
100+
absPath, err := filepath.Abs(configPath)
101+
if err != nil {
102+
return nil, fmt.Errorf("failed to resolve absolute path to config file: %w", err)
103+
}
104+
dirPath := filepath.Dir(absPath)
105+
106+
cfg, err := ReadToml(configData, append(opts, withDirPath(dirPath))...)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
return cfg, nil
86112
}
87113

88-
// ReadToml reads the toml data and returns the StaticConfig.
89-
func ReadToml(configData []byte) (*StaticConfig, error) {
114+
// ReadToml reads the toml data and returns the StaticConfig, with any opts applied
115+
func ReadToml(configData []byte, opts ...ReadConfigOpt) (*StaticConfig, error) {
90116
config := Default()
91117
md, err := toml.NewDecoder(bytes.NewReader(configData)).Decode(config)
92118
if err != nil {
93119
return nil, err
94120
}
95121

122+
for _, opt := range opts {
123+
opt(config)
124+
}
125+
96126
if err := config.parseClusterProviderConfigs(md); err != nil {
97127
return nil, err
98128
}
@@ -111,13 +141,15 @@ func (c *StaticConfig) parseClusterProviderConfigs(md toml.MetaData) error {
111141
c.parsedClusterProviderConfigs = make(map[string]ProviderConfig, len(c.ClusterProviderConfigs))
112142
}
113143

144+
ctx := withConfigDirPath(context.Background(), c.configDirPath)
145+
114146
for strategy, primitive := range c.ClusterProviderConfigs {
115147
parser, ok := getProviderConfigParser(strategy)
116148
if !ok {
117149
continue
118150
}
119151

120-
providerConfig, err := parser(primitive, md)
152+
providerConfig, err := parser(ctx, primitive, md)
121153
if err != nil {
122154
return fmt.Errorf("failed to parse config for ClusterProvider '%s': %w", strategy, err)
123155
}

pkg/config/provider_config.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config
22

33
import (
4+
"context"
45
"fmt"
56

67
"github.com/BurntSushi/toml"
@@ -12,7 +13,27 @@ type ProviderConfig interface {
1213
Validate() error
1314
}
1415

15-
type ProviderConfigParser func(primitive toml.Primitive, md toml.MetaData) (ProviderConfig, error)
16+
type ProviderConfigParser func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (ProviderConfig, error)
17+
18+
type configDirPathKey struct{}
19+
20+
func withConfigDirPath(ctx context.Context, dirPath string) context.Context {
21+
return context.WithValue(ctx, configDirPathKey{}, dirPath)
22+
}
23+
24+
func ConfigDirPathFromContext(ctx context.Context) string {
25+
val := ctx.Value(configDirPathKey{})
26+
27+
if val == nil {
28+
return ""
29+
}
30+
31+
if strVal, ok := val.(string); ok {
32+
return strVal
33+
}
34+
35+
return ""
36+
}
1637

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

pkg/config/provider_config_test.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package config
22

33
import (
4+
"context"
45
"errors"
6+
"path/filepath"
57
"testing"
68

79
"github.com/BurntSushi/toml"
@@ -42,7 +44,7 @@ func (p *ProviderConfigForTest) Validate() error {
4244
return nil
4345
}
4446

45-
func providerConfigForTestParser(primitive toml.Primitive, md toml.MetaData) (ProviderConfig, error) {
47+
func providerConfigForTestParser(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (ProviderConfig, error) {
4648
var providerConfigForTest ProviderConfigForTest
4749
if err := md.PrimitiveDecode(primitive, &providerConfigForTest); err != nil {
4850
return nil, err
@@ -131,7 +133,7 @@ func (s *ProviderConfigSuite) TestReadConfigUnregisteredProviderConfig() {
131133
}
132134

133135
func (s *ProviderConfigSuite) TestReadConfigParserError() {
134-
RegisterProviderConfig("test", func(primitive toml.Primitive, md toml.MetaData) (ProviderConfig, error) {
136+
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (ProviderConfig, error) {
135137
return nil, errors.New("parser error forced by test")
136138
})
137139
invalidConfigPath := s.writeConfig(`
@@ -152,6 +154,35 @@ func (s *ProviderConfigSuite) TestReadConfigParserError() {
152154
})
153155
}
154156

157+
func (s *ProviderConfigSuite) TestConfigDirPathInContext() {
158+
var capturedDirPath string
159+
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (ProviderConfig, error) {
160+
capturedDirPath = ConfigDirPathFromContext(ctx)
161+
var providerConfigForTest ProviderConfigForTest
162+
if err := md.PrimitiveDecode(primitive, &providerConfigForTest); err != nil {
163+
return nil, err
164+
}
165+
return &providerConfigForTest, nil
166+
})
167+
configPath := s.writeConfig(`
168+
cluster_provider_strategy = "test"
169+
[cluster_provider_configs.test]
170+
bool_prop = true
171+
str_prop = "a string"
172+
int_prop = 42
173+
`)
174+
175+
absConfigPath, err := filepath.Abs(configPath)
176+
s.Require().NoError(err, "test error: getting the absConfigPath should not fail")
177+
178+
_, err = Read(configPath)
179+
s.Run("provides config directory path in context to parser", func() {
180+
s.Require().NoError(err, "Expected no error reading config")
181+
s.NotEmpty(capturedDirPath, "Expected non-empty directory path in context")
182+
s.Equal(filepath.Dir(absConfigPath), capturedDirPath, "Expected directory path to match config file directory")
183+
})
184+
}
185+
155186
func TestProviderConfig(t *testing.T) {
156187
suite.Run(t, new(ProviderConfigSuite))
157188
}

0 commit comments

Comments
 (0)