config: refactor config unmarshal

* Don't use a complex UnmarshalJSON for Task but use specific UnmarshalJSON for
every type that requires custom unmarshaling
This commit is contained in:
Simone Gotti 2019-06-08 16:07:15 +02:00
parent aad661adc8
commit 96c0fa9aea
4 changed files with 199 additions and 179 deletions

View File

@ -108,11 +108,11 @@ type Task struct {
WorkingDir string `json:"working_dir"` WorkingDir string `json:"working_dir"`
Shell string `json:"shell"` Shell string `json:"shell"`
User string `json:"user"` User string `json:"user"`
Steps []interface{} `json:"steps"` Steps Steps `json:"steps"`
Depends []*Depend `json:"depends"` Depends Depends `json:"depends"`
IgnoreFailure bool `json:"ignore_failure"` IgnoreFailure bool `json:"ignore_failure"`
Approval bool `json:"approval"` Approval bool `json:"approval"`
When *types.When `json:"when"` When *When `json:"when"`
DockerRegistriesAuth map[string]*DockerRegistryAuth `json:"docker_registries_auth"` DockerRegistriesAuth map[string]*DockerRegistryAuth `json:"docker_registries_auth"`
} }
@ -124,22 +124,29 @@ const (
DependConditionOnSkipped DependCondition = "on_skipped" DependConditionOnSkipped DependCondition = "on_skipped"
) )
type Depends []*Depend
type Depend struct { type Depend struct {
TaskName string `json:"task"` TaskName string `json:"task"`
Conditions []DependCondition `json:"conditions"` Conditions []DependCondition `json:"conditions"`
} }
type Step struct { type Step interface{}
type Steps []Step
type BaseStep struct {
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
When *When `json:"when"`
} }
type CloneStep struct { type CloneStep struct {
Step `json:",inline"` BaseStep `json:",inline"`
} }
type RunStep struct { type RunStep struct {
Step `json:",inline"` BaseStep `json:",inline"`
Command string `json:"command"` Command string `json:"command"`
Environment map[string]Value `json:"environment,omitempty"` Environment map[string]Value `json:"environment,omitempty"`
WorkingDir string `json:"working_dir"` WorkingDir string `json:"working_dir"`
@ -147,16 +154,26 @@ type RunStep struct {
User string `json:"user"` User string `json:"user"`
} }
type ValueType int type SaveToWorkspaceStep struct {
BaseStep `json:",inline"`
Contents []*SaveContent `json:"contents"`
}
const ( type RestoreWorkspaceStep struct {
ValueTypeString ValueType = iota BaseStep `json:",inline"`
ValueTypeFromVariable DestDir string `json:"dest_dir"`
) }
type Value struct { type SaveCacheStep struct {
Type ValueType BaseStep `json:",inline"`
Value string Key string `json:"key"`
Contents []*SaveContent `json:"contents"`
}
type RestoreCacheStep struct {
BaseStep `json:",inline"`
Keys []string `json:"keys"`
DestDir string `json:"dest_dir"`
} }
type SaveContent struct { type SaveContent struct {
@ -165,141 +182,101 @@ type SaveContent struct {
Paths []string `json:"paths"` Paths []string `json:"paths"`
} }
type SaveToWorkspaceStep struct { func (s *Steps) UnmarshalJSON(b []byte) error {
Step `json:",inline"` var stepsRaw []json.RawMessage
Contents []*SaveContent `json:"contents"` if err := json.Unmarshal(b, &stepsRaw); err != nil {
}
type RestoreWorkspaceStep struct {
Step `json:",inline"`
DestDir string `json:"dest_dir"`
}
type SaveCacheStep struct {
Step `json:",inline"`
Key string `json:"key"`
Contents []*SaveContent `json:"contents"`
}
type RestoreCacheStep struct {
Step `json:",inline"`
Keys []string `json:"keys"`
DestDir string `json:"dest_dir"`
}
func (t *Task) UnmarshalJSON(b []byte) error {
type when struct {
Branch interface{} `json:"branch"`
Tag interface{} `json:"tag"`
Ref interface{} `json:"ref"`
}
type runtask struct {
Name string `json:"name"`
Runtime *Runtime `json:"runtime"`
Environment map[string]Value `json:"environment,omitempty"`
WorkingDir string `json:"working_dir"`
Shell string `json:"shell"`
User string `json:"user"`
Steps []map[string]interface{} `json:"steps"`
Depends []interface{} `json:"depends"`
IgnoreFailure bool `json:"ignore_failure"`
Approval bool `json:"approval"`
When *when `json:"when"`
DockerRegistriesAuth map[string]*DockerRegistryAuth `json:"docker_registries_auth"`
}
var tr *runtask
if err := json.Unmarshal(b, &tr); err != nil {
return err return err
} }
t.Name = tr.Name steps := make(Steps, len(stepsRaw))
t.Runtime = tr.Runtime for i, stepRaw := range stepsRaw {
t.Environment = tr.Environment var step interface{}
t.WorkingDir = tr.WorkingDir
t.Shell = tr.Shell
t.User = tr.User
t.IgnoreFailure = tr.IgnoreFailure
t.Approval = tr.Approval
t.DockerRegistriesAuth = tr.DockerRegistriesAuth
steps := make([]interface{}, len(tr.Steps)) var stepMap map[string]json.RawMessage
for i, stepEntry := range tr.Steps { if err := json.Unmarshal(stepRaw, &stepMap); err != nil {
if _, ok := stepEntry["type"]; ok { return err
// handle default step definition using format { type: "steptype", other steps fields } }
stepType, ok := stepEntry["type"].(string) // handle default step definition using format { type: "steptype", other steps fields }
if _, ok := stepMap["type"]; ok {
var stepTypeI interface{}
if err := json.Unmarshal(stepMap["type"], &stepTypeI); err != nil {
return err
}
stepType, ok := stepTypeI.(string)
if !ok { if !ok {
return errors.Errorf("step type at index %d must be a string", i) return errors.Errorf("step type at index %d must be a string", i)
} }
o, err := json.Marshal(stepEntry)
if err != nil {
return err
}
switch stepType { switch stepType {
case "clone": case "clone":
var s CloneStep var s CloneStep
if err := json.Unmarshal(stepRaw, &s); err != nil {
return err
}
s.Type = stepType s.Type = stepType
steps[i] = &s step = &s
case "run": case "run":
var s RunStep var s RunStep
if err := json.Unmarshal(o, &s); err != nil { if err := json.Unmarshal(stepRaw, &s); err != nil {
return err return err
} }
s.Type = stepType s.Type = stepType
steps[i] = &s step = &s
case "save_to_workspace": case "save_to_workspace":
var s SaveToWorkspaceStep var s SaveToWorkspaceStep
if err := json.Unmarshal(o, &s); err != nil { if err := json.Unmarshal(stepRaw, &s); err != nil {
return err return err
} }
s.Type = stepType s.Type = stepType
steps[i] = &s step = &s
case "restore_workspace": case "restore_workspace":
var s RestoreWorkspaceStep var s RestoreWorkspaceStep
if err := json.Unmarshal(o, &s); err != nil { if err := json.Unmarshal(stepRaw, &s); err != nil {
return err return err
} }
s.Type = stepType s.Type = stepType
steps[i] = &s step = &s
case "save_cache": case "save_cache":
var s SaveCacheStep var s SaveCacheStep
if err := json.Unmarshal(o, &s); err != nil { if err := json.Unmarshal(stepRaw, &s); err != nil {
return err return err
} }
s.Type = stepType s.Type = stepType
steps[i] = &s step = &s
case "restore_cache": case "restore_cache":
var s RestoreCacheStep var s RestoreCacheStep
if err := json.Unmarshal(o, &s); err != nil { if err := json.Unmarshal(stepRaw, &s); err != nil {
return err return err
} }
s.Type = stepType s.Type = stepType
steps[i] = &s step = &s
default: default:
return errors.Errorf("unknown step type: %s", stepType) return errors.Errorf("unknown step type: %s", stepType)
} }
} else { } else {
// handle simpler (for yaml not for json) steps definition using format "steptype": { stepSpecification } // handle simpler (for yaml not for json) steps definition using format "steptype": { stepSpecification }
if len(stepEntry) > 1 { if len(stepMap) > 1 {
return errors.Errorf("wrong steps description at index %d: more than one step name per list entry", i) return errors.Errorf("wrong steps description at index %d: more than one step name per list entry", i)
} }
for stepType, stepSpec := range stepEntry { for stepType, stepSpecRaw := range stepMap {
o, err := json.Marshal(stepSpec) var stepSpec interface{}
if err != nil { if err := json.Unmarshal(stepSpecRaw, &stepSpec); err != nil {
return err return err
} }
switch stepType { switch stepType {
case "clone": case "clone":
var s CloneStep var s CloneStep
if err := json.Unmarshal(stepSpecRaw, &s); err != nil {
return err
}
s.Type = stepType s.Type = stepType
steps[i] = &s step = &s
case "run": case "run":
var s RunStep var s RunStep
@ -307,62 +284,78 @@ func (t *Task) UnmarshalJSON(b []byte) error {
case string: case string:
s.Command = stepSpec.(string) s.Command = stepSpec.(string)
default: default:
if err := json.Unmarshal(o, &s); err != nil { if err := json.Unmarshal(stepSpecRaw, &s); err != nil {
return err return err
} }
} }
s.Type = stepType s.Type = stepType
steps[i] = &s step = &s
case "save_to_workspace": case "save_to_workspace":
var s SaveToWorkspaceStep var s SaveToWorkspaceStep
if err := json.Unmarshal(o, &s); err != nil { if err := json.Unmarshal(stepSpecRaw, &s); err != nil {
return err return err
} }
s.Type = stepType s.Type = stepType
steps[i] = &s step = &s
case "restore_workspace": case "restore_workspace":
var s RestoreWorkspaceStep var s RestoreWorkspaceStep
if err := json.Unmarshal(o, &s); err != nil { if err := json.Unmarshal(stepSpecRaw, &s); err != nil {
return err return err
} }
s.Type = stepType s.Type = stepType
steps[i] = &s step = &s
case "save_cache": case "save_cache":
var s SaveCacheStep var s SaveCacheStep
if err := json.Unmarshal(o, &s); err != nil { if err := json.Unmarshal(stepSpecRaw, &s); err != nil {
return err return err
} }
s.Type = stepType s.Type = stepType
steps[i] = &s step = &s
case "restore_cache": case "restore_cache":
var s RestoreCacheStep var s RestoreCacheStep
if err := json.Unmarshal(o, &s); err != nil { if err := json.Unmarshal(stepSpecRaw, &s); err != nil {
return err return err
} }
s.Type = stepType s.Type = stepType
steps[i] = &s step = &s
default: default:
return errors.Errorf("unknown step type: %s", stepType) return errors.Errorf("unknown step type: %s", stepType)
} }
} }
} }
steps[i] = step
} }
t.Steps = steps *s = steps
depends := make([]*Depend, len(tr.Depends)) return nil
for i, dependEntry := range tr.Depends { }
func (d *Depends) UnmarshalJSON(b []byte) error {
var dependsRaw []json.RawMessage
if err := json.Unmarshal(b, &dependsRaw); err != nil {
return err
}
depends := make([]*Depend, len(dependsRaw))
for i, dependRaw := range dependsRaw {
var dependi interface{}
if err := json.Unmarshal(dependRaw, &dependi); err != nil {
return err
}
var depend *Depend var depend *Depend
isSimpler := false isSimpler := false
switch de := dependEntry.(type) { switch de := dependi.(type) {
// handle simpler (for yaml) depends definition using format "taskname": // handle simpler (for yaml) depends definition using format "taskname":
case string: case string:
depend = &Depend{ depend = &Depend{
TaskName: dependEntry.(string), TaskName: dependi.(string),
} }
case map[string]interface{}: case map[string]interface{}:
if len(de) == 1 { if len(de) == 1 {
@ -378,11 +371,7 @@ func (t *Task) UnmarshalJSON(b []byte) error {
} }
if !isSimpler { if !isSimpler {
// handle default depends definition using format "task": "taskname", conditions: [ list of conditions ] // handle default depends definition using format "task": "taskname", conditions: [ list of conditions ]
o, err := json.Marshal(dependEntry) if err := json.Unmarshal(dependRaw, &depend); err != nil {
if err != nil {
return err
}
if err := json.Unmarshal(o, &depend); err != nil {
return err return err
} }
} else { } else {
@ -392,11 +381,7 @@ func (t *Task) UnmarshalJSON(b []byte) error {
} }
type deplist map[string][]DependCondition type deplist map[string][]DependCondition
var dl deplist var dl deplist
o, err := json.Marshal(dependEntry) if err := json.Unmarshal(dependRaw, &dl); err != nil {
if err != nil {
return err
}
if err := json.Unmarshal(o, &dl); err != nil {
return err return err
} }
if len(dl) != 1 { if len(dl) != 1 {
@ -416,40 +401,23 @@ func (t *Task) UnmarshalJSON(b []byte) error {
depends[i] = depend depends[i] = depend
} }
t.Depends = depends *d = depends
if tr.When != nil {
w := &types.When{}
var err error
if tr.When.Branch != nil {
w.Branch, err = parseWhenConditions(tr.When.Branch)
if err != nil {
return err
}
}
if tr.When.Tag != nil {
w.Tag, err = parseWhenConditions(tr.When.Tag)
if err != nil {
return err
}
}
if tr.When.Ref != nil {
w.Ref, err = parseWhenConditions(tr.When.Ref)
if err != nil {
return err
}
}
t.When = w
}
return nil return nil
} }
type ValueType int
const (
ValueTypeString ValueType = iota
ValueTypeFromVariable
)
type Value struct {
Type ValueType
Value string
}
func (val *Value) UnmarshalJSON(b []byte) error { func (val *Value) UnmarshalJSON(b []byte) error {
var ival interface{} var ival interface{}
if err := json.Unmarshal(b, &ival); err != nil { if err := json.Unmarshal(b, &ival); err != nil {
@ -477,6 +445,46 @@ func (val *Value) UnmarshalJSON(b []byte) error {
return nil return nil
} }
type When types.When
type when struct {
Branch interface{} `json:"branch"`
Tag interface{} `json:"tag"`
Ref interface{} `json:"ref"`
}
func (w *When) UnmarshalJSON(b []byte) error {
var wi *when
if err := json.Unmarshal(b, &wi); err != nil {
return err
}
var err error
if wi.Branch != nil {
w.Branch, err = parseWhenConditions(wi.Branch)
if err != nil {
return err
}
}
if wi.Tag != nil {
w.Tag, err = parseWhenConditions(wi.Tag)
if err != nil {
return err
}
}
if wi.Ref != nil {
w.Ref, err = parseWhenConditions(wi.Ref)
if err != nil {
return err
}
}
return nil
}
func parseWhenConditions(wi interface{}) (*types.WhenConditions, error) { func parseWhenConditions(wi interface{}) (*types.WhenConditions, error) {
w := &types.WhenConditions{} w := &types.WhenConditions{}

View File

@ -307,24 +307,24 @@ func TestParseOutput(t *testing.T) {
WorkingDir: defaultWorkingDir, WorkingDir: defaultWorkingDir,
Shell: "", Shell: "",
User: "", User: "",
Steps: []interface{}{ Steps: Steps{
&CloneStep{Step: Step{Type: "clone"}}, &CloneStep{BaseStep: BaseStep{Type: "clone"}},
&RunStep{ &RunStep{
Step: Step{ BaseStep: BaseStep{
Type: "run", Type: "run",
Name: "command01", Name: "command01",
}, },
Command: "command01", Command: "command01",
}, },
&RunStep{ &RunStep{
Step: Step{ BaseStep: BaseStep{
Type: "run", Type: "run",
Name: "name different than command", Name: "name different than command",
}, },
Command: "command02", Command: "command02",
}, },
&RunStep{ &RunStep{
Step: Step{ BaseStep: BaseStep{
Type: "run", Type: "run",
Name: "command03", Name: "command03",
}, },
@ -335,27 +335,27 @@ func TestParseOutput(t *testing.T) {
}, },
}, },
&SaveCacheStep{ &SaveCacheStep{
Step: Step{Type: "save_cache"}, BaseStep: BaseStep{Type: "save_cache"},
Key: "cache-{{ arch }}", Key: "cache-{{ arch }}",
Contents: []*SaveContent{&SaveContent{SourceDir: "/go/pkg/mod/cache", Paths: []string{"**"}}}, Contents: []*SaveContent{&SaveContent{SourceDir: "/go/pkg/mod/cache", Paths: []string{"**"}}},
}, },
&CloneStep{Step: Step{Type: "clone"}}, &CloneStep{BaseStep: BaseStep{Type: "clone"}},
&RunStep{ &RunStep{
Step: Step{ BaseStep: BaseStep{
Type: "run", Type: "run",
Name: "command01", Name: "command01",
}, },
Command: "command01", Command: "command01",
}, },
&RunStep{ &RunStep{
Step: Step{ BaseStep: BaseStep{
Type: "run", Type: "run",
Name: "name different than command", Name: "name different than command",
}, },
Command: "command02", Command: "command02",
}, },
&RunStep{ &RunStep{
Step: Step{ BaseStep: BaseStep{
Type: "run", Type: "run",
Name: "command03", Name: "command03",
}, },
@ -366,14 +366,14 @@ func TestParseOutput(t *testing.T) {
}, },
}, },
&SaveCacheStep{ &SaveCacheStep{
Step: Step{Type: "save_cache"}, BaseStep: BaseStep{Type: "save_cache"},
Key: "cache-{{ arch }}", Key: "cache-{{ arch }}",
Contents: []*SaveContent{&SaveContent{SourceDir: "/go/pkg/mod/cache", Paths: []string{"**"}}}, Contents: []*SaveContent{&SaveContent{SourceDir: "/go/pkg/mod/cache", Paths: []string{"**"}}},
}, },
}, },
IgnoreFailure: false, IgnoreFailure: false,
Approval: false, Approval: false,
When: &types.When{ When: &When{
Branch: &types.WhenConditions{ Branch: &types.WhenConditions{
Include: []types.WhenCondition{ Include: []types.WhenCondition{
{Type: types.WhenConditionTypeSimple, Match: "master"}, {Type: types.WhenConditionTypeSimple, Match: "master"},
@ -413,8 +413,8 @@ func TestParseOutput(t *testing.T) {
}, },
}, },
WorkingDir: defaultWorkingDir, WorkingDir: defaultWorkingDir,
Steps: []interface{}{}, Steps: nil,
Depends: []*Depend{}, Depends: nil,
}, },
&Task{ &Task{
Name: "task03", Name: "task03",
@ -428,8 +428,8 @@ func TestParseOutput(t *testing.T) {
}, },
}, },
WorkingDir: defaultWorkingDir, WorkingDir: defaultWorkingDir,
Steps: []interface{}{}, Steps: nil,
Depends: []*Depend{}, Depends: nil,
}, },
&Task{ &Task{
Name: "task04", Name: "task04",
@ -443,8 +443,8 @@ func TestParseOutput(t *testing.T) {
}, },
}, },
WorkingDir: defaultWorkingDir, WorkingDir: defaultWorkingDir,
Steps: []interface{}{}, Steps: nil,
Depends: []*Depend{}, Depends: nil,
}, },
}, },
}, },

View File

@ -48,6 +48,17 @@ func genRuntime(c *config.Config, ce *config.Runtime, variables map[string]strin
} }
} }
func whenFromConfigWhen(cw *config.When) *types.When {
if cw == nil {
return nil
}
return &types.When{
Branch: cw.Branch,
Tag: cw.Tag,
Ref: cw.Ref,
}
}
func stepFromConfigStep(csi interface{}, variables map[string]string) interface{} { func stepFromConfigStep(csi interface{}, variables map[string]string) interface{} {
switch cs := csi.(type) { switch cs := csi.(type) {
case *config.CloneStep: case *config.CloneStep:
@ -183,7 +194,7 @@ func GenRunConfigTasks(uuid util.UUIDGenerator, c *config.Config, runName string
rcts := map[string]*rstypes.RunConfigTask{} rcts := map[string]*rstypes.RunConfigTask{}
for _, ct := range cr.Tasks { for _, ct := range cr.Tasks {
include := types.MatchWhen(ct.When, branch, tag, ref) include := types.MatchWhen(whenFromConfigWhen(ct.When), branch, tag, ref)
steps := make([]interface{}, len(ct.Steps)) steps := make([]interface{}, len(ct.Steps))
for i, cpts := range ct.Steps { for i, cpts := range ct.Steps {

View File

@ -19,11 +19,12 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/google/go-cmp/cmp"
"github.com/sorintlab/agola/internal/config" "github.com/sorintlab/agola/internal/config"
rstypes "github.com/sorintlab/agola/internal/services/runservice/types" rstypes "github.com/sorintlab/agola/internal/services/runservice/types"
"github.com/sorintlab/agola/internal/services/types" "github.com/sorintlab/agola/internal/services/types"
"github.com/sorintlab/agola/internal/util" "github.com/sorintlab/agola/internal/util"
"github.com/google/go-cmp/cmp"
errors "golang.org/x/xerrors" errors "golang.org/x/xerrors"
) )
@ -673,23 +674,23 @@ func TestGenRunConfig(t *testing.T) {
WorkingDir: "", WorkingDir: "",
Shell: "", Shell: "",
User: "", User: "",
Steps: []interface{}{ Steps: config.Steps{
&config.RunStep{ &config.RunStep{
Step: config.Step{ BaseStep: config.BaseStep{
Type: "run", Type: "run",
Name: "command01", Name: "command01",
}, },
Command: "command01", Command: "command01",
}, },
&config.RunStep{ &config.RunStep{
Step: config.Step{ BaseStep: config.BaseStep{
Type: "run", Type: "run",
Name: "name different than command", Name: "name different than command",
}, },
Command: "command02", Command: "command02",
}, },
&config.RunStep{ &config.RunStep{
Step: config.Step{ BaseStep: config.BaseStep{
Type: "run", Type: "run",
Name: "command03", Name: "command03",
}, },
@ -704,7 +705,7 @@ func TestGenRunConfig(t *testing.T) {
Depends: []*config.Depend{}, Depends: []*config.Depend{},
IgnoreFailure: false, IgnoreFailure: false,
Approval: false, Approval: false,
When: &types.When{ When: &config.When{
Branch: &types.WhenConditions{Include: []types.WhenCondition{{Match: "master"}}}, Branch: &types.WhenConditions{Include: []types.WhenCondition{{Match: "master"}}},
Tag: &types.WhenConditions{Include: []types.WhenCondition{{Match: "v1.x"}, {Match: "v2.x"}}}, Tag: &types.WhenConditions{Include: []types.WhenCondition{{Match: "v1.x"}, {Match: "v2.x"}}},
Ref: &types.WhenConditions{ Ref: &types.WhenConditions{
@ -781,9 +782,9 @@ func TestGenRunConfig(t *testing.T) {
}, },
}, },
}, },
Steps: []interface{}{ Steps: config.Steps{
&config.RunStep{ &config.RunStep{
Step: config.Step{ BaseStep: config.BaseStep{
Type: "run", Type: "run",
Name: "command01", Name: "command01",
}, },
@ -867,9 +868,9 @@ func TestGenRunConfig(t *testing.T) {
}, },
}, },
}, },
Steps: []interface{}{ Steps: config.Steps{
&config.RunStep{ &config.RunStep{
Step: config.Step{ BaseStep: config.BaseStep{
Type: "run", Type: "run",
Name: "command01", Name: "command01",
}, },