Skip to content

Commit 15916a6

Browse files
fix: propagation of attributes mode, fail_fast, and hooks from originalYamlString (#41)
* fix: propagation of attributes mode and fail_fast from originalYamlString * Add support for Hooks in original Yaml String * Introduce retry logic for project delete to address eventual consistency
1 parent 6eeaf1c commit 15916a6

8 files changed

+202
-23
lines changed

client/context.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func (client *Client) CreateContext(context *Context) (*Context, error) {
6767
}
6868

6969
resp, err := client.RequestAPI(&opts)
70-
log.Printf("[DEBUG] Called API for context with Body %v", body)
70+
7171
if err != nil {
7272
log.Printf("[DEBUG] Call to API for context creation failed with Error = %v for Body %v", err, body)
7373
return nil, err

client/pipeline.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,10 @@ type Spec struct {
8181
Steps *Steps `json:"steps,omitempty"`
8282
Stages *Stages `json:"stages,omitempty"`
8383
Mode string `json:"mode,omitempty"`
84+
FailFast bool `json:"fail_fast,omitempty"`
8485
RuntimeEnvironment RuntimeEnvironment `json:"runtimeEnvironment,omitempty"`
8586
TerminationPolicy []map[string]interface{} `json:"terminationPolicy,omitempty"`
87+
Hooks *Hooks `json:"hooks,omitempty"`
8688
}
8789

8890
type Steps struct {
@@ -92,6 +94,10 @@ type Stages struct {
9294
Stages string
9395
}
9496

97+
type Hooks struct {
98+
Hooks string
99+
}
100+
95101
func (d Steps) MarshalJSON() ([]byte, error) {
96102
bytes := []byte(d.Steps)
97103
return bytes, nil
@@ -101,14 +107,23 @@ func (d Stages) MarshalJSON() ([]byte, error) {
101107
return bytes, nil
102108
}
103109

104-
func (d Steps) UnmarshalJSON(data []byte) error {
110+
func (d Hooks) MarshalJSON() ([]byte, error) {
111+
bytes := []byte(d.Hooks)
112+
return bytes, nil
113+
}
114+
115+
func (d *Steps) UnmarshalJSON(data []byte) error {
105116
d.Steps = string(data)
106117
return nil
107118
}
108-
func (d Stages) UnmarshalJSON(data []byte) error {
119+
func (d *Stages) UnmarshalJSON(data []byte) error {
109120
d.Stages = string(data)
110121
return nil
111122
}
123+
func (d *Hooks) UnmarshalJSON(data []byte) error {
124+
d.Hooks = string(data)
125+
return nil
126+
}
112127

113128
type Pipeline struct {
114129
Metadata Metadata `json:"metadata,omitempty"`

codefresh/resource_pipeline.go

+77-16
Original file line numberDiff line numberDiff line change
@@ -575,13 +575,7 @@ func mapResourceToPipeline(d *schema.ResourceData) *cfClient.Pipeline {
575575
Context: d.Get("spec.0.spec_template.0.context").(string),
576576
}
577577
} else {
578-
stages, steps := extractStagesAndSteps(originalYamlString)
579-
pipeline.Spec.Steps = &cfClient.Steps{
580-
Steps: steps,
581-
}
582-
pipeline.Spec.Stages = &cfClient.Stages{
583-
Stages: stages,
584-
}
578+
extractSpecAttributesFromOriginalYamlString(originalYamlString, pipeline)
585579
}
586580

587581
if _, ok := d.GetOk("spec.0.runtime_environment"); ok {
@@ -659,24 +653,30 @@ func mapResourceToPipeline(d *schema.ResourceData) *cfClient.Pipeline {
659653
return pipeline
660654
}
661655

662-
// extractStagesAndSteps extracts the steps and stages from the original yaml string to enable propagation in the `Spec` attribute of the pipeline
656+
// extractSpecAttributesFromOriginalYamlString extracts the steps and stages from the original yaml string to enable propagation in the `Spec` attribute of the pipeline
663657
// We cannot leverage on the standard marshal/unmarshal because the steps attribute needs to maintain the order of elements
664658
// while by default the standard function doesn't do it because in JSON maps are unordered
665-
func extractStagesAndSteps(originalYamlString string) (stages, steps string) {
659+
func extractSpecAttributesFromOriginalYamlString(originalYamlString string, pipeline *cfClient.Pipeline) {
666660
// Use mapSlice to preserve order of items from the YAML string
667661
m := yaml.MapSlice{}
668662
err := yaml.Unmarshal([]byte(originalYamlString), &m)
669663
if err != nil {
670-
log.Fatal("Unable to unmarshall original_yaml_string")
664+
log.Fatalf("Unable to unmarshall original_yaml_string. Error: %v", err)
671665
}
672666

673-
stages = "[]"
667+
stages := "[]"
674668
// Dynamically build JSON object for steps using String builder
675669
stepsBuilder := strings.Builder{}
676670
stepsBuilder.WriteString("{")
671+
// Dynamically build JSON object for steps using String builder
672+
hooksBuilder := strings.Builder{}
673+
hooksBuilder.WriteString("{")
674+
677675
// Parse elements of the YAML string to extract Steps and Stages if defined
678676
for _, item := range m {
679-
if item.Key == "steps" {
677+
key := item.Key.(string)
678+
switch key {
679+
case "steps":
680680
switch x := item.Value.(type) {
681681
default:
682682
log.Fatalf("unsupported value type: %T", item.Value)
@@ -694,17 +694,78 @@ func extractStagesAndSteps(originalYamlString string) (stages, steps string) {
694694
}
695695
}
696696
}
697-
}
698-
if item.Key == "stages" {
697+
case "stages":
699698
// For Stages we don't have ordering issue because it's a list
700699
y, _ := yaml.Marshal(item.Value)
701700
j2, _ := ghodss.YAMLToJSON(y)
702701
stages = string(j2)
702+
case "hooks":
703+
switch hooks := item.Value.(type) {
704+
default:
705+
log.Fatalf("unsupported value type: %T", item.Value)
706+
707+
case yaml.MapSlice:
708+
numberOfHooks := len(hooks)
709+
for indexHook, hook := range hooks {
710+
// E.g. on_finish
711+
hooksBuilder.WriteString("\"" + hook.Key.(string) + "\" : {")
712+
numberOfAttributes := len(hook.Value.(yaml.MapSlice))
713+
for indexAttribute, hookAttribute := range hook.Value.(yaml.MapSlice) {
714+
attribute := hookAttribute.Key.(string)
715+
switch attribute {
716+
case "steps":
717+
hooksBuilder.WriteString("\"steps\" : {")
718+
numberOfSteps := len(hookAttribute.Value.(yaml.MapSlice))
719+
for indexStep, step := range hookAttribute.Value.(yaml.MapSlice) {
720+
// We only need to preserve order at the first level to guarantee order of the steps, hence the child nodes can be marshalled
721+
// with the standard library
722+
y, _ := yaml.Marshal(step.Value)
723+
j2, _ := ghodss.YAMLToJSON(y)
724+
hooksBuilder.WriteString("\"" + step.Key.(string) + "\" : " + string(j2))
725+
if indexStep < numberOfSteps-1 {
726+
hooksBuilder.WriteString(",")
727+
}
728+
}
729+
hooksBuilder.WriteString("}")
730+
default:
731+
// For Other elements we don't need to preserve order
732+
y, _ := yaml.Marshal(hookAttribute.Value)
733+
j2, _ := ghodss.YAMLToJSON(y)
734+
hooksBuilder.WriteString("\"" + hookAttribute.Key.(string) + "\" : " + string(j2))
735+
}
736+
737+
if indexAttribute < numberOfAttributes-1 {
738+
hooksBuilder.WriteString(",")
739+
}
740+
}
741+
hooksBuilder.WriteString("}")
742+
if indexHook < numberOfHooks-1 {
743+
hooksBuilder.WriteString(",")
744+
}
745+
}
746+
}
747+
case "mode":
748+
pipeline.Spec.Mode = item.Value.(string)
749+
case "fail_fast":
750+
pipeline.Spec.FailFast = item.Value.(bool)
751+
default:
752+
log.Printf("Unsupported entry %s", key)
703753
}
704754
}
705755
stepsBuilder.WriteString("}")
706-
steps = stepsBuilder.String()
707-
return
756+
hooksBuilder.WriteString("}")
757+
steps := stepsBuilder.String()
758+
hooks := hooksBuilder.String()
759+
pipeline.Spec.Steps = &cfClient.Steps{
760+
Steps: steps,
761+
}
762+
pipeline.Spec.Stages = &cfClient.Stages{
763+
Stages: stages,
764+
}
765+
pipeline.Spec.Hooks = &cfClient.Hooks{
766+
Hooks: hooks,
767+
}
768+
708769
}
709770

710771
func getSupportedTerminationPolicyAttributes(policy string) map[string]interface{} {

codefresh/resource_pipeline_test.go

+84-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package codefresh
22

33
import (
44
"fmt"
5+
"reflect"
56
"regexp"
67
"testing"
78

@@ -165,7 +166,56 @@ func TestAccCodefreshPipeline_RuntimeEnvironment(t *testing.T) {
165166
func TestAccCodefreshPipeline_OriginalYamlString(t *testing.T) {
166167
name := pipelineNamePrefix + acctest.RandString(10)
167168
resourceName := "codefresh_pipeline.test"
168-
originalYamlString := "version: \"1.0\"\nsteps:\n test:\n image: alpine:latest\n commands:\n - echo \"ACC tests\""
169+
originalYamlString := `version: 1.0
170+
fail_fast: false
171+
stages:
172+
- test
173+
mode: parallel
174+
hooks:
175+
on_finish:
176+
steps:
177+
secondmycleanup:
178+
commands:
179+
- echo echo cleanup step
180+
image: alpine:3.9
181+
firstmynotification:
182+
commands:
183+
- echo Notify slack
184+
image: cloudposse/slack-notifier
185+
on_elected:
186+
exec:
187+
commands:
188+
- echo 'Creating an adhoc test environment'
189+
image: alpine:3.9
190+
annotations:
191+
set:
192+
- annotations:
193+
- my_annotation_example1: 10.45
194+
- my_string_annotation: Hello World
195+
entity_type: build
196+
steps:
197+
zz_firstStep:
198+
stage: test
199+
image: alpine
200+
commands:
201+
- echo Hello World First Step
202+
aa_secondStep:
203+
stage: test
204+
image: alpine
205+
commands:
206+
- echo Hello World Second Step`
207+
208+
expectedSpecAttributes := &cfClient.Spec{
209+
Steps: &cfClient.Steps{
210+
Steps: `{"zz_firstStep":{"commands":["echo Hello World First Step"],"image":"alpine","stage":"test"},"aa_secondStep":{"commands":["echo Hello World Second Step"],"image":"alpine","stage":"test"}}`,
211+
},
212+
Stages: &cfClient.Stages{
213+
Stages: `["test"]`,
214+
},
215+
Hooks: &cfClient.Hooks{
216+
Hooks: `{"on_finish":{"steps":{"secondmycleanup":{"commands":["echo echo cleanup step"],"image":"alpine:3.9"},"firstmynotification":{"commands":["echo Notify slack"],"image":"cloudposse/slack-notifier"}}},"on_elected":{"exec":{"commands":["echo 'Creating an adhoc test environment'"],"image":"alpine:3.9"},"annotations":{"set":[{"annotations":[{"my_annotation_example1":10.45},{"my_string_annotation":"Hello World"}],"entity_type":"build"}]}}}`,
217+
},
218+
}
169219

170220
resource.ParallelTest(t, resource.TestCase{
171221
PreCheck: func() { testAccPreCheck(t) },
@@ -178,6 +228,7 @@ func TestAccCodefreshPipeline_OriginalYamlString(t *testing.T) {
178228

179229
testAccCheckCodefreshPipelineExists(resourceName),
180230
resource.TestCheckResourceAttr(resourceName, "original_yaml_string", originalYamlString),
231+
testAccCheckCodefreshPipelineOriginalYamlStringAttributePropagation(resourceName, expectedSpecAttributes),
181232
),
182233
},
183234
{
@@ -426,6 +477,38 @@ func testAccCheckCodefreshPipelineDestroy(s *terraform.State) error {
426477
return nil
427478
}
428479

480+
func testAccCheckCodefreshPipelineOriginalYamlStringAttributePropagation(resource string, spec *cfClient.Spec) resource.TestCheckFunc {
481+
return func(state *terraform.State) error {
482+
483+
rs, ok := state.RootModule().Resources[resource]
484+
if !ok {
485+
return fmt.Errorf("Not found: %s", resource)
486+
}
487+
if rs.Primary.ID == "" {
488+
return fmt.Errorf("No Record ID is set")
489+
}
490+
491+
pipelineID := rs.Primary.ID
492+
493+
apiClient := testAccProvider.Meta().(*cfClient.Client)
494+
pipeline, err := apiClient.GetPipeline(pipelineID)
495+
496+
if !reflect.DeepEqual(pipeline.Spec.Steps, spec.Steps) {
497+
return fmt.Errorf("Expected Step %v. Got %v", spec.Steps, pipeline.Spec.Steps)
498+
}
499+
if !reflect.DeepEqual(pipeline.Spec.Stages, spec.Stages) {
500+
return fmt.Errorf("Expected Stages %v. Got %v", spec.Stages, pipeline.Spec.Stages)
501+
}
502+
if !reflect.DeepEqual(pipeline.Spec.Hooks, spec.Hooks) {
503+
return fmt.Errorf("Expected Hooks %v. Got %v", spec.Hooks, pipeline.Spec.Hooks)
504+
}
505+
if err != nil {
506+
return fmt.Errorf("error fetching pipeline with resource %s. %s", resource, err)
507+
}
508+
return nil
509+
}
510+
}
511+
429512
// CONFIGS
430513
func testAccCodefreshPipelineBasicConfig(rName, repo, path, revision, context string) string {
431514
return fmt.Sprintf(`

codefresh/resource_project.go

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package codefresh
22

33
import (
4+
"log"
5+
"time"
6+
7+
"github.com/cenkalti/backoff"
48
cfClient "github.com/codefresh-io/terraform-provider-codefresh/client"
59
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
610
)
@@ -90,8 +94,17 @@ func resourceProjectUpdate(d *schema.ResourceData, meta interface{}) error {
9094

9195
func resourceProjectDelete(d *schema.ResourceData, meta interface{}) error {
9296
client := meta.(*cfClient.Client)
93-
94-
err := client.DeleteProject(d.Id())
97+
// Adding a Retry backoff to address eventual consistency for the API
98+
expBackoff := backoff.NewExponentialBackOff()
99+
expBackoff.MaxElapsedTime = 2 * time.Second
100+
err := backoff.Retry(
101+
func() error {
102+
err := client.DeleteProject(d.Id())
103+
if err != nil {
104+
log.Printf("Unable to destroy Project due to error %v", err)
105+
}
106+
return err
107+
}, expBackoff)
95108
if err != nil {
96109
return err
97110
}

codefresh/resource_step_types.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ func mapResourceToStepTypesVersions(d *schema.ResourceData) *cfClient.StepTypesV
382382
return &stepTypesVersions
383383
}
384384

385-
// extractStagesAndSteps extracts the steps and stages from the original yaml string to enable propagation in the `Spec` attribute of the pipeline
385+
// extractSteps extracts the steps and stages from the original yaml string to enable propagation in the `Spec` attribute of the pipeline
386386
// We cannot leverage on the standard marshal/unmarshal because the steps attribute needs to maintain the order of elements
387387
// while by default the standard function doesn't do it because in JSON maps are unordered
388388
func extractSteps(stepTypesYaml string) (steps *orderedmap.OrderedMap) {

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ require (
55
github.com/aws/aws-sdk-go v1.30.12 // indirect
66
github.com/bflad/tfproviderdocs v0.6.0
77
github.com/bflad/tfproviderlint v0.14.0
8+
github.com/cenkalti/backoff v2.2.1+incompatible
9+
github.com/cenkalti/backoff/v4 v4.1.0
810
github.com/client9/misspell v0.3.4
911
github.com/dlclark/regexp2 v1.4.0
1012
github.com/ghodss/yaml v1.0.0

go.sum

+5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ github.com/bmatcuk/doublestar v1.2.1 h1:eetYiv8DDYOZcBADY+pRvRytf3Dlz1FhnpvL2FsC
5656
github.com/bmatcuk/doublestar v1.2.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
5757
github.com/bombsimon/wsl/v3 v3.0.0 h1:w9f49xQatuaeTJFaNP4SpiWSR5vfT6IstPtM62JjcqA=
5858
github.com/bombsimon/wsl/v3 v3.0.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
59+
github.com/cenkalti/backoff v1.1.0 h1:QnvVp8ikKCDWOsFheytRCoYWYPO/ObCTBGxT19Hc+yE=
60+
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
61+
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
62+
github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc=
63+
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
5964
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
6065
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
6166
github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=

0 commit comments

Comments
 (0)