Skip to content

Commit 593ccc2

Browse files
authored
Merge pull request #1315 from merico-dev/feat-apps-cicd-pass-vars
feat: pass variable from ci to cd
2 parents 2349a04 + e093747 commit 593ccc2

26 files changed

+690
-290
lines changed

docs/plugins/argocdapp.md

+29
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,32 @@ In the example above:
7272
- We used `repo-scaffolding.golang-github`'s output as input for the `githubactions-golang` plugin.
7373

7474
Pay attention to the `${{ xxx }}` part in the example. `${{ TOOL_NAME.PLUGIN.outputs.var}}` is the syntax for using an output.
75+
76+
## Automatically Create Helm Configuration
77+
78+
This plugin can push helm configuration automatically when your source.path helm config not exist, so you can use this plugin with helm configured alreay. For example:
79+
80+
```yaml
81+
---
82+
tools:
83+
- name: go-webapp-argocd-deploy
84+
plugin: argocdapp
85+
dependsOn: ["repo-scaffolding.golang-github"]
86+
options:
87+
app:
88+
name: hello
89+
namespace: argocd
90+
destination:
91+
server: https://kubernetes.default.svc
92+
namespace: default
93+
source:
94+
valuefile: values.yaml
95+
path: charts/go-hello-http
96+
repoURL: ${{repo-scaffolding.golang-github.outputs.repoURL}}
97+
imageRepo:
98+
url: http://test.barbor.com/library
99+
user: test_owner
100+
tag: "1.0.0"
101+
```
102+
103+
This config will push default helm config to repo `${{repo-scaffolding.golang-github.outputs.repoURL}}`, and the generated config will use image `http://test.barbor.com/library/test_owner/hello:1.0.0` as inital image for helm.

internal/pkg/configmanager/app.go

+59-33
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/devstream-io/devstream/pkg/util/log"
77
"github.com/devstream-io/devstream/pkg/util/scm"
8+
"github.com/devstream-io/devstream/pkg/util/scm/git"
89
)
910

1011
const (
@@ -23,41 +24,49 @@ type app struct {
2324
RepoTemplate *repoTemplate `yaml:"repoTemplate" mapstructure:"repoTemplate"`
2425
CIRawConfigs []pipelineRaw `yaml:"ci" mapstructure:"ci"`
2526
CDRawConfigs []pipelineRaw `yaml:"cd" mapstructure:"cd"`
27+
28+
// these two variables is used internal for convince
29+
// repoInfo is generated from Repo field with setDefault method
30+
repoInfo *git.RepoInfo `yaml:"-" mapstructure:"-"`
31+
// repoTemplateInfo is generated from RepoTemplate field with setDefault method
32+
repoTemplateInfo *git.RepoInfo `yaml:"-" mapstructure:"-"`
2633
}
2734

2835
func (a *app) getTools(vars map[string]any, templateMap map[string]string) (Tools, error) {
29-
// generate app repo and template repo from scmInfo
30-
a.setDefault()
31-
repoScaffoldingTool, err := a.getRepoTemplateTool()
32-
if err != nil {
33-
return nil, fmt.Errorf("app[%s] can't get valid repo config: %w", a.Name, err)
36+
// 1. set app default field repoInfo and repoTemplateInfo
37+
if err := a.setDefault(); err != nil {
38+
return nil, err
3439
}
3540

36-
// get ci/cd pipelineTemplates
41+
// 2. get ci/cd pipelineTemplates
3742
appVars := a.Spec.merge(vars)
3843
tools, err := a.generateCICDTools(templateMap, appVars)
3944
if err != nil {
4045
return nil, fmt.Errorf("app[%s] get pipeline tools failed: %w", a.Name, err)
4146
}
47+
48+
// 3. generate app repo and template repo from scmInfo
49+
repoScaffoldingTool := a.generateRepoTemplateTool()
4250
if repoScaffoldingTool != nil {
4351
tools = append(tools, repoScaffoldingTool)
4452
}
45-
4653
log.Debugf("Have got %d tools from app %s.", len(tools), a.Name)
47-
4854
return tools, nil
4955
}
5056

51-
// getAppPipelineTool generate ci/cd tools from app config
57+
// generateAppPipelineTool generate ci/cd tools from app config
5258
func (a *app) generateCICDTools(templateMap map[string]string, appVars map[string]any) (Tools, error) {
5359
allPipelineRaw := append(a.CIRawConfigs, a.CDRawConfigs...)
5460
var tools Tools
61+
// pipelineGlobalVars is used to pass variable from ci/cd pipelines
62+
pipelineGlobalVars := a.newPipelineGlobalOptionFromApp()
5563
for _, p := range allPipelineRaw {
5664
t, err := p.getPipelineTemplate(templateMap, appVars)
5765
if err != nil {
5866
return nil, err
5967
}
60-
pipelineTool, err := t.generatePipelineTool(a)
68+
t.updatePipelineVars(pipelineGlobalVars)
69+
pipelineTool, err := t.generatePipelineTool(pipelineGlobalVars)
6170
if err != nil {
6271
return nil, err
6372
}
@@ -67,41 +76,48 @@ func (a *app) generateCICDTools(templateMap map[string]string, appVars map[strin
6776
return tools, nil
6877
}
6978

70-
// getRepoTemplateTool will use repo-scaffolding plugin for app
71-
func (a *app) getRepoTemplateTool() (*Tool, error) {
72-
if a.Repo == nil {
73-
return nil, fmt.Errorf("app.repo field can't be empty")
74-
}
75-
appRepo, err := a.Repo.BuildRepoInfo()
76-
if err != nil {
77-
return nil, fmt.Errorf("configmanager[app] parse repo failed: %w", err)
78-
}
79-
if a.RepoTemplate != nil {
79+
// generateRepoTemplateTool will use repo-scaffolding plugin for app
80+
func (a *app) generateRepoTemplateTool() *Tool {
81+
if a.repoTemplateInfo != nil {
82+
templateVars := make(RawOptions)
8083
// templateRepo doesn't need auth info
81-
templateRepo, err := a.RepoTemplate.BuildRepoInfo()
82-
templateRepo.NeedAuth = false
83-
if err != nil {
84-
return nil, fmt.Errorf("configmanager[app] parse repoTemplate failed: %w", err)
85-
}
8684
if a.RepoTemplate.Vars == nil {
87-
a.RepoTemplate.Vars = make(RawOptions)
85+
templateVars = make(RawOptions)
8886
}
8987
return newTool(
9088
repoScaffoldingPluginName, a.Name, RawOptions{
91-
"destinationRepo": RawOptions(appRepo.Encode()),
92-
"sourceRepo": RawOptions(templateRepo.Encode()),
93-
"vars": a.RepoTemplate.Vars,
89+
"destinationRepo": RawOptions(a.repoInfo.Encode()),
90+
"sourceRepo": RawOptions(a.repoTemplateInfo.Encode()),
91+
"vars": templateVars,
9492
},
95-
), nil
93+
)
9694
}
97-
return nil, nil
95+
return nil
9896
}
9997

10098
// setDefault will set repoName to appName if repo.name field is empty
101-
func (a *app) setDefault() {
102-
if a.Repo != nil && a.Repo.Name == "" {
99+
func (a *app) setDefault() error {
100+
if a.Repo == nil {
101+
return fmt.Errorf("configmanager[app] is invalid, repo field must be configured")
102+
}
103+
if a.Repo.Name == "" {
103104
a.Repo.Name = a.Name
104105
}
106+
appRepo, err := a.Repo.BuildRepoInfo()
107+
if err != nil {
108+
return fmt.Errorf("configmanager[app] parse repo failed: %w", err)
109+
}
110+
a.repoInfo = appRepo
111+
if a.RepoTemplate != nil {
112+
// templateRepo doesn't need auth info
113+
templateRepo, err := a.RepoTemplate.BuildRepoInfo()
114+
if err != nil {
115+
return fmt.Errorf("configmanager[app] parse repoTemplate failed: %w", err)
116+
}
117+
templateRepo.NeedAuth = false
118+
a.repoTemplateInfo = templateRepo
119+
}
120+
return nil
105121
}
106122

107123
// since all plugin depends on code is deployed, get dependsOn for repoTemplate
@@ -113,3 +129,13 @@ func (a *app) getRepoTemplateDependants() []string {
113129
}
114130
return dependsOn
115131
}
132+
133+
// newPipelineGlobalOptionFromApp generate pipeline options used for pipeline option configuration
134+
func (a *app) newPipelineGlobalOptionFromApp() *pipelineGlobalOption {
135+
return &pipelineGlobalOption{
136+
RepoInfo: a.repoInfo,
137+
AppSpec: a.Spec,
138+
Scm: a.Repo,
139+
AppName: a.Name,
140+
}
141+
}

internal/pkg/configmanager/app_test.go

+52-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package configmanager
22

33
import (
4-
"fmt"
5-
64
. "github.com/onsi/ginkgo/v2"
75
. "github.com/onsi/gomega"
86

@@ -29,7 +27,7 @@ var _ = Describe("app struct", func() {
2927
It("should return error", func() {
3028
_, err := a.getTools(vars, templateMap)
3129
Expect(err).Should(HaveOccurred())
32-
Expect(err.Error()).Should(ContainSubstring(fmt.Sprintf("app[%s] can't get valid repo config", appName)))
30+
Expect(err.Error()).Should(ContainSubstring("configmanager[app] is invalid, repo field must be configured"))
3331
})
3432
})
3533
When("ci/cd template is not valid", func() {
@@ -52,6 +50,57 @@ var _ = Describe("app struct", func() {
5250
Expect(err.Error()).Should(ContainSubstring("not found in pipelineTemplates"))
5351
})
5452
})
53+
When("app repo template is empty", func() {
54+
BeforeEach(func() {
55+
a = &app{
56+
Name: appName,
57+
Repo: &scm.SCMInfo{
58+
CloneURL: "http://test.com/test/test_app",
59+
},
60+
}
61+
})
62+
It("should return empty tools", func() {
63+
tools, err := a.getTools(vars, templateMap)
64+
Expect(err).ShouldNot(HaveOccurred())
65+
Expect(len(tools)).Should(Equal(0))
66+
})
67+
})
68+
When("repo url is not valid", func() {
69+
BeforeEach(func() {
70+
a = &app{
71+
Name: appName,
72+
Repo: &scm.SCMInfo{
73+
CloneURL: "not_valid_url",
74+
},
75+
}
76+
})
77+
It("should return empty tools", func() {
78+
_, err := a.getTools(vars, templateMap)
79+
Expect(err).Should(HaveOccurred())
80+
Expect(err.Error()).Should(ContainSubstring("configmanager[app] parse repo failed"))
81+
})
82+
})
83+
When("template repo url is not valid", func() {
84+
BeforeEach(func() {
85+
a = &app{
86+
Name: appName,
87+
RepoTemplate: &repoTemplate{
88+
SCMInfo: &scm.SCMInfo{
89+
CloneURL: "not_valid_url",
90+
},
91+
},
92+
Repo: &scm.SCMInfo{
93+
CloneURL: "http://test.com/test/test_app",
94+
},
95+
}
96+
})
97+
It("should return empty tools", func() {
98+
_, err := a.getTools(vars, templateMap)
99+
Expect(err).Should(HaveOccurred())
100+
Expect(err.Error()).Should(ContainSubstring("configmanager[app] parse repoTemplate failed"))
101+
})
102+
})
103+
55104
})
56105

57106
Context("generateCICDTools method", func() {

internal/pkg/configmanager/appspec.go

+1-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ func (s *appSpec) merge(vars map[string]any) map[string]any {
2121
log.Warnf("appspec %+v decode failed: %+v", s, err)
2222
return map[string]any{}
2323
}
24-
if err := mergo.Merge(&specMap, vars); err != nil {
25-
log.Warnf("appSpec %+v merge map failed: %+v", s, err)
26-
return vars
27-
}
24+
_ = mergo.Merge(&specMap, vars)
2825
return specMap
2926
}
3027

internal/pkg/configmanager/config.go

+1-11
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package configmanager
22

33
import (
44
"fmt"
5-
6-
"gopkg.in/yaml.v3"
75
)
86

97
// Config is a general config in DevStream.
@@ -24,19 +22,11 @@ func (c *Config) renderInstanceIDtoOptions() {
2422

2523
func (c *Config) validate() error {
2624
if c.Config.State == nil {
27-
return fmt.Errorf("state is not defined")
25+
return fmt.Errorf("config.state is not defined")
2826
}
2927

3028
if err := c.Tools.validateAll(); err != nil {
3129
return err
3230
}
3331
return nil
3432
}
35-
36-
func (c *Config) String() string {
37-
bs, err := yaml.Marshal(c)
38-
if err != nil {
39-
return err.Error()
40-
}
41-
return string(bs)
42-
}
+46
Original file line numberDiff line numberDiff line change
@@ -1 +1,47 @@
11
package configmanager
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
. "github.com/onsi/gomega"
6+
)
7+
8+
var _ = Describe("Config struct", func() {
9+
var (
10+
c *Config
11+
toolName, instanceID string
12+
)
13+
BeforeEach(func() {
14+
c = &Config{}
15+
toolName = "test_tool"
16+
instanceID = "test_instance"
17+
})
18+
Context("renderInstanceIDtoOptions method", func() {
19+
When("tool option is null", func() {
20+
BeforeEach(func() {
21+
c.Tools = Tools{
22+
{Name: toolName, InstanceID: instanceID},
23+
}
24+
})
25+
It("should set nil to RawOptions", func() {
26+
c.renderInstanceIDtoOptions()
27+
Expect(len(c.Tools)).Should(Equal(1))
28+
tool := c.Tools[0]
29+
Expect(tool.Options).Should(Equal(RawOptions{
30+
"instanceID": instanceID,
31+
}))
32+
})
33+
})
34+
})
35+
Context("validate method", func() {
36+
When("config state is null", func() {
37+
BeforeEach(func() {
38+
c.Config.State = nil
39+
})
40+
It("should return err", func() {
41+
err := c.validate()
42+
Expect(err).Should(HaveOccurred())
43+
Expect(err.Error()).Should(Equal("config.state is not defined"))
44+
})
45+
})
46+
})
47+
})

0 commit comments

Comments
 (0)