diff --git a/yaml/build_test.go b/yaml/build_test.go index 8875d755..1f143c60 100644 --- a/yaml/build_test.go +++ b/yaml/build_test.go @@ -7,6 +7,7 @@ import ( "reflect" "testing" + "github.com/google/go-cmp/cmp" "gopkg.in/yaml.v3" "github.com/go-vela/types/library" @@ -327,6 +328,72 @@ func TestYaml_Build_UnmarshalYAML(t *testing.T) { }, }, }, + { + file: "testdata/merge_anchor_step.yml", + want: &Build{ + Version: "1", + Metadata: Metadata{ + Template: false, + Clone: nil, + Environment: []string{"steps", "services", "secrets"}, + }, + Services: ServiceSlice{ + { + Name: "service-a", + Ports: []string{"5432:5432"}, + Environment: raw.StringSliceMap{ + "REGION": "dev", + }, + Image: "postgres", + Pull: "not_present", + }, + }, + Steps: StepSlice{ + { + Commands: raw.StringSlice{"echo alpha"}, + Name: "alpha", + Image: "alpine:latest", + Pull: "not_present", + Ruleset: Ruleset{ + If: Rules{ + Event: []string{"push"}, + }, + Matcher: "filepath", + Operator: "and", + }, + }, + { + Commands: raw.StringSlice{"echo beta"}, + Name: "beta", + Image: "alpine:latest", + Pull: "not_present", + Ruleset: Ruleset{ + If: Rules{ + Event: []string{"push"}, + }, + Matcher: "filepath", + Operator: "and", + }, + }, + { + Commands: raw.StringSlice{"echo gamma"}, + Name: "gamma", + Image: "alpine:latest", + Pull: "not_present", + Environment: raw.StringSliceMap{ + "REGION": "dev", + }, + Ruleset: Ruleset{ + If: Rules{ + Event: []string{"push"}, + }, + Matcher: "filepath", + Operator: "and", + }, + }, + }, + }, + }, { file: "testdata/build_anchor_stage.yml", want: &Build{ @@ -613,8 +680,8 @@ func TestYaml_Build_UnmarshalYAML(t *testing.T) { t.Errorf("UnmarshalYAML returned err: %v", err) } - if !reflect.DeepEqual(got, test.want) { - t.Errorf("UnmarshalYAML is %v, want %v", got, test.want) + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("UnmarshalYAML mismatch (-want +got):\n%s", diff) } } } diff --git a/yaml/service.go b/yaml/service.go index b6807a21..385f86d2 100644 --- a/yaml/service.go +++ b/yaml/service.go @@ -6,6 +6,8 @@ import ( "fmt" "strings" + "gopkg.in/yaml.v3" + "github.com/go-vela/types/constants" "github.com/go-vela/types/pipeline" "github.com/go-vela/types/raw" @@ -58,21 +60,78 @@ func (s *ServiceSlice) ToPipeline() *pipeline.ContainerSlice { // UnmarshalYAML implements the Unmarshaler interface for the ServiceSlice type. // //nolint:dupl // accepting duplicative code that exists in step.go as well -func (s *ServiceSlice) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (s *ServiceSlice) UnmarshalYAML(v *yaml.Node) error { + // service slice should be sequence + if v.Kind != yaml.SequenceNode { + return fmt.Errorf("invalid yaml: expected sequence node for service slice") + } + // service slice we try unmarshalling to serviceSlice := new([]*Service) - // attempt to unmarshal as a service slice type - err := unmarshal(serviceSlice) - if err != nil { - return err - } - // iterate through each element in the service slice - for _, service := range *serviceSlice { - // handle nil service to avoid panic - if service == nil { - return fmt.Errorf("invalid service with nil content found") + for _, st := range v.Content { + // make local var + tmpService := *st + + // services are mapping nodes + if tmpService.Kind != yaml.MappingNode { + return fmt.Errorf("invalid yaml: expected map node for service") + } + + // initialize anchor node -- will be nil if `<<` never used + var anchorKey *yaml.Node + + // initialize anchor sets + anchorList := new([]*yaml.Node) // collect multiple anchor references + newContent := new([]*yaml.Node) // new service content + anchorSequence := new(yaml.Node) // final type that is appended to service contents + + // iterate through map contents (key, value) + for i := 0; i < len(tmpService.Content); i += 2 { + key := tmpService.Content[i] + value := tmpService.Content[i+1] + + // check if key is an anchor reference + if strings.EqualFold(key.Value, "<<") { + // if this is first anchor, initialize key and value + if anchorKey == nil { + anchorKey = key + anchorSequence = value + } + + // append value to anchor list + *anchorList = append(*anchorList, value) + } else { + *newContent = append(*newContent, key, value) + } + } + + // overwrite content + tmpService.Content = *newContent + + // if there is only one anchor key, use existing sequence + if len(*anchorList) == 1 { + tmpService.Content = append(tmpService.Content, anchorKey, anchorSequence) + } + + // if there are multiple anchor keys, create a sequence using anchorList as the content + if len(*anchorList) > 1 { + anchorSequence = new(yaml.Node) + anchorSequence.Kind = yaml.SequenceNode + anchorSequence.Style = yaml.FlowStyle + anchorSequence.Tag = "!!seq" + anchorSequence.Content = *anchorList + + tmpService.Content = append(tmpService.Content, anchorKey, anchorSequence) + } + + // convert processed node to service type + service := new(Service) + + err := tmpService.Decode(service) + if err != nil { + return err } // implicitly set `pull` field if empty @@ -97,6 +156,8 @@ func (s *ServiceSlice) UnmarshalYAML(unmarshal func(interface{}) error) error { if strings.EqualFold(service.Pull, "false") { service.Pull = constants.PullNotPresent } + + *serviceSlice = append(*serviceSlice, service) } // overwrite existing ServiceSlice diff --git a/yaml/step.go b/yaml/step.go index e9da9f4f..012f2161 100644 --- a/yaml/step.go +++ b/yaml/step.go @@ -6,6 +6,8 @@ import ( "fmt" "strings" + "gopkg.in/yaml.v3" + "github.com/go-vela/types/constants" "github.com/go-vela/types/pipeline" "github.com/go-vela/types/raw" @@ -72,22 +74,79 @@ func (s *StepSlice) ToPipeline() *pipeline.ContainerSlice { // UnmarshalYAML implements the Unmarshaler interface for the StepSlice type. // -//nolint:dupl // accepting duplicative code that exits in service.go as well -func (s *StepSlice) UnmarshalYAML(unmarshal func(interface{}) error) error { +//nolint:dupl // ignore similarities with service unmarshal +func (s *StepSlice) UnmarshalYAML(v *yaml.Node) error { + // step slice should be sequence + if v.Kind != yaml.SequenceNode { + return fmt.Errorf("invalid yaml: expected sequence node for step slice") + } + // step slice we try unmarshalling to stepSlice := new([]*Step) - // attempt to unmarshal as a step slice type - err := unmarshal(stepSlice) - if err != nil { - return err - } - // iterate through each element in the step slice - for _, step := range *stepSlice { - // handle nil step to avoid panic - if step == nil { - return fmt.Errorf("invalid step with nil content found") + for _, st := range v.Content { + // make local var + tmpStep := *st + + // steps are mapping nodes + if tmpStep.Kind != yaml.MappingNode { + return fmt.Errorf("invalid yaml: expected map node for step") + } + + // initialize anchor node -- will be nil if `<<` never used + var anchorKey *yaml.Node + + // initialize anchor sets + anchorList := new([]*yaml.Node) // collect multiple anchor references + newContent := new([]*yaml.Node) // new step content + anchorSequence := new(yaml.Node) // final type that is appended to step contents + + // iterate through map contents (key, value) + for i := 0; i < len(tmpStep.Content); i += 2 { + key := tmpStep.Content[i] + value := tmpStep.Content[i+1] + + // check if key is an anchor reference + if strings.EqualFold(key.Value, "<<") { + // if this is first anchor, initialize key and value + if anchorKey == nil { + anchorKey = key + anchorSequence = value + } + + // append value to anchor list + *anchorList = append(*anchorList, value) + } else { + *newContent = append(*newContent, key, value) + } + } + + // overwrite content + tmpStep.Content = *newContent + + // if there is only one anchor key, use existing sequence + if len(*anchorList) == 1 { + tmpStep.Content = append(tmpStep.Content, anchorKey, anchorSequence) + } + + // if there are multiple anchor keys, create a sequence using anchorList as the content + if len(*anchorList) > 1 { + anchorSequence = new(yaml.Node) + anchorSequence.Kind = yaml.SequenceNode + anchorSequence.Style = yaml.FlowStyle + anchorSequence.Tag = "!!seq" + anchorSequence.Content = *anchorList + + tmpStep.Content = append(tmpStep.Content, anchorKey, anchorSequence) + } + + // convert processed node to step type + step := new(Step) + + err := tmpStep.Decode(step) + if err != nil { + return err } // implicitly set `pull` field if empty @@ -112,6 +171,8 @@ func (s *StepSlice) UnmarshalYAML(unmarshal func(interface{}) error) error { if strings.EqualFold(step.Pull, "false") { step.Pull = constants.PullNotPresent } + + *stepSlice = append(*stepSlice, step) } // overwrite existing StepSlice diff --git a/yaml/testdata/merge_anchor_step.yml b/yaml/testdata/merge_anchor_step.yml new file mode 100644 index 00000000..1f09d562 --- /dev/null +++ b/yaml/testdata/merge_anchor_step.yml @@ -0,0 +1,46 @@ +# test file that uses the non-standard multiple anchor keys in one step to test custom step unmarshaler + +version: "1" + +aliases: + images: + alpine: &alpine-image + image: alpine:latest + postgres: &pg-image + image: postgres + + events: + push: &event-push + ruleset: + event: + - push + env: + dev-env: &dev-environment + environment: + REGION: dev + +services: + - name: service-a + <<: *pg-image + <<: *dev-environment + ports: + - "5432:5432" + +steps: + - name: alpha + <<: *alpine-image + <<: *event-push + commands: + - echo alpha + + - name: beta + <<: [ *alpine-image, *event-push ] + commands: + - echo beta + + - name: gamma + <<: *alpine-image + <<: *event-push + <<: *dev-environment + commands: + - echo gamma