Skip to content

Commit bc8271b

Browse files
committed
fix: numeric type variables are parsed as strings
Signed-off-by: Daniel Hu <[email protected]>
1 parent be7a9be commit bc8271b

File tree

16 files changed

+326
-138
lines changed

16 files changed

+326
-138
lines changed

internal/pkg/configmanager/app.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package configmanager
33
import (
44
"fmt"
55

6+
"gopkg.in/yaml.v3"
7+
8+
"github.com/devstream-io/devstream/pkg/util/file"
69
"github.com/devstream-io/devstream/pkg/util/scm"
710
)
811

@@ -12,7 +15,7 @@ const (
1215

1316
type repoTemplate struct {
1417
*scm.SCMInfo `yaml:",inline"`
15-
Vars map[string]any `yaml:"vars"`
18+
Vars RawOptions `yaml:"vars"`
1619
}
1720

1821
type app struct {
@@ -24,6 +27,31 @@ type app struct {
2427
CDRawConfigs []pipelineRaw `yaml:"cd" mapstructure:"cd"`
2528
}
2629

30+
func getAppsFromConfigFileWithVarsRendered(fileBytes []byte, vars map[string]any) ([]*app, error) {
31+
yamlPath := "$.apps[*]"
32+
yamlStrArray, err := file.GetYamlNodeArrayByPath(fileBytes, yamlPath)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
if yamlStrArray == nil {
38+
return make([]*app, 0), nil
39+
}
40+
41+
yamlWithVars, err := renderConfigWithVariables(yamlStrArray.StrOrigin, vars)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
var retTApps = make([]*app, 0)
47+
err = yaml.Unmarshal(yamlWithVars, &retTApps)
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
return retTApps, nil
53+
}
54+
2755
// getAppPipelineTool generate ci/cd tools from app config
2856
func (a *app) generateCICDToolsFromAppConfig(templateMap map[string]string, appVars map[string]any) (Tools, error) {
2957
allPipelineRaw := append(a.CIRawConfigs, a.CDRawConfigs...)
@@ -57,11 +85,14 @@ func (a *app) getRepoTemplateTool() (*Tool, error) {
5785
if err != nil {
5886
return nil, fmt.Errorf("configmanager[app] parse repoTemplate failed: %w", err)
5987
}
88+
if a.RepoTemplate.Vars == nil {
89+
a.RepoTemplate.Vars = make(RawOptions)
90+
}
6091
return newTool(
6192
repoScaffoldingPluginName, a.Name, RawOptions{
6293
"destinationRepo": RawOptions(appRepo.Encode()),
6394
"sourceRepo": RawOptions(templateRepo.Encode()),
64-
"vars": RawOptions(a.RepoTemplate.Vars),
95+
"vars": a.RepoTemplate.Vars,
6596
},
6697
), nil
6798
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package configmanager
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
. "github.com/onsi/gomega"
6+
)
7+
8+
var _ = Describe("getToolsFromConfigFileWithVarsRendered", func() {
9+
const appsConfig = `---
10+
apps:
11+
- name: app-1
12+
cd:
13+
- type: template
14+
vars:
15+
app: [[ appName ]]
16+
`
17+
When("get apps from config file", func() {
18+
It("should return config with vars", func() {
19+
apps, err := getAppsFromConfigFileWithVarsRendered([]byte(appsConfig), map[string]any{"appName": interface{}("app-1")})
20+
Expect(err).NotTo(HaveOccurred())
21+
Expect(apps).NotTo(BeNil())
22+
Expect(len(apps)).To(Equal(1))
23+
Expect(len(apps[0].CDRawConfigs)).To(Equal(1))
24+
Expect(apps[0].CDRawConfigs[0].Vars["app"]).To(Equal(interface{}("app-1")))
25+
})
26+
})
27+
})

internal/pkg/configmanager/config.go

Lines changed: 23 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,19 @@ import (
1010

1111
// Config is a general config in DevStream.
1212
type Config struct {
13-
Config CoreConfig `yaml:"config"`
14-
Vars map[string]any `yaml:"vars"`
15-
Tools Tools `yaml:"tools"`
16-
Apps []app `yaml:"apps"`
17-
PipelineTemplates []pipelineTemplate `yaml:"pipelineTemplates"`
18-
pipelineTemplateMap map[string]string `yaml:"-"`
13+
Config CoreConfig `yaml:"config"`
14+
Vars map[string]any `yaml:"vars"`
15+
Tools Tools `yaml:"tools"`
16+
Apps []*app `yaml:"apps"`
17+
// We'll read the pipeline templates from config file and render it to pipelineTemplateMap in no time.
18+
//PipelineTemplates []*pipelineTemplate `yaml:"-"`
19+
pipelineTemplateMap map[string]string `yaml:"-"`
1920
}
2021

2122
func (c *Config) getToolsFromApps() (Tools, error) {
22-
err := c.renderPipelineTemplateMap()
23-
if err != nil {
24-
return nil, err
25-
}
26-
2723
var tools Tools
2824
for _, a := range c.Apps {
29-
appTools, err := c.getToolsWithVarsFromApp(a)
25+
appTools, err := c.getToolsFromApp(a)
3026
if err != nil {
3127
return nil, err
3228
}
@@ -35,92 +31,45 @@ func (c *Config) getToolsFromApps() (Tools, error) {
3531
return tools, nil
3632
}
3733

38-
func (c *Config) getToolsWithVarsFromApp(a app) (Tools, error) {
39-
appStr, err := yaml.Marshal(a)
34+
func (c *Config) getToolsFromApp(a *app) (Tools, error) {
35+
// generate app repo and template repo from scmInfo
36+
a.setDefault()
37+
repoScaffoldingTool, err := a.getRepoTemplateTool()
4038
if err != nil {
41-
return nil, err
39+
return nil, fmt.Errorf("app[%s] get repo failed: %w", a.Name, err)
4240
}
4341

44-
// 1. render appStr with Vars
45-
appRenderStr, err := renderConfigWithVariables(string(appStr), c.Vars)
42+
// get ci/cd pipelineTemplates
43+
appVars := a.Spec.merge(c.Vars)
44+
tools, err := a.generateCICDToolsFromAppConfig(c.pipelineTemplateMap, appVars)
4645
if err != nil {
47-
log.Debugf("configmanager/app %s render globalVars %+v failed", appRenderStr, c.Vars)
48-
return nil, fmt.Errorf("app render globalVars failed: %w", err)
49-
}
50-
51-
// 2. unmarshal app config for render pipelineTemplate
52-
var rawApp app
53-
if err = yaml.Unmarshal(appRenderStr, &rawApp); err != nil {
54-
return nil, fmt.Errorf("app parse yaml failed: %w", err)
55-
}
56-
57-
// 3. generate app repo and template repo from scmInfo
58-
rawApp.setDefault()
59-
repoScaffoldingTool, err := rawApp.getRepoTemplateTool()
60-
if err != nil {
61-
return nil, fmt.Errorf("app[%s] get repo failed: %w", rawApp.Name, err)
62-
}
63-
64-
// 4. get ci/cd pipelineTemplates
65-
appVars := rawApp.Spec.merge(c.Vars)
66-
tools, err := rawApp.generateCICDToolsFromAppConfig(c.pipelineTemplateMap, appVars)
67-
if err != nil {
68-
return nil, fmt.Errorf("app[%s] get pipeline tools failed: %w", rawApp.Name, err)
46+
return nil, fmt.Errorf("app[%s] get pipeline tools failed: %w", a.Name, err)
6947
}
7048
if repoScaffoldingTool != nil {
7149
tools = append(tools, repoScaffoldingTool)
7250
}
7351

74-
// 5. all tools from apps should depend on the original tools,
75-
// because dtm will execute all original tools first, then execute all tools from apps
52+
// all tools from apps should depend on the original tools,
53+
// because dtm will execute all original tools first, then execute all tools from apps
7654
for _, toolFromApps := range tools {
7755
for _, t := range c.Tools {
7856
toolFromApps.DependsOn = append(toolFromApps.DependsOn, t.KeyWithNameAndInstanceID())
7957
}
8058
}
8159

82-
log.Debugf("Have got %d tools from app %s.", len(tools), rawApp.Name)
60+
log.Debugf("Have got %d tools from app %s.", len(tools), a.Name)
8361
for i, t := range tools {
8462
log.Debugf("Tool %d: %v", i+1, t)
8563
}
8664

8765
return tools, nil
8866
}
8967

90-
func (c *Config) renderToolsWithVars() error {
91-
toolsStr, err := yaml.Marshal(c.Tools)
92-
if err != nil {
93-
return err
94-
}
95-
96-
toolsStrWithVars, err := renderConfigWithVariables(string(toolsStr), c.Vars)
97-
if err != nil {
98-
return err
99-
}
100-
101-
var tools Tools
102-
if err = yaml.Unmarshal(toolsStrWithVars, &tools); err != nil {
103-
return err
104-
}
105-
c.Tools = tools
106-
107-
return nil
108-
}
109-
110-
func (c *Config) renderPipelineTemplateMap() error {
111-
c.pipelineTemplateMap = make(map[string]string)
112-
for _, pt := range c.PipelineTemplates {
113-
tplBytes, err := yaml.Marshal(pt)
114-
if err != nil {
115-
return err
116-
}
117-
c.pipelineTemplateMap[pt.Name] = string(tplBytes)
118-
}
119-
return nil
120-
}
121-
12268
func (c *Config) renderInstanceIDtoOptions() {
12369
for _, t := range c.Tools {
70+
if t.Options == nil {
71+
t.Options = make(RawOptions)
72+
}
12473
t.Options["instanceID"] = t.InstanceID
12574
}
12675
}
Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package configmanager
22

33
import (
4+
"fmt"
45
"os"
5-
"regexp"
6-
7-
"gopkg.in/yaml.v3"
8-
9-
"github.com/devstream-io/devstream/pkg/util/log"
106
)
117

128
// Manager is used to load the config file from the ConfigFilePath and finally get the Config object.
@@ -28,7 +24,7 @@ func NewManager(configFilePath string) *Manager {
2824
// 3. Validation.
2925
func (m *Manager) LoadConfig() (*Config, error) {
3026
// step 1
31-
c, err := m.getConfigFromFile()
27+
c, err := m.getConfigFromFileWithGlobalVars()
3228
if err != nil {
3329
return nil, err
3430
}
@@ -39,10 +35,6 @@ func (m *Manager) LoadConfig() (*Config, error) {
3935
return nil, err
4036
}
4137

42-
err = c.renderToolsWithVars()
43-
if err != nil {
44-
return nil, err
45-
}
4638
c.Tools = append(c.Tools, appTools...)
4739
c.renderInstanceIDtoOptions()
4840

@@ -54,26 +46,54 @@ func (m *Manager) LoadConfig() (*Config, error) {
5446
return c, nil
5547
}
5648

57-
// getConfigFromFile gets Config from the config file specified by Manager.ConfigFilePath
58-
func (m *Manager) getConfigFromFile() (*Config, error) {
49+
// getConfigFromFileWithGlobalVars gets Config from the config file specified by Manager.ConfigFilePath, then:
50+
// 1. render the global variables to Config.Tools and Config.Apps
51+
// 2. transfer the PipelineTemplates to Config.pipelineTemplateMap, it's map[string]string type.
52+
// We can't render the original config file to Config.PipelineTemplates directly for the:
53+
// 1. variables rendered must be before the yaml.Unmarshal() called for the [[ foo ]] will be treated as a two-dimensional array by the yaml parser;
54+
// 2. the variables used([[ foo ]]) in the Config.PipelineTemplates can be defined in the Config.Apps or Config.Vars;
55+
// 3. pipeline templates are used in apps, so it would be more appropriate to refer to pipeline templates when dealing with apps
56+
func (m *Manager) getConfigFromFileWithGlobalVars() (*Config, error) {
5957
configBytes, err := os.ReadFile(m.ConfigFilePath)
6058
if err != nil {
6159
return nil, err
6260
}
6361

64-
configBytesEscaped := escapeBrackets(configBytes)
62+
// global variables
63+
vars, err := getVarsFromConfigFile(configBytes)
64+
if err != nil {
65+
return nil, fmt.Errorf("failed to get variables from config file. Error: %w", err)
66+
}
6567

66-
var c Config
67-
if err = yaml.Unmarshal(configBytesEscaped, &c); err != nil {
68-
log.Errorf("Please verify the format of your config. Error: %s.", err)
69-
return nil, err
68+
// tools with global variables rendered
69+
tools, err := getToolsFromConfigFileWithVarsRendered(configBytes, vars)
70+
if err != nil {
71+
return nil, fmt.Errorf("failed to get tools from config file. Error: %w", err)
7072
}
7173

72-
return &c, nil
73-
}
74+
// apps with global variables rendered
75+
apps, err := getAppsFromConfigFileWithVarsRendered(configBytes, vars)
76+
if err != nil {
77+
return nil, fmt.Errorf("failed to get apps from config file. Error: %w", err)
78+
}
79+
80+
// pipelineTemplateMap transfer from PipelineTemplates
81+
pipelineTemplateMap, err := getPipelineTemplatesMapFromConfigFile(configBytes)
82+
if err != nil {
83+
return nil, fmt.Errorf("failed to get pipelineTemplatesMap from config file. Error: %w", err)
84+
}
85+
86+
// coreConfig without any changes
87+
coreConfig, err := getCoreConfigFromConfigFile(configBytes)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to get coreConfig from config file. Error: %w", err)
90+
}
7491

75-
// escapeBrackets is used to escape []byte(": [[xxx]]xxx\n") to []byte(": \"[[xxx]]\"xxx\n")
76-
func escapeBrackets(param []byte) []byte {
77-
re := regexp.MustCompile(`([^:]+:)(\s*)((\[\[[^\]]+\]\][^\s\[]*)+)[^\s#\n]*`)
78-
return re.ReplaceAll(param, []byte("$1$2\"$3\""))
92+
return &Config{
93+
Config: *coreConfig,
94+
Vars: vars,
95+
Tools: tools,
96+
Apps: apps,
97+
pipelineTemplateMap: pipelineTemplateMap,
98+
}, nil
7999
}

0 commit comments

Comments
 (0)